1

I am completely new to C#, and need to encrypt the data sent and received between client and server, after googled it for two days, learnt the best way is to use SslStream, some answers I found give good examples but they all somehow assume we just need to read one message and then close the connection, which is totally not my case, I have to read whenever a user triggers his device to send a message through the persistent connection. one example from Microsoft documentation:

static string ReadMessage(SslStream sslStream)
    {
        // Read the  message sent by the client.
        // The client signals the end of the message using the
        // "<EOF>" marker.
        byte [] buffer = new byte[2048];
        StringBuilder messageData = new StringBuilder();
        int bytes = -1;
        do
        {
            // Read the client's test message.
            bytes = sslStream.Read(buffer, 0, buffer.Length);

            // Use Decoder class to convert from bytes to UTF8
            // in case a character spans two buffers.
            Decoder decoder = Encoding.UTF8.GetDecoder();
            char[] chars = new char[decoder.GetCharCount(buffer,0,bytes)];
            decoder.GetChars(buffer, 0, bytes, chars,0);
            messageData.Append (chars);
            // Check for EOF or an empty message. <------   In my case,I don't have EOF
            if (messageData.ToString().IndexOf("<EOF>") != -1)
            {
                break;
            }
        } while (bytes !=0);

        return messageData.ToString();
    }

and other answers actually tell me how to continuously read from a SslStream, but they are using infinite loop to do it, on the server side, there could be thousands clients connected to it, so the possible poor performance concerns me,like this one : Read SslStream continuously in C# Web MVC 5 project

So I want to know if there is a better way to continuously read from a persistent SslStream connection.

I know with bare socket I can use SocketAsyncEventArgs to know when there is new data ready, I hope I could do this with SslStream, probably I misunderstand something, any ideas would be appreciated, thanks in advance.

Qiu Zhou
  • 1,235
  • 8
  • 12
  • How SSLStream works https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream?view=netcore-3.1 – Nadeem Taj Aug 22 '20 at 09:55
  • The easy answer is to keep the infinite loop, but use `await ReadAsync(...`. While the method is paused & waiting for I/O, no thread will be executing. – Jeremy Lakeman Aug 24 '20 at 02:58
  • @JeremyLakeman That's the right answer. I did saw `ReadAsync` when I was searching for answers, but I misunderstood it would immediately return zero when there's no data in the buffer. It actually only return zero when the connection being closed. Many thanks! – Qiu Zhou Aug 24 '20 at 05:48

2 Answers2

1

Here's my shot at it. Instead of looping forever, I chose recursion. This method will return immediately but will fire an event when EOF is hit and continue to keep reading:

public static void ReadFromSSLStreamAsync(
    SslStream sslStream,
    Action<string> result,
    Action<Exception> error,
    StringBuilder stringBuilder = null)
{
    const string EOFToken = "<EOF>";

    stringBuilder = stringBuilder ?? new StringBuilder();
    var buffer = new byte[4096];

    try
    {
        sslStream.BeginRead(buffer, 0, buffer.Length, asyncResult =>
        {
            // Read all bytes avaliable from stream and then
            // add them to string builder
            {
                int bytesRead;
                try
                {
                    bytesRead = sslStream.EndRead(asyncResult);
                }
                catch (Exception ex)
                {
                    error?.Invoke(ex);
                    return;
                }

                // Use Decoder class to convert from bytes to
                // UTF8 in case a character spans two buffers.
                var decoder = Encoding.UTF8.GetDecoder();
                var buf = new char[decoder.GetCharCount(buffer, 0, bytesRead)];
                decoder.GetChars(buffer, 0, bytesRead, buf, 0);
                stringBuilder.Append(buf);
            }

            // Find the EOFToken, if found copy all data before the token
            // and send it to event, then remove it from string builder
            {
                int tokenIndex;
                while((tokenIndex = stringBuilder.ToString().IndexOf(EOFToken)) != -1)
                {
                    var buf = new char[tokenIndex];
                    stringBuilder.CopyTo(0, buf, 0, tokenIndex);
                    result?.Invoke(new string(buf));
                    stringBuilder.Remove(0, tokenIndex + EOFToken.Length);
                }
            }

            // Continue reading...
            ReadFromSSLStreamAsync(sslStream, result, error, stringBuilder);
        }, null);
    }
    catch(Exception ex)
    {
        error?.Invoke(ex);
    }
}

You could call it as so:

ReadFromSSLStreamAsync(sslStream, sslData =>
{
    Console.WriteLine($"Finished: {sslData}");
}, error =>
{
    Console.WriteLine($"Errored: {error}");
});

It's not TaskAsync, so you don't have to await on it. But it is asynchronous so your thread can go on to do other things.

Andy
  • 12,859
  • 5
  • 41
  • 56
  • Great pattern for reading from SslStream, but still missed some of my point, I need to read continuously from the stream, or say there are endless messages sequentially coming from a client at any time, every one of them ends with a __ . But I think just a little change to your code would make it works fine: simply move this line `ReadUntilEOFAsync(sslStream, result, error, stringBuilder);` out of _else_curly brace,it will keep reading more messages. – Qiu Zhou Aug 24 '20 at 06:10
  • @QiuZhou -- added that in. It will now continue to read after `` is found instead of stopping. – Andy Aug 24 '20 at 18:03
  • @QiuZhou -- keep in mind there could be a condition where a buffer reads in two or more `` tokens in one pass. I added in code for that condition as well. – Andy Aug 24 '20 at 18:18
  • 1
    @QiuZhou -- Also, the `ToString().IndexOf()` may be a bottleneck. You may consider doing something a little more optimized, like this: https://stackoverflow.com/a/12261971/1204153 or just scrap `StringBuilder` and use `Span` to make it really fast – Andy Aug 24 '20 at 18:29
0

Consider checking out the following asnwer. SSLStream was derived from the Stream class therefore the ReadAsnyc method can be used. Code below, read until the <EOF> delimiter characters then return with the received message as string.

    internal static readonly byte[] EOF = Encoding.UTF8.GetBytes("<EOF>");

    internal static async Task<string> ReadToEOFAsync(Stream stream)
    {
        byte[] buffer = new byte[8192];
        using (MemoryStream memoryStream = new MemoryStream())
        {
            long eofLength = EOF.LongLength;
            byte[] messageTail = new byte[eofLength];
            while (!messageTail.SequenceEqual(EOF))
            {
                int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
                await memoryStream.WriteAsync(buffer, 0, bytesRead);

                Array.Copy(memoryStream.GetBuffer(), memoryStream.Length - eofLength, messageTail, 0, eofLength);
            }

            // Truncate the EOF tail from the data stream
            byte[] result = new byte[memoryStream.Length - eofLength];
            Array.Copy(memoryStream.GetBuffer(), 0, result, 0, result.LongLength);

            return Encoding.UTF8.GetString(result);
        }
    }

The received messages was appended to the memoryStream. The first Array.Copy copies the message tail from the buffer. If the message tail is euqals to the <EOF> then it stops reading from the stream. Second copy is to ensure truncating the delimiter characters from the message.

Note: There is a more sophisticated way of slicing using Span introduced in .NET Core 2.1.

Péter Szilvási
  • 362
  • 4
  • 17