StreamReader ReadLineAsync ignores carriage return and hangs

Viewed 316

I have simple TcpListener which accepts a pending connection and I read data from NetworkStream with using a StreamReader.ReadLineAsync.

TcpClient writes data to the stream.

According to documentation

A line is defined as a sequence of characters followed by a line feed ("\n"), a carriage return ("\r"), or a carriage return immediately followed by a line feed ("\r\n")

The problem is that StreamReader.ReadLineAsync ignores the carriage return and never reads the line.

Server:

static async Task Main(string[] args)
{
    var listener = TcpListener.Create(22);
    listener.Start();
    var client = await listener.AcceptTcpClientAsync();
    using (var networkStream = client.GetStream())
    {
        StreamReader sr = null;
        StreamWriter sw = null;
        try
        {
            sr = new StreamReader(networkStream, leaveOpen: true);
            sw = new StreamWriter(networkStream, leaveOpen: true);
            while (!sr.EndOfStream)
            {
                var clientData = await sr.ReadLineAsync();
                ...
            }
        }
        catch (Exception ex)
        {
            ...
        }
        finally
        {
            sr?.Close();
            sw?.Close();
        }
    }
}

Client:

static async Task Main(string[] args)
{
    // Server ReadLineAsync hangs for CR
    var data = new byte[] { 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 13 };
    
    // Server ReadLineAsync works for LF
    // var data = new byte[] { 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 10 };

    var client = new TcpClient("localhost", 22);
    using (var stream = client.GetStream())
    {
        await stream.WriteAsync(data, 0, data.Length);
        Thread.Sleep(5000);
    }
}

This client is only for testing purposes. The real client will be an external application which will send data ended by CR.

1 Answers

The StreamReader behaves exactly as described in documentation. Unfortunately it does it by waiting for next character following \r to see if it is \n or not.

Code for both sync and async version (reference code ) is roughly

 if (currentChar == `\r`) {
      nextChar = ReadNextCharFromBufferOrStream();
      if (nextChar == `\n`) {
        //ignore
      } else {
         UnRead(nextChar);
      }
      return resultUpToR;
 }
       

In your case stream is not closed yet but at the moment of the call the last character is \r - and as result reader sits and waits for next character - it will give you the string when stream is either closed or gets any other subsequent character.

If you want behavior where \r instantly terminates the string and return you need to write your own version of the "read line" that will return string as soon as it sees \r and skip \n if previous character was \r. Or switch to always sending \n which does not have this problem.

Related