简体   繁体   中英

How to handle incoming TCP messages with a Kestrel ConnectionHandler?

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

  • What response should I send back to the client so that it does not run into a timeout?
  • When should I send back the response? When readResult.IsCompleted returns true?
  • How should I change the code to fetch the whole message sent by the client? Should I store each messageSegment in a List<string> and join it to a single string when readResult.IsCompleted returns true?
  1. that is entirely protocol dependent; in many cases, you're fine to do nothing; in others, there will be specific "ping"/"pong" frames to send if you just want to say "I'm still here"
  2. the "when" is entirely protocol dependent; waiting for 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)
  3. it sounds like you might indeed be writing a one-shot channel, ie the client only sends one thing to the server, and after that: the server only sends one thing to the client; in that case , you do something like:
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 a List<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:

  1. linearize the segments into a contiguous buffer, probably leasing an oversized buffer from ArrayPool<byte>.Shared , and use UTF8.GetString on the correct portion of the leased buffer
  2. use the GetDecoder() 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 API

Frankly, "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);
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM