Search code examples
c#.nettcpclient.net-coretcplistener

Explain weird behavior of C# TcpClient/TcpListener on NetCore


I'm running NetCore on Windows 10, I have two programs - server and client that I am running both locally.

The execution sequence is as follows:

  • Run server - there is a loop for handling clients
  • Run client - client does its jobs and the process terminates
  • Run client for the second time

This sequence produces the following output from server using the code below:

Waiting for client.
Reading message.
Incoming message: This message is longer than what
Sending message.
Closing connection.
Waiting for client.
Reading message.
Incoming message: This message is longer than what
Sending message.
Closing connection.
Waiting for client.

and following output from client:

Connecting to server.
Sending message.
Reading message.
Incoming message: Thank you!


Connecting to server.
Sending message.
Reading message.

Unhandled Exception: System.AggregateException: One or more errors occurred. (Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.) ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host

In other words - the second attempt to run the client ends with an exception. That is the weird part.

Here is my minimal code sample for reproducing the issue.

Server code:

public static async Task StartServerAsync()
{
  TcpListener listener = new TcpListener(IPAddress.Any, 1234);
  listener.Server.NoDelay = true;
  listener.Server.LingerState = new LingerOption(true, 0);
  listener.Start();

  while (true)
  {
    Console.WriteLine("Waiting for client.");
    using (TcpClient client = await listener.AcceptTcpClientAsync())
    {
      client.NoDelay = true;
      client.LingerState = new LingerOption(true, 0);

      Console.WriteLine("Reading message.");
      using (NetworkStream stream = client.GetStream())
      {
        byte[] buffer = new byte[32];
        int len = await stream.ReadAsync(buffer, 0, buffer.Length);
        string incomingMessage = Encoding.UTF8.GetString(buffer, 0, len);
        Console.WriteLine("Incoming message: {0}", incomingMessage);


        Console.WriteLine("Sending message.");
        byte[] message = Encoding.UTF8.GetBytes("Thank you!");
        await stream.WriteAsync(message, 0, message.Length);
        Console.WriteLine("Closing connection.");
      }
    }
  }
}

Client code:

public static async Task StartClientAsync()
{
  using (TcpClient client = new TcpClient())
  {
    client.NoDelay = true;
    client.LingerState = new LingerOption(true, 0);

    Console.WriteLine("Connecting to server.");
    await client.ConnectAsync("127.0.0.1", 1234);

    Console.WriteLine("Sending message.");
    using (NetworkStream stream = client.GetStream())
    {
      byte[] buffer = Encoding.UTF8.GetBytes("This message is longer than what the server is willing to read.");
      await stream.WriteAsync(buffer, 0, buffer.Length);

      Console.WriteLine("Reading message.");
      int len = await stream.ReadAsync(buffer, 0, buffer.Length);
      string message = Encoding.UTF8.GetString(buffer, 0, len);

      Console.WriteLine("Incoming message: {0}", message);
    }
  }
}

Oddly enough, if we change the client's message from

"This message is longer than what the server is willing to read."

to

"This message is short."

both instances of the client finish without crash and produces the same output.

Note that if I omit all three lines where LingerState is set, it makes no difference. Also there is no difference in behavior if I set LingerState to

new LingerState(true, 5)

Also there is no difference if I set NoDelay to false. In other words, setting whatever values to LingerState and NoDelay on either side does not seem to have any effect on the crash. The only way to prevent the crash is to read the whole input from the client on the server side.

This is weird and I wonder if anyone can explain that. I am not sure if it holds for .NET Framework as well, only tested with NetCore 1.0.0 and on Windows 10.


Solution

  • This was finally answered on GitHub by .NET Core team.

    Long answer - see last comment of https://github.com/dotnet/corefx/issues/13114

    Short answer -

    The behavior is expected, and is by-design.