2017-11-24 17:10:24 +00:00
|
|
|
+--------------------+
|
|
|
|
| Peers protocol 2.1 |
|
|
|
|
+--------------------+
|
2017-11-15 13:41:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
Peers protocol has been implemented over TCP. Its aim is to transmit
|
|
|
|
stick-table entries information between several haproxy processes.
|
|
|
|
|
|
|
|
This protocol is symmetrical. This means that at any time, each peer
|
2021-10-29 19:59:33 +00:00
|
|
|
may connect to other peers they have been configured for, to send
|
2017-11-15 13:41:00 +00:00
|
|
|
their last stick-table updates. There is no role of client or server in this
|
|
|
|
protocol. As peers may connect to each others at the same time, the protocol
|
|
|
|
ensures that only one peer session may stay opened between a couple of peers
|
|
|
|
before they start sending their stick-table information, possibly in both
|
|
|
|
directions (or not).
|
|
|
|
|
|
|
|
|
|
|
|
Handshake
|
|
|
|
+++++++++
|
|
|
|
|
2021-10-29 19:59:33 +00:00
|
|
|
Just after having connected to another one, a peer must identify itself
|
2017-11-15 13:41:00 +00:00
|
|
|
and identify the remote peer, sending a "hello" message. The remote peer
|
|
|
|
replies with a "status" message.
|
|
|
|
|
|
|
|
A "hello" message is made of three lines terminated by a line feed character
|
|
|
|
as follows:
|
|
|
|
|
|
|
|
<protocol identifier> <version>\n
|
|
|
|
<remote peer identifier>\n
|
|
|
|
<local peer identifier> <process ID> <relative process ID>\n
|
|
|
|
|
|
|
|
protocol identifier : HAProxyS
|
|
|
|
version : 2.1
|
|
|
|
remote peer identifier: the peer name this "hello" message is sent to.
|
|
|
|
local peer identifier : the name of the peer which sends this "hello" message.
|
|
|
|
process ID : the ID of the process handling this peer session.
|
|
|
|
relative process ID : the haproxy's relative process ID (0 if nbproc == 1).
|
|
|
|
|
|
|
|
The "status" message is made of a unique line terminated by a line feed
|
|
|
|
character as follows:
|
|
|
|
|
|
|
|
<status code>\n
|
|
|
|
|
|
|
|
with these values as status code (a three-digit number):
|
|
|
|
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| status code | signification |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| 200 | Handshake succeeded |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| 300 | Try again later |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| 501 | Protocol error |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| 502 | Bad version |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| 503 | Local peer identifier mismatch |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
| 504 | Remote peer identifier mismatch |
|
|
|
|
+-------------+---------------------------------+
|
|
|
|
|
2021-10-29 19:59:33 +00:00
|
|
|
As the protocol is symmetrical, some peers may connect to each other at the
|
2017-11-15 13:41:00 +00:00
|
|
|
same time. For efficiency reasons, the protocol ensures there may be only
|
|
|
|
one TCP session opened after the handshake succeeded and before transmitting
|
2021-10-29 19:59:33 +00:00
|
|
|
any stick-table data information. In fact, for each couple of peers, this is
|
2017-11-15 13:41:00 +00:00
|
|
|
the last connected peer which wins. Each time a peer A receives a "hello"
|
|
|
|
message from a peer B, peer A checks if it already managed to open a peer
|
|
|
|
session with peer B, so with a successful handshake. If it is the case,
|
|
|
|
peer A closes its peer session. So, this is the peer session opened by B
|
|
|
|
which stays opened.
|
|
|
|
|
|
|
|
|
|
|
|
Peer A Peer B
|
|
|
|
hello
|
|
|
|
---------------------->
|
|
|
|
status 200
|
|
|
|
<----------------------
|
|
|
|
hello
|
|
|
|
<++++++++++++++++++++++
|
|
|
|
TCP/FIN-ACK
|
|
|
|
---------------------->
|
|
|
|
TCP/FIN-ACK
|
|
|
|
<----------------------
|
|
|
|
status 200
|
|
|
|
++++++++++++++++++++++>
|
|
|
|
data
|
|
|
|
<++++++++++++++++++++++
|
|
|
|
data
|
|
|
|
++++++++++++++++++++++>
|
|
|
|
data
|
|
|
|
++++++++++++++++++++++>
|
|
|
|
data
|
|
|
|
<++++++++++++++++++++++
|
|
|
|
.
|
|
|
|
.
|
|
|
|
.
|
|
|
|
|
|
|
|
As it is still possible that a couple of peers decide to close both their
|
|
|
|
peer sessions at the same time, the protocol ensures peers will not reconnect
|
|
|
|
at the same time, adding a random delay (50 up to 2050 ms) before any
|
|
|
|
reconnection.
|
|
|
|
|
|
|
|
|
|
|
|
Encoding
|
|
|
|
++++++++
|
|
|
|
|
|
|
|
As some TCP data may be corrupted, for integrity reason, some data fields
|
|
|
|
are encoded at peer session level.
|
|
|
|
|
|
|
|
The following algorithms explain how to encode/decode the data.
|
|
|
|
|
|
|
|
encode:
|
|
|
|
input : val (64bits integer)
|
|
|
|
output: bitf (variable-length bitfield)
|
|
|
|
|
|
|
|
if val has no bit set above bit 4 (or if val is less than 0xf0)
|
|
|
|
set the next byte of bitf to the value of val
|
|
|
|
return bitf
|
|
|
|
|
|
|
|
set the next byte of bitf to the value of val OR'ed with 0xf0
|
2018-11-14 00:55:16 +00:00
|
|
|
subtract 0xf0 from val
|
2017-11-15 13:41:00 +00:00
|
|
|
right shift val by 4
|
|
|
|
|
|
|
|
while val bit 7 is set (or if val is greater or equal to 0x80):
|
|
|
|
set the next byte of bitf to the value of the byte made of the last
|
|
|
|
7 bits of val OR'ed with 0x80
|
2018-11-14 00:55:16 +00:00
|
|
|
subtract 0x80 from val
|
2017-11-15 13:41:00 +00:00
|
|
|
right shift val by 7
|
|
|
|
|
|
|
|
set the next byte of bitf to the value of val
|
|
|
|
return bitf
|
|
|
|
|
|
|
|
decode:
|
|
|
|
input : bitf (variable-length bitfield)
|
|
|
|
output: val (64bits integer)
|
|
|
|
|
|
|
|
set val to the value of the first byte of bitf
|
|
|
|
if bit 4 up to 7 of val are not set
|
|
|
|
return val
|
|
|
|
|
|
|
|
set loop to 0
|
|
|
|
do
|
|
|
|
add to val the value of the next byte of bitf left shifted by (4 + 7*loop)
|
|
|
|
set loop to (loop + 1)
|
|
|
|
while the bit 7 of the next byte of bitf is set
|
|
|
|
return val
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
let's say that we must encode 0x1234.
|
|
|
|
|
|
|
|
"set the next byte of bitf to the value of val OR'ed with 0xf0"
|
|
|
|
=> bitf[0] = (0x1234 | 0xf0) & 0xff = 0xf4
|
|
|
|
|
2018-11-14 00:55:16 +00:00
|
|
|
"subtract 0xf0 from val"
|
2017-11-15 13:41:00 +00:00
|
|
|
=> val = 0x1144
|
|
|
|
|
|
|
|
right shift val by 4
|
|
|
|
=> val = 0x114
|
|
|
|
|
|
|
|
"set the next byte of bitf to the value of the byte made of the last
|
|
|
|
7 bits of val OR'ed with 0x80"
|
|
|
|
=> bitf[1] = (0x114 | 0x80) & 0xff = 0x94
|
|
|
|
|
2018-11-14 00:55:16 +00:00
|
|
|
"subtract 0x80 from val"
|
2017-11-15 13:41:00 +00:00
|
|
|
=> val= 0x94
|
|
|
|
|
|
|
|
"right shift val by 7"
|
|
|
|
=> val = 0x1
|
|
|
|
|
|
|
|
=> bitf[2] = 0x1
|
|
|
|
|
|
|
|
So, the encoded value of 0x1234 is 0xf49401.
|
|
|
|
|
|
|
|
To decode this value:
|
|
|
|
|
|
|
|
"set val to the value of the first byte of bitf"
|
|
|
|
=> val = 0xf4
|
|
|
|
|
|
|
|
"add to val the value of the next byte of bitf left shifted by 4"
|
|
|
|
=> val = 0xf4 + (0x94 << 4) = 0xf4 + 0x940 = 0xa34
|
|
|
|
|
|
|
|
"add to val the value of the next byte of bitf left shifted by (4 + 7)"
|
|
|
|
=> val = 0xa34 + (0x01 << 11) = 0xa34 + 0x800 = 0x1234
|
|
|
|
|
|
|
|
|
|
|
|
Messages
|
|
|
|
++++++++
|
|
|
|
|
|
|
|
*** General ***
|
|
|
|
|
|
|
|
After the handshake has successfully completed, peers are authorized to send
|
|
|
|
some messages to each others, possibly in both direction.
|
|
|
|
|
|
|
|
All the messages are made at least of a two bytes length header.
|
|
|
|
|
|
|
|
The first byte of this header identifies the class of the message. The next
|
|
|
|
byte identifies the type of message in the class.
|
|
|
|
|
|
|
|
Some of these messages are variable-length. Others have a fixed size.
|
|
|
|
Variable-length messages are identified by the value of the message type
|
|
|
|
byte. For such messages, it is greater than or equal to 128.
|
|
|
|
|
|
|
|
All variable-length message headers must be followed by the encoded length
|
|
|
|
of the remaining bytes (so the encoded length of the message minus 2 bytes
|
|
|
|
for the header and minus the length of the encoded length).
|
|
|
|
|
|
|
|
There exist four classes of messages:
|
|
|
|
|
|
|
|
+------------+---------------------+--------------+
|
|
|
|
| class byte | signification | message size |
|
|
|
|
+------------+---------------------+--------------+
|
|
|
|
| 0 | control | fixed (2) |
|
|
|
|
+------------+---------------------+--------------|
|
|
|
|
| 1 | error | fixed (2) |
|
|
|
|
+------------+---------------------+--------------|
|
|
|
|
| 10 | stick-table updates | variable |
|
|
|
|
+------------+---------------------+--------------|
|
|
|
|
| 255 | reserved | |
|
|
|
|
+------------+---------------------+--------------+
|
|
|
|
|
|
|
|
At this time of this writing, only control and error messages have a fixed
|
|
|
|
size of two bytes (header only). The stick-table updates messages are all
|
|
|
|
variable-length (their message type bytes are greater than 128).
|
|
|
|
|
|
|
|
|
|
|
|
*** Control message class ***
|
|
|
|
|
|
|
|
At this time of writing, control messages are fixed-length messages used
|
2019-03-26 15:17:33 +00:00
|
|
|
only to control the synchronizations between local and/or remote processes
|
|
|
|
and to emit heartbeat messages.
|
2017-11-15 13:41:00 +00:00
|
|
|
|
2019-03-26 15:17:33 +00:00
|
|
|
There exists five types of such control messages:
|
2017-11-15 13:41:00 +00:00
|
|
|
|
|
|
|
+------------+--------------------------------------------------------+
|
|
|
|
| type byte | signification |
|
|
|
|
+------------+--------------------------------------------------------+
|
|
|
|
| 0 | synchronisation request: ask a remote peer for a full |
|
|
|
|
| | synchronization |
|
|
|
|
+------------+--------------------------------------------------------+
|
|
|
|
| 1 | synchronization finished: signal a remote peer that |
|
|
|
|
| | local updates have been pushed and local is considered |
|
|
|
|
| | up to date. |
|
|
|
|
+------------+--------------------------------------------------------+
|
|
|
|
| 2 | synchronization partial: signal a remote peer that |
|
|
|
|
| | local updates have been pushed and local is not |
|
|
|
|
| | considered up to date. |
|
|
|
|
+------------+--------------------------------------------------------+
|
|
|
|
| 3 | synchronization confirmed: acknowledge a finished or |
|
|
|
|
| | partial synchronization message. |
|
|
|
|
+------------+--------------------------------------------------------+
|
2019-03-26 15:17:33 +00:00
|
|
|
| 4 | Heartbeat message. |
|
|
|
|
+------------+--------------------------------------------------------+
|
2017-11-15 13:41:00 +00:00
|
|
|
|
2019-03-26 15:17:33 +00:00
|
|
|
About hearbeat messages: a peer sends heartbeat messages to peers it is
|
|
|
|
connected to after periods of 3s of inactivity (i.e. when there is no
|
|
|
|
stick-table to synchronize for 3s). After a successful peer protocol
|
|
|
|
handshake between two peers, if one of them does not send any other peer
|
|
|
|
protocol messages (i.e. no heartbeat and no stick-table update messages)
|
|
|
|
during a 5s period, it is considered as no more alive by its remote peer
|
|
|
|
which closes the session and then tries to reconnect to the peer which
|
|
|
|
has just disappeared.
|
2017-11-15 13:41:00 +00:00
|
|
|
|
|
|
|
*** Error message class ***
|
|
|
|
|
|
|
|
There exits two types of such error messages:
|
|
|
|
|
|
|
|
+-----------+------------------+
|
|
|
|
| type byte | signification |
|
|
|
|
+-----------+------------------+
|
|
|
|
| 0 | protocol error |
|
|
|
|
+-----------+------------------+
|
|
|
|
| 1 | size limit error |
|
|
|
|
+-----------+------------------+
|
|
|
|
|
|
|
|
|
|
|
|
*** Stick-table update message class ***
|
|
|
|
|
|
|
|
This class is the more important one because it is in relation with the
|
|
|
|
stick-table entries handling between peers which is at the core of peers
|
|
|
|
protocol.
|
|
|
|
|
|
|
|
All the messages of this class are variable-length. Their type bytes are
|
|
|
|
all greater than or equal to 128.
|
|
|
|
|
|
|
|
There exits five types of such stick-table update messages:
|
|
|
|
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
| type byte | signification |
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
| 128 | Entry update |
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
| 129 | Incremental entry update |
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
| 130 | Stick-table definition |
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
| 131 | Stick-table switch (unused) |
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
| 133 | Update message acknowledgement |
|
|
|
|
+-----------+--------------------------------+
|
|
|
|
|
|
|
|
Note that entry update messages may be multiplexed. This means that different
|
|
|
|
entry update messages for different stick-tables may be sent over the same
|
|
|
|
peer session.
|
|
|
|
|
2018-11-14 00:55:16 +00:00
|
|
|
To do so, each time entry update messages have to sent, they must be preceded
|
2017-11-15 13:41:00 +00:00
|
|
|
by a stick-table definition message. This remains true for incremental entry
|
|
|
|
update messages.
|
|
|
|
|
|
|
|
As its name indicate, "Update message acknowledgement" messages are used to
|
|
|
|
acknowledge the entry update messages.
|
|
|
|
|
|
|
|
In this following paragraph, we give some information about the format of
|
|
|
|
each stick-table update messages. This very simple following legend will
|
|
|
|
contribute in understanding it. The unit used is the octet.
|
|
|
|
|
|
|
|
XX
|
|
|
|
+-----------+
|
|
|
|
| foo | Unique fixed sized "foo" field, made of XX octets.
|
|
|
|
+-----------+
|
|
|
|
|
|
|
|
+===========+
|
|
|
|
| foo | Variable-length "foo" field.
|
|
|
|
+===========+
|
|
|
|
|
|
|
|
+xxxxxxxxxxx+
|
|
|
|
| foo | Encoded variable-length "foo" field.
|
|
|
|
+xxxxxxxxxxx+
|
|
|
|
|
|
|
|
+###########+
|
|
|
|
| foo | hereunder described "foo" field.
|
|
|
|
+###########+
|
|
|
|
|
|
|
|
|
|
|
|
With this legend, all the stick-table update messages have such a header:
|
|
|
|
|
|
|
|
1 1
|
|
|
|
+--------------------+------------------------+xxxxxxxxxxxxxxxx+
|
|
|
|
| Message Class (10) | Message type (128-133) | Message length |
|
|
|
|
+--------------------+------------------------+xxxxxxxxxxxxxxxx+
|
|
|
|
|
|
|
|
Note that to help in making communicate different versions of peers protocol,
|
|
|
|
such stick-table update messages may be extended adding non mandatory
|
|
|
|
fields at the end of such messages, announcing a total message length
|
|
|
|
which is greater than the message length of the previous versions of
|
|
|
|
peers protocol. After having parsed such messages, the remaining ones
|
|
|
|
will be skipped to parse the next message.
|
|
|
|
|
|
|
|
- Definition message format:
|
|
|
|
|
|
|
|
Before sending entry update messages, a peer must announce the configuration
|
|
|
|
of the stick-table in relation with these messages thanks to a
|
|
|
|
"Stick-table definition" message with such a following format:
|
|
|
|
|
|
|
|
+xxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxx+==================+
|
|
|
|
| Stick-table ID | Stick-table name length | Stick-table name |
|
|
|
|
+xxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxx+==================+
|
|
|
|
|
|
|
|
+xxxxxxxxxxxx+xxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxx+
|
|
|
|
| Key type | Key length | Data types bitfield | Expiry |
|
|
|
|
+xxxxxxxxxxxx+xxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxx+
|
|
|
|
|
|
|
|
+xxxxxxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+
|
|
|
|
| Frequency counter #1 | Frequency counter #1 period |
|
|
|
|
+xxxxxxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+
|
|
|
|
|
|
|
|
+xxxxxxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+
|
|
|
|
| Frequency counter #2 | Frequency counter #2 period |
|
|
|
|
+xxxxxxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+
|
|
|
|
.
|
|
|
|
.
|
|
|
|
.
|
|
|
|
|
|
|
|
Note that "Stick-table ID" field is an encoded integer which is used to
|
|
|
|
identify the stick-table without using its name (or "Stick-table name"
|
|
|
|
field). It is local to the process handling the stick-table. So we can have
|
|
|
|
two peers attached to processes which generate stick-table updates for
|
|
|
|
the same stick-table (same name) but with different stick-table IDs.
|
|
|
|
|
|
|
|
Also note that the list of "Frequency counter #X" and their associated
|
|
|
|
periods fields exists only if their underlying types are already defined
|
|
|
|
in "Data types bitfield" field.
|
|
|
|
|
|
|
|
"Expiry" field and the remaining ones are not used by all the existing
|
|
|
|
version of haproxy peers. But they are MANDATORY, so that to make a
|
|
|
|
stick-table aggregator peer be able to autoconfigure itself.
|
|
|
|
|
|
|
|
|
|
|
|
- Entry update message format:
|
|
|
|
4
|
|
|
|
+-----------------+###########+############+
|
|
|
|
| Local update ID | Key | Data |
|
|
|
|
+-----------------+###########+############+
|
|
|
|
|
|
|
|
with "Key" described as follows:
|
|
|
|
|
|
|
|
+xxxxxxxxxxx+=======+
|
|
|
|
| length | value | if key type is (non null terminated) "string",
|
|
|
|
+xxxxxxxxxxx+=======+
|
|
|
|
|
|
|
|
4
|
|
|
|
+-------+
|
|
|
|
| value | if key type is "integer",
|
|
|
|
+-------+
|
|
|
|
|
|
|
|
+=======+
|
|
|
|
| value | for other key types: the size is announced in
|
|
|
|
+=======+ the previous stick-table definition message.
|
|
|
|
|
|
|
|
"Data" field is basically a list of encoded values for each type announced
|
|
|
|
by the "Data types bitfield" field of the previous "Stick-table definition"
|
|
|
|
message:
|
|
|
|
|
|
|
|
+xxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxx+ +xxxxxxxxxxxxxxxxxxxx+
|
|
|
|
| Data type #1 value | Data type #2 value | .... | Data type #n value |
|
|
|
|
+xxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxx+ +xxxxxxxxxxxxxxxxxxxx+
|
|
|
|
|
|
|
|
|
2019-06-06 13:53:20 +00:00
|
|
|
Most of these fields are internally stored as uint32_t (see STD_T_SINT,
|
|
|
|
STD_T_UINT, STD_T_ULL C enumerations) or structures made of several uint32_t
|
|
|
|
(see STD_T_FRQP C enumeration). The remaining one STD_T_DICT is internally
|
|
|
|
used to store entries of LRU caches for others literal dictionary entries
|
|
|
|
(couples of IDs associated to strings). It is used to transmit these cache
|
|
|
|
entries as follows:
|
|
|
|
|
|
|
|
+xxxxxxxxxxx+xxxx+xxxxxxxxxxxxxxx+========+
|
|
|
|
| length | ID | string length | string |
|
|
|
|
+xxxxxxxxxxx+xxxx+xxxxxxxxxxxxxxx+========+
|
|
|
|
|
|
|
|
"length" is the length in bytes of the remaining data after this "length" field.
|
|
|
|
"string length" is the length of "string" field which follows.
|
|
|
|
|
|
|
|
Here the cache is used so that not to have to send again and again an already
|
|
|
|
sent string. Indeed, the second time we have to send the same dictionary entry,
|
|
|
|
if still cached, a peer sends only its ID:
|
|
|
|
|
|
|
|
+xxxxxxxxxxx+xxxx+
|
|
|
|
| length | ID |
|
|
|
|
+xxxxxxxxxxx+xxxx+
|
2017-11-15 13:41:00 +00:00
|
|
|
|
|
|
|
- Update message acknowledgement format:
|
|
|
|
|
|
|
|
These messages are responses to "Entry update" messages only.
|
|
|
|
|
|
|
|
Its format is very basic for efficiency reasons:
|
|
|
|
|
|
|
|
4
|
|
|
|
+xxxxxxxxxxxxxxxx+-----------+
|
|
|
|
| Stick-table ID | Update ID |
|
|
|
|
+xxxxxxxxxxxxxxxx+-----------+
|
|
|
|
|
|
|
|
|
|
|
|
Note that the "Stick-table ID" field value is in relation with the one which
|
|
|
|
has been previously announce by a "Stick-table definition" message.
|
|
|
|
|
|
|
|
The following schema may help in understanding how to handle a stream of
|
|
|
|
stick-table update messages. The handshake step is not represented.
|
|
|
|
Stick-table IDs are preceded by a '#' character.
|
|
|
|
|
|
|
|
|
|
|
|
Peer A Peer B
|
|
|
|
|
|
|
|
stkt def. #1
|
|
|
|
---------------------->
|
|
|
|
updates (1-5)
|
|
|
|
---------------------->
|
|
|
|
stkt def. #3
|
|
|
|
---------------------->
|
|
|
|
updates (1000-1005)
|
|
|
|
---------------------->
|
|
|
|
|
|
|
|
stkt def. #2
|
|
|
|
<----------------------
|
|
|
|
updates (10-15)
|
|
|
|
<----------------------
|
|
|
|
ack 5 for #1
|
|
|
|
<----------------------
|
|
|
|
ack 1005 for #3
|
|
|
|
<----------------------
|
|
|
|
stkt def. #4
|
|
|
|
<----------------------
|
|
|
|
updates (100-105)
|
|
|
|
<----------------------
|
|
|
|
|
|
|
|
ack 10 for #2
|
|
|
|
---------------------->
|
|
|
|
ack 105 for #4
|
|
|
|
---------------------->
|
|
|
|
(from here, on both sides, all stick-table updates
|
|
|
|
are considered as received)
|
|
|
|
|