# Transmisson Control Blocks This unit implement TCBs for use in [com/tcp]. It lives in a separate unit for clarity. The goal of the structures here is to manage TCP's "windowing" mechanics. Input buffers and Output buffers are separate structs. ## The Sequence Number Data ranges managed by TCB live in this virtual and big sequence, through which we have windows of data sliding. What we call a "sequence number" is an index into that virtual sequence. That number can get pretty big (and in fact, often begin at arbitrary large numbers at TCP connection initialization) but only a small window within that sequence is actually kept in buffers. In the document below, we refer to sequence numbers as "seqn". ### Relation with TCPMessage.SeqNum The "SeqNum" field of TCPMessage in [com/tcp] is very closely related to the sequence number in this unit. In general, the match is exact. That is, when receiving a payload, the value of SeqNum directly transfers to "seqn" here. When sending a payload, the number one need to put in SeqNum is the same as "seqn" reported here. ... except during SYN packets! In SYN packets, the value of SeqNum we need to send is the "orig" field of our output TCB *minus one*. Yes, that's how TCP is designed. Then, the ACKed number will be *plus one* of that number and will be the SeqNum actually expected by the other side in the following payload. ## Design choices On the incoming side, a fully efficient TCB would have to maintain a list of ranges it received, allowing for holes in the middle. Such a structure would add significant complexity for a case that, I think, doesn't happen very often, that is receiving chunks not only out of order, but with holes in the middle. We allow ourselves to simplify significantly at the cost of possibly under-acknowledging data, leading to needless retransmission. What we do is that we track a single range with a beginning seqn and an end seqn, which complement the concept of "last acknowledged seqn", which we call "origin seqn". The logic goes thus: 1. In a fully acknowleged state, beginning, end and origin are equal. 2. On an incoming range that begins at our origin seqn, we increase our three seqns and acknowledge. This is the happy path. 3. On an incoming range that begins later, we set beginning and end seqns to that new range, leaving origin behind. 4. On further incoming ranges, *if* the range is adjacent to either the beginning or the end seqn, we enlarge the range this way. If it's not, we ignore that range. It's lost and will lead to needless retransmission. 5. If that enlargement allows us to close the gap between "beginning" and "origin" fully, we acknowledge and reset our three seqns to that point. 6. If the received range is outside origin+windowsize, we ignore it. ## Implementation details Logic in this unit evacuates network-related logic such as timing, addresses, ports and packets to concentrate only on windowing and buffer management. These TCBs have a shared buffer pool. More often than not, TCP connections will have no outstanding data and when that happens, we don't want data buffers to be held up. Therefore, we have more TCBs than buffers and each TCB uses only a small amount of memory. TCB structures are designed to be "inlined" in connection structures from [com/tcp] which is why there is no convenience word to allocate them. We have TCBIn, for data coming in from the network and TCBOut, for data going out to the network. When receiving a TCP packet, we write to a TCBIn buffer and later, when whichever application opened the connection is ready to check on it, we read from that buffer, consuming the data in the buffer. When we want to send data out, we write to a TCBOut, then in the network idling loop, that contents is read from TCBOut in chunks and sent to the network, consuming data from the buffer. ## Buffer rotation The maximum window size of our TCBs is the size of each buffer. As the window slides forward, if we want to avoid having to copy the whole window "left" at each sliding operation, we need a rolling mechanism. Naively, one could think that we could use [mem/roll] for this, but unfortunately no, it can't work, we need to have the ability to read/write ahead of the "position" and it doesn't suite [io/stream] semantics well, not without ugly kludges. Our buffer rolling mechanism is controlled by the "bufseq" field, which is always smaller or equal to orig. That's the seqn where the buffer begins. This means that the contents between "bufseq" and "orig" is actually the contents from index "bufseq+MAXWINDOWSZ". And that's pretty much all there is to it. ## Origin and acknowledgment Theoretically, we could acknowledge incoming data as soon as we receive them. That would be the proper thing to do. However, our buffers are of limited size. If we acknowledge the sliding of our incoming window before we consume the data, it means that we put ourselves in the position of possibly receiving data we have no place for. Therefore, we acknowledge incoming data when *consuming* it, not when receiving it. ## Running out of buffers As written above, buffers are shared across TCBs. It makes it possible to be in a position where we don't have enough buffers for what we want to do. This can arise in two places, "tcbin" and "tcbwrite". It happens if TCB.buf at that time is zero and that all buffers of the pool are taken somewhere else. When this happens, there is no abort, it's just that "tcbin" and "tcbwrite" will write nothing. ## API ### Constants MAXWINDOWSZ Maximum window size, which is also the size of the buffers. BUFCNT Number of buffers in the pool. ### Structures struct TCB { uint buf bufseq orig last winsz ; } buf Pointer to buffer holding the TCB data. Can be 0 if there's no data. bufseq seqn of the beginning of the buffer. orig Origin Sequence number. The beginning of our buffer refers to that sequence. Anything before that has been fully synced. last seqn where our data ends. When the same as orig, the buffer is empty. winsz Size of the window in bytes, relative to orig. Incoming data past this window is discarded and we never send data past this window. It doesn't make much sense to set this to something else than MAXWINDOWSZ on the incoming side, but we still put it in the common struct. Who knows, it might be useful in some cases... extends TCB struct TCBIn { uint first ; } first seqn where received data begins. When the same as orig, it means that the data is ready to consume. Otherwise, it means there's a hole in the data, we need to wait for out-of-order data to fill that hole before we can consume it. extends TCB struct TCBOut { uint tosend ; } tosend seqn of the beginning of data that will be sent next. When orig is the same as tosend, it means that all sent data has been acknowledged. ### Words tcbin$ ( orig tcbin -- ) tcbout$ ( orig tcbout -- ) Free held buffer if any then initialize winsz to MAXWINDOWSZ and other fields to be consistent with the specified "orig". After initialization, a TCB is always "fully synced". winsz! ( sz tcb -- ) Set "winsz" field to "sz" or MAXWINDOWSZ, whichever is lower. tcbin ( a u seqn tcbin -- ) Process incoming range "a u" at specified seqn, writing it into "tcbin". tcbread ( n tcbin -- ?neworig ?a u ) If "tcbin" has data ready to be consumed, yield that range and slide window accordingly for a maximum of "n" bytes. "neworig" is the value of "orig" following that operation. In other words, when u<>0, a ACK should normally be sent out after calling this word with a new base seqn of "neworig". If there is nothing to read, u=0 and "a" and "neworig" are absent. Note that because of the rolling buffer mechanism, it's possible that consuming that range entirely requires two read operations (for the rollover). This word takes care of this by, in this case, copying those two ranges in a static buffer and yield that range. Therefore, there is no need to worry about generating spurious acknowledgements of "neworig". tcbout ( maxn tcbout -- ?seqn ?a u ) Send a maximum of "maxn" bytes out, yielding the corresponding range of outgoing data. If there is no such data, u=0 and "a" and "seqn" are absent. Like with "tcbread", this word handles rollover concatenation to avoid sending data in more packets than we have to. tcbwrite ( a u tcbout -- n ) Write specified range to tcbout and yield the number of bytes effectively written tcback ( seqn tcbout -- ) Acknowledge outgoing data up to "seqn", effectively freeing up space in the outgoing buffer. TODO: what happens if "seqn" is a number we don't expect, indicating that the other side needs resyncing? Design a way to bubble that situation up to [com/tcp] for a resync. tcbrewind ( tcbout -- ) Go back to last acknowledged seqn in the outgoing process. Therefore, the next seqn to be sent in "tcbout" will start at "orig". This happens on ACK timeouts. ### Private words findfreebuf ( -- buf-or-0 ) Iterate the buffer list and look for a free rolling buffer. If found, yield a pointer to it and mark it as used. Otherwise, yield 0. freebuf ( tcb -- ) If buf is nonzero, mark that buffer as unused and set it to 0. ?freebuf ( tcb -- ) If buf is nonzero and if that buffer is empty, free it. ?getbuf ( tcb -- buf-or-0 ) If buf is zero, find a free buffer and assign it to this tcb. Regardless of everything, yields the value of "buf" afterwards, which might be 0 if we're out of free buffers. For assignment integrity to work, one must absolutely write something to the buffer after having called this. buf[] ( n seqn tcb -- ??oa ou a u ) Yield a range corresponding to a maximum of "n" bytes at "seqn" on specified TCB. If that range exists, "a u" represents that range. If that range is in the rollover zone, it means that it's split in two and the range "oa ou" ("other" range) represent that range. Otherwise, ou=0 and "oa" is absent. This range will never exceed the capacity of the buffer, that is, "a u" will never go outside the buffer and "oa ou" will never overlap "orig", but otherwise this word doesn't check any other bounds. This implies a "?getbuf" call. peekahead ( n seqn tcb -- ?a u ) Same as buf[], but limits "n" to the range ending at "last" and, if that range is split because of the rollover, concatenate into a static buffer to yield it as a contiguous memory range. writeahead ( a u seqn tcb -- n ) Write specified range to ranges yielded by buf[], handling the possible dual- yield of it. Is that range is written beyond "last", update "last" so that it includes that newly written range. That extension *can* mean that there's a data hole between orig and last. This can happen in a TCBIn. In that case, the caller of that word must handle setting "first". This word yields the number of bytes effectively written. empty? ( tcb -- f ) Is TCB's buffer empty? True when orig=last.