# 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".

## 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.
