Home > Software design >  How to close a TcpClient cleanly so the server reads end of stream instead of throwing a System.IO.I
How to close a TcpClient cleanly so the server reads end of stream instead of throwing a System.IO.I

Time:09-14

I have separate client and server apps written in C#. When I test the server by connecting to it with PuTTY, when I close PuTTY the server detects end of stream and exits nicely at the return statement in the function below. But when I use my own client application, the NetworkStream on the server side always throws a System.IO.IOException "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.", with an InnerException of type System.Net.Sockets.SocketException "An existing connection was forcibly closed by the remote host.".

The server code looks like this (LineReader and ProcessCommand are my own code).

    public static async Task ServeStream(Stream stream, CancellationToken token)
    {
        LineReader reader = new LineReader(stream, Encoding.ASCII, 1024);

        while (!token.IsCancellationRequested && !reader.EndOfStream)
        {
            string command = await reader.ReadLineAsync(token);

            if (reader.EndOfStream)
                return;

            byte[] response = await ProcessCommand(command);
            stream.Write(response, 0, response.Length);
        }
    }

The stream passed in is a NetworkStream obtained from TcpClient.GetStream(). The TcpClient in turn was obtained from TcpListener.AcceptTcpClientAsync().

And here is ReadLineAsync():

    public async Task<string> ReadLineAsync(CancellationToken token)
    {
        if (lines.Count > 0)
            return lines.Dequeue();

        for (; ; )
        {
            int n = await Stream.ReadAsync(buffer, 0, buffer.Length, token);

            if (n <= 0)
            {
                EndOfStream = true;
                return string.Empty;
            }

            // ... lots of buffer management ... 
        }
    }

On the server side I am also using a TcpClient and a NetworkStream. I have tried calling TcpClient.Close(), TcpClient.Client.Close(), and NetworkStream.Close() (TcpClient.Client is the underlying socket). In all three cases I get the above exception from Stream.ReadAsync() inside reader.ReadLineAsync(), rather than the clean end of stream I get when disconnecting PuTTY from my server. Is there any way to fix this?

CodePudding user response:

It's hard to tell what's going on in your custom line reader. There are a variety of different reasons this could be occurring and it's hard to tell what the root cause is, especially given that it's happening inside your custom line reader which we can't see.

First, I'd recommend removing the `token' (cancellation token) from the ReadLineAsync() to see if that's causing the error.

Second, in your client code make sure you're closing the stream and the client in succession. You should not need the line marked #1, but depending on what you're doing on the client side it may solve the problem

CLIENT

using var client = new TcpClient(server, port);
using var stream = client.GetStream();

// YOUR APP CODE

// 1. try flushing the stream [UPDATE]
await stream.FlushAsync();

// 2. give this a try if you get error w/ #3 without it
client.Client.Shutdown(SocketShutdown.Both);

// 3. make close both in order
stream.Close(10000); //allow for timeout
client.Close();

SERVER

I would recommend switching to StreamReader if you're line endings are terminated with supported some combination of CR and LF. It's incredibly fast and battle tested. Again, you can use pass the cancellation token to the ReadLineAsync() command if you want, but it doesn't seem meaningful given your use case and may throw an unwanted exception (based upon your question).

public static async Task ServeStream(Stream stream, CancellationToken token)
{
     var command = default;
     using var reader = new StreamReader(stream, Encoding.ASCII);
     while ((command = await reader.ReadLineAsync()) != null)
     {
        byte[] response = await ProcessCommand(command);
        stream.Write(response, 0, response.Length);
    }
}

Alternatively, I'd suggest using StreamWriter as well. It handles all of the buffering and edge cases for your...

public static async Task ServeStream(Stream stream, CancellationToken token)
{
     var command = default;
     using var reader = new StreamReader(stream, Encoding.ASCII);
     using var writer = new StreamWriter(stream, Encoding.ASCII);
     while ((command = await reader.ReadLineAsync()) != null)
     {
        string response = await ProcessCommand(command);
        await writer.WriteLineAsync(response);
    }
}

CodePudding user response:

Of course, the first thing I always ask is "are you sure you need TCP/IP?" Because it's hard and unless there's a really good reason to use it, then you shouldn't.

That said, you need to call Shutdown before Close/Dispose. I explain this here in my series on asynchronous TCP/IP.

  • Related