Reputation: 79
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?
Upvotes: 1
Views: 2078
Reputation: 79
Thank you all for your responses. Sorry for the delay in replying, I was taken ill, and when I got back to work, well, there was a lot of work.
TL;DR: the problem was caused by closing the stream with data still in it.
Initially, changing from calling tcpClient.Close() to calling socket.Shutdown(Both) did NOT fix the problem. In the process of writing a minimal example to post here without my LineReader code, I determined that the problem was that I had scaled up my test to do some performance testing and hadn't increased the buffer accordingly. So the client didn't read all the bytes that come back from the server. When I closed the stream on the client side without having read everything, it caused the exception on the server side. Simply increasing the read buffer size fixed this. After fixing this I tried going back to Stream.Close() and that also worked correctly. So calling Close instead of Shutdown wasn't really the problem in the first place. @Stephen Cleary's comments still seem relevant however, as in his linked video he is always reading, so calling Close with a read pending causes the problem, in my case there was also some slightly different unfinished business.
Also, as @Adam Cohen pointed out, I would have been better off using StreamReader. I had tried that initially but had some problems with it that I now can't reproduce.
Upvotes: 1
Reputation:
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)
{
string command = default;
using var stream = new NetworkStream(socket);
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);
}
stream.Close(1000);
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
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)
{
string command = default;
using var stream = new NetworkStream(socket);
using var writer = new StreamWriter(stream, Encoding.ASCII);
using var reader = new StreamReader(stream, Encoding.ASCII);
while ((command = await reader.ReadLineAsync()) != null)
{
if (command == "Hello")
{
await writer.WriteLineAsync($"Hi from Client{clientId} - {_receiveCnt}");
await writer.FlushAsync();
}
}
stream.Close(1000);
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
FULL WORKING EXAMPLE W/2 ASYNC CLIENTS
Copy and paste into Linqpad or a console app, add references (System.Net, System.Net.Sockets, System.Threading.Tasks) and hit F5...
async Task Main()
{
using var server = new Server();
_ = server.Start();
Task.WaitAll(new Client().Connect(),new Client().Connect());
server.Stop();
}
public class Client
{
Socket _client;
int _sendCount = 0;
IPEndPoint _endPoint;
public Client()
{
// Set the endpoint to connect on port 13000.
Int32 port = 13000;
_endPoint = new(IPAddress.Loopback, port);
_client = new(
_endPoint.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
}
public async Task Connect()
{
await _client.ConnectAsync(_endPoint);
if (!_client.Connected) throw new Exception("Connection error.");
using var stream = new NetworkStream(_client);
using var streamWriter = new StreamWriter(stream);
using var streamReader = new StreamReader(stream);
while (_sendCount < 10)
{
await streamWriter.WriteLineAsync("Hello");
await streamWriter.FlushAsync();
var response = streamReader.ReadLine();
_sendCount++;
Console.WriteLine(response);
}
stream.Close(1000);
_client.Close();
}
}
public class Server : IDisposable
{
Socket _server = null;
IPEndPoint _endpoint;
int _clientCnt = 0;
int _receiveCnt = 0;
CancellationTokenSource _cancellation;
List<Task> _clients = new List<Task>();
public Server()
{
// Set the TcpListener on port 13000.
Int32 port = 13000;
_endpoint = new(IPAddress.Loopback, port);
_server = new(_endpoint.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
_cancellation = new CancellationTokenSource();
}
public async Task Start()
{
try
{
// Start listening for client requests.
_server.Bind(_endpoint);
_server.Listen();
do
{
// Perform a blocking call to accept requests.
var socket = await _server.AcceptAsync(_cancellation.Token);
// Handle client async on background thread and continue
_ = HandleClientAsync(socket, _cancellation.Token);
} while (_server.IsBound);
}
catch (OperationCanceledException e)
{
Console.WriteLine("Async socket cancallation requested.");
}
catch (SocketException e)
{
Console.WriteLine("SocketException: {0}", e);
}
catch (Exception ex)
{
Console.WriteLine("Exception: {0}", ex);
}
finally
{
// Stop listening for new clients.
_server.Close();
}
}
public void Stop()
{
if (_server.IsBound)
{
_cancellation.Cancel();
_server.Close(10000);
}
}
public async Task HandleClientAsync(Socket socket, CancellationToken token)
{
var clientId = Interlocked.Increment(ref _clientCnt);
Console.WriteLine($"Client {_clientCnt} Connected!");
string command = default;
using var stream = new NetworkStream(socket);
using var writer = new StreamWriter(stream, Encoding.ASCII);
using var reader = new StreamReader(stream, Encoding.ASCII);
while ((command = await reader.ReadLineAsync()) != null)
{
Interlocked.Increment(ref _receiveCnt);
if (command == "Hello")
{
await writer.WriteLineAsync($"Hi from Client{clientId} - {_receiveCnt}");
await writer.FlushAsync();
}
}
stream.Close(1000);
socket.Shutdown(SocketShutdown.Both);
socket.Close();
Interlocked.Decrement(ref _clientCnt);
Console.WriteLine($"Client {clientId} disconnected.");
}
public void Dispose()
{
if (_server.IsBound)
{
Stop();
_server.Dispose();
}
}
}
Upvotes: 1
Reputation: 457302
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.
Upvotes: 2