Search code examples
c#socketssslsslstream

C# Read from SslStream continuously (long connection, last for up to days) and Efficiently without infinite loop


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.


Solution

  • 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.