Repository navigation
Connections and Framing
This page applies to TcpServer and TcpServerSsl. With UDP, every datagram is already one message.
A client can send any number of requests over one connection:
using var server = new MockServer(new TcpServer(0));
server.Mock.Send("ping").Receive("pong");
server.Mock.Send("pong").Receive("ping");
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
Assert.Equal("pong", await client.SendAndReceiveAsync("ping"));
Assert.Equal("ping", await client.SendAndReceiveAsync("pong"));
Assert.Equal("pong", await client.SendAndReceiveAsync("ping"));How connections are handled:
- Independently. A slow or silent client never blocks the others.
- In order. Responses on one connection are sent in the order of their requests, even when some are delayed.
-
Closed when needed. A connection closes when the client closes it, when the server stops, after
Disconnect()orAndDisconnect(), or when a request has no matching response. If the client stops sending (half-close), it still gets the responses to the requests it already sent.
Some clients send a request and then read until the server closes the connection. For those, set KeepAlive = false:
using var server = new MockServer(new TcpServer(0) { KeepAlive = false });
server.Mock.Send("GET").Receive("data");
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync("GET");
// The client can read until the server closes the connection.
Assert.Equal("data", await client.ReadToEndAsync());To close after only some responses, use .AndDisconnect() on those responses instead; see Simulating Failures.
TCP is a stream of bytes. It doesn't keep message boundaries: two messages can arrive together, and one message can arrive in several pieces. Framing tells the server how to split the stream back into messages.
| Framing | A request is | A response is sent as |
|---|---|---|
MessageFraming.None (default) |
Whatever arrives in one burst | It is |
MessageFraming.Delimiter("\n") |
The bytes up to the delimiter (the delimiter is removed) | The response + the delimiter |
MessageFraming.LengthPrefix(2) |
A length, then that many bytes (the prefix is removed) | A length prefix + the response |
MessageFraming.LengthPrefix(2, bigEndian: true, includesPrefix: true) |
Like LengthPrefix, but the length counts the prefix too |
Same, with the prefix counted in the length |
MessageFraming.FixedLength(8) |
Every 8 bytes (nothing is removed) | The response, padded on the right to 8 bytes |
MessageFraming.StxEtx / MessageFraming.StartEnd(start, end)
|
The bytes between a start and an end byte (both removed) | Start byte + the response + end byte |
Your own IMessageFraming
|
Whatever you decide | Whatever you decide |
The default works when the client sends one request and waits for the response before sending the next. Use framing when your protocol defines message boundaries, or when your client sends several requests without waiting.
Requests are matched after framing is removed, so you configure Send("QUIT"), not Send("QUIT\r\n").
using var server = new MockServer(new TcpServer(0) { Framing = MessageFraming.Delimiter("\r\n") });
server.Mock.Send("HELO client").Receive("250 Hello");
server.Mock.Send("QUIT").Receive("221 Bye").AndDisconnect();
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
// Two commands in one packet are still two requests; responses get "\r\n" appended.
await client.SendAsync("HELO client\r\nQUIT\r\n");
Assert.Equal("250 Hello\r\n221 Bye\r\n", await client.ReadToEndAsync());A request split over several packets is put back together:
using var server = new MockServer(new TcpServer(0) { Framing = MessageFraming.Delimiter("\n") });
server.Mock.Send("hello world").Receive("hi");
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync("hello ");
await client.SendAsync("world\n");
Assert.Equal("hi\n", await client.ReceiveAsync());The delimiter can be any text or bytes: MessageFraming.Delimiter(new byte[] { 0x00 }).
Don't include the delimiter in your configured responses, because it is appended for you.
MessageFraming.LengthPrefix(prefixLength, bigEndian) reads a 1, 2 or 4-byte unsigned length before each message.
The default is a 4-byte big-endian length.
var framing = MessageFraming.LengthPrefix(prefixLength: 2, bigEndian: true);
using var server = new MockServer(new TcpServer(0) { Framing = framing });
server.Mock.Send(new byte[] { 0x01, 0x02 }).Receive(new byte[] { 0xAA, 0xBB, 0xCC });
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync(new byte[] { 0x00, 0x02, 0x01, 0x02 }); // length 2, then the payload
var response = await client.ReceiveExactlyAsync(5);
Assert.Equal(new byte[] { 0x00, 0x03, 0xAA, 0xBB, 0xCC }, response);framing.Encode(bytes) adds the prefix for you, which is handy when building requests in a test.
For a little-endian length, pass bigEndian: false:
using var server = new MockServer(new TcpServer(0) { Framing = MessageFraming.LengthPrefix(4, bigEndian: false) });
server.Mock.Send(new byte[] { 0x01 }).Receive(new byte[] { 0xAA, 0xBB });
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x01 }); // length 1 (little-endian), then the payload
var response = await client.ReceiveExactlyAsync(6);
Assert.Equal(new byte[] { 0x02, 0x00, 0x00, 0x00, 0xAA, 0xBB }, response);Some protocols count the prefix in the length. With includesPrefix: true the length is the size of the whole frame,
so a 3-byte payload behind a 2-byte prefix has the length 5:
var framing = MessageFraming.LengthPrefix(2, bigEndian: true, includesPrefix: true);
using var server = new MockServer(new TcpServer(0) { Framing = framing });
server.Mock.Send(new byte[] { 0x01, 0x02, 0x03 }).Receive(new byte[] { 0xAA });
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync(new byte[] { 0x00, 0x05, 0x01, 0x02, 0x03 }); // length 5 = 2 prefix bytes + 3 payload bytes
var response = await client.ReceiveExactlyAsync(3);
Assert.Equal(new byte[] { 0x00, 0x03, 0xAA }, response);A length smaller than the prefix itself can never be valid. The server closes that one connection and reports it
through ConnectionFailed and the log; other connections and the server keep running. A response that doesn't fit
in the prefix throws InvalidOperationException.
When a length is invalid, messages decoded earlier in the same burst are dropped together with the connection.
A 4-byte length that is negative (for example FF FF FF FF) is invalid as well: that connection is closed and reported the
same way, instead of the server waiting for a message that never completes.
A connection keeps the bytes it received until they form a complete message. MaxBufferedBytes (default 0, unlimited) limits
that: a connection that holds more bytes than the limit without a complete message is closed and reported through
ConnectionFailed and the log (InvalidDataException), while other connections keep working. A single message larger than
the limit is refused too. Servers created from a configuration file use 16 MiB unless the file says otherwise.
new TcpServer(0) { Framing = MessageFraming.Delimiter("\n"), MaxBufferedBytes = 64 * 1024 };MessageFraming.FixedLength(length, padding) treats every length bytes as one message. Requests are delivered
as they are, padding included, so configure the padded text. A shorter response is padded on the right with
padding (default 0); a longer one throws InvalidOperationException.
using var server = new MockServer(new TcpServer(0) { Framing = MessageFraming.FixedLength(8, padding: (byte)' ') });
server.Mock.Send("PING ").Receive("PONG");
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync("PING ");
Assert.Equal("PONG ", await client.ReceiveAsync());MessageFraming.StartEnd(start, end) wraps messages in two marker bytes, and MessageFraming.StxEtx is the
common case 0x02 ... 0x03. The markers are removed from requests and added to responses; bytes outside a message
are ignored, and a message split over several packets is put back together. There is no escaping and no checksum, so
an end byte inside the payload ends the message. The start and end byte must differ.
using var server = new MockServer(new TcpServer(0) { Framing = MessageFraming.StxEtx });
server.Mock.Send("STATUS").Receive("OK");
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
await client.SendAsync(new byte[] { 0x02 }.Concat("STATUS".GetBytes()).Append((byte)0x03).ToArray());
var response = await client.ReceiveBytesAsync();
Assert.Equal(new byte[] { 0x02, (byte)'O', (byte)'K', 0x03 }, response);Implement IMessageFraming for any other format. This one handles messages wrapped in STX (0x02) and ETX (0x03), like the built-in StxEtx, to show how it's done:
public sealed class StxEtxFraming : IMessageFraming
{
private const byte Stx = 0x02;
private const byte Etx = 0x03;
public IReadOnlyList<byte[]> Decode(ReadOnlySpan<byte> data, bool endOfBurst, out int consumed)
{
var messages = new List<byte[]>();
consumed = 0;
while (true)
{
var start = data[consumed..].IndexOf(Stx);
if (start < 0) { consumed = data.Length; break; } // no frame start: drop the noise
var end = data[(consumed + start)..].IndexOf(Etx);
if (end < 0) { consumed += start; break; } // incomplete frame: wait for more data
messages.Add(data.Slice(consumed + start + 1, end - 1).ToArray());
consumed += start + end + 1;
}
return messages;
}
public byte[] Encode(byte[] response) => new[] { Stx }.Concat(response).Append(Etx).ToArray();
}using var server = new MockServer(new TcpServer(0) { Framing = new StxEtxFraming() });
server.Mock.Send("STATUS").Receive("OK");How Decode is called:
-
dataholds every byte received on the connection that isn't part of a message yet. - Return each complete message at the start of
data, and setconsumedto the number of bytes they used. Bytes you don't consume are kept and passed in again, with more data, on the next call. -
endOfBurstistruewhen no more data is waiting on the connection right now.MessageFraming.Noneuses it to treat a whole burst as one message.
Encode is called for every non-empty response. An empty response sends nothing, with any framing.
Runnable code: ConnectionAndFramingSamples.cs