If you ever face a task that involves tricky low-level network communication, you have to consider that network might (and most likely will) partition data that are being sent over.
Let’s say, there is a TCP server that receives messages from clients. When a client connects to the server and pushes some bytes to a socket, if the size of the data is small enough, the whole message will be sent as a single chunk. But if the size of the data is above a certain limit, the message will be split into multiple chunks. So you have to take care of concatenating these chunks on the receiving side.
Consider a client sends two messages — hello
and world
— one after another. Depending on a network, the server might receive these messages in two chunks, i.e.:
Or in four chunks:
In case if data is under your control and you can be sure that a payload wouldn't contain some special character, such as a line feed, then you can use it as a delimiter to indicate the end of a message. I.e. once a receiver encounters \n
byte, it means the message is fully received.
But in case if a payload is arbitrary user input, it is not reliable to rely on delimiters of any kind. The solution is to encode a length of a message into a payload sent to a receiving part.
When sending a string, it needs to be represented as bytes first:
The length of this message is 5
. To transfer this information to a receiver, we can get a memory representation of this integer in big-endian byte order.
Both parties must agree on a type of this integer. I will elaborate on why in a sec.
Let's pick a u32
as a type for a length. In Rust, a size of 32-bit unsigned integer (u32
) is 4 bytes. So the memory representation of 5
of type u32
is:
We can get this representation by calling to_be_bytes
method.
Now, we can concatenate these 2 arrays and shape a payload: the bytes that will be sent over the network.
When the receiver sees new data, it must take the first 4 bytes and convert them back to a 32-bit integer to get the length of the incoming message. This is why the type is so important: 16-bit unsigned integer (u16
) would take 2 bytes, so the receiver would need to eject 2 first bytes instead of 4 and use a different function for the conversion.
When the length of the message is known, the receiver keeps reading bytes from the socket into some buffer until the size of the buffer is equal to the length of the message.
Consider the case, when a network splits the message above into two chunks.
Since the length of the buffer is only 2
, the receiver knows that this is only a part of the message, so it waits for the next chunk.
Now, the length of the buffer is 5
, which means the whole message was received and it can be used for whatever.
Implementation of a TCP client-server pair, where the client sends a string to the server and the server prints messages to a stdout.
And the server half:
You can poke this code by forking this repo.
Have fun!