I want to create a TCP listener for my .NET Core project. I'm using Kestrel and configured a new ConnectionHandler
for this via
kestrelServerOptions.ListenLocalhost(5000, builder =>
{
builder.UseConnectionHandler<MyTCPConnectionHandler>();
});
So what I have so far is
internal class MyTCPConnectionHandler : ConnectionHandler
{
public override async Task OnConnectedAsync(ConnectionContext connection)
{
IDuplexPipe pipe = connection.Transport;
PipeReader pipeReader = pipe.Input;
while (true)
{
ReadResult readResult = await pipeReader.ReadAsync();
ReadOnlySequence<byte> readResultBuffer = readResult.Buffer;
foreach (ReadOnlyMemory<byte> segment in readResultBuffer)
{
// read the current message
string messageSegment = Encoding.UTF8.GetString(segment.Span);
// send back an echo
await pipe.Output.WriteAsync(segment);
}
if (readResult.IsCompleted)
{
break;
}
pipeReader.AdvanceTo(readResultBuffer.Start, readResultBuffer.End);
}
}
}
When sending messages from a TCP client to the server application the code works fine. The line await pipe.Output.WriteAsync(segment);
is acting like an echo for now.
Some questions come up
readResult.IsCompleted
returns true?messageSegment
in a List<string>
and join it to a single string when readResult.IsCompleted
returns true?readResult.IsCompleted
means that you're waiting for the inbound socket to be marked as closed, which means you won't send anything until the client closes their outbound socket; for single-shot protocols, that might be fine; but in most cases, you'll want to look for a single inbound frame, and reply to that frame (and repeat)while (true)
{
var readResult = await pipeReader.ReadAsync();
if (readResult.IsCompleted)
{
// TODO: not shown; process readResult.Buffer
// tell the pipe that we consumed everything, and exit
pipeReader.AdvanceTo(readResultBuffer.End, readResultBuffer.End);
break;
}
else
{
// wait for the client to close their outbound; tell
// the pipe that we couldn't consume anything
pipeReader.AdvanceTo(readResultBuffer.Start, readResultBuffer.End);
}
As for:
Should I store each
messageSegment
in aList<string>
and join it to a single string when
The first thing to consider here is that it is not necessarily the case that each buffer segment contains an exact number of characters. Since you are using UTF-8, which is a multi-byte encoding, a segment might contain fractions of characters at the start and end, so: decoding it is a bit more involved than that.
Because of this, it is common to check IsSingleSegment
on the buffer; if this is true, you can just use simple code:
if (buffer.IsSingleSegment)
{
string message = Encoding.UTF8.GetString(s.FirstSpan);
DoSomethingWith(message);
}
else
{
// ... more complex
}
The discontiguous buffer case is much harder; basically, you have two choices here:
ArrayPool<byte>.Shared
, and use UTF8.GetString
on the correct portion of the leased bufferGetDecoder()
API on the encoding, and use that to populate a new string, which on older frameworks means overwriting a newly allocated string, or in newer frameworks means using the string.Create
APIFrankly, "1" is much simpler. For example (untested):
public static string GetString(in this ReadOnlySequence<byte> payload,
Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
return payload.IsSingleSegment ? encoding.GetString(payload.FirstSpan)
: GetStringSlow(payload, encoding);
static string GetStringSlow(in ReadOnlySequence<byte> payload, Encoding encoding)
{
// linearize
int length = checked((int)payload.Length);
var oversized = ArrayPool<byte>.Shared.Rent(length);
try
{
payload.CopyTo(oversized);
return encoding.GetString(oversized, 0, length);
}
finally
{
ArrayPool<byte>.Shared.Return(oversized);
}
}
}