C
C#3mo ago
maxmahem

Canceling TcpClient after TcpListener has been stopped

So, I may have done this completely right, but I'm a bit unsure on the behavior. I'm creating TcpClients with AcceptTcpClientAsync and passing it a CancellationToken which does seem to end the TcpClients, but I guess I'm expecting to see a more graceful end in my TcpCleints which I am not. To wit, I'm expecting to end up hitting client.Close and then get the "Client disconnected" message, which isn't happening.
28 Replies
maxmahem
maxmahem3mo ago
public sealed class EchoService(IPAddress address, ushort port) : IService, IDisposable
{
const uint BUFFER_SIZE = 1024;

readonly TcpListener listener = new(address, port);
EndPoint? server;

readonly CancellationTokenSource cancellationTokenSource = new();

public async Task StartAsync()
{
if (cancellationTokenSource.IsCancellationRequested)
throw new OperationCanceledException("This object cannot be restarted after being canceled", cancellationTokenSource.Token);

this.listener.Start();
this.server = this.listener.Server.LocalEndPoint;
OnMessage(this.server, "Echo Service started... ");

while (!cancellationTokenSource.IsCancellationRequested) {
using TcpClient client = await listener.AcceptTcpClientAsync(cancellationTokenSource.Token);
OnMessage(client.Client.RemoteEndPoint, "Client connected.");
await HandleClientAsync(client, cancellationTokenSource.Token);
}
}

public async Task StopAsync()
{
await cancellationTokenSource.CancelAsync();
this.listener.Stop();
OnMessage(this.server, "Echo Service stopped.");
}

private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
using NetworkStream stream = client.GetStream();
var endpoint = client.Client.RemoteEndPoint;
var buffer = new byte[BUFFER_SIZE];

while (!cancellationToken.IsCancellationRequested) {
int bytesRead = await stream.ReadAsync(buffer, cancellationToken);
if (bytesRead <= 0) break;

string message = TranslateMessage(buffer.AsSpan()[..bytesRead]);
OnMessage(endpoint, $"Received: {message}");

await stream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}

client.Close();
OnMessage(endpoint, "Client disconnected.");
}
}
public sealed class EchoService(IPAddress address, ushort port) : IService, IDisposable
{
const uint BUFFER_SIZE = 1024;

readonly TcpListener listener = new(address, port);
EndPoint? server;

readonly CancellationTokenSource cancellationTokenSource = new();

public async Task StartAsync()
{
if (cancellationTokenSource.IsCancellationRequested)
throw new OperationCanceledException("This object cannot be restarted after being canceled", cancellationTokenSource.Token);

this.listener.Start();
this.server = this.listener.Server.LocalEndPoint;
OnMessage(this.server, "Echo Service started... ");

while (!cancellationTokenSource.IsCancellationRequested) {
using TcpClient client = await listener.AcceptTcpClientAsync(cancellationTokenSource.Token);
OnMessage(client.Client.RemoteEndPoint, "Client connected.");
await HandleClientAsync(client, cancellationTokenSource.Token);
}
}

public async Task StopAsync()
{
await cancellationTokenSource.CancelAsync();
this.listener.Stop();
OnMessage(this.server, "Echo Service stopped.");
}

private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
using NetworkStream stream = client.GetStream();
var endpoint = client.Client.RemoteEndPoint;
var buffer = new byte[BUFFER_SIZE];

while (!cancellationToken.IsCancellationRequested) {
int bytesRead = await stream.ReadAsync(buffer, cancellationToken);
if (bytesRead <= 0) break;

string message = TranslateMessage(buffer.AsSpan()[..bytesRead]);
OnMessage(endpoint, $"Received: {message}");

await stream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}

client.Close();
OnMessage(endpoint, "Client disconnected.");
}
}
SineѶeҀҬOӶ⒉⓸⎤ᚙ▟ ▞╸
If you're looking for a graceful end. You need to call .disconnect() first before closing.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
SineѶeҀҬOӶ⒉⓸⎤ᚙ▟ ▞╸
If you cancel first. It's gonna throw a System.OperationCancelled exception.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
graceful end isn't the right word yeah.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
but yeah I guess I need to wrap it. I'll try that.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
SineѶeҀҬOӶ⒉⓸⎤ᚙ▟ ▞╸
That's a server setup.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
SineѶeҀҬOӶ⒉⓸⎤ᚙ▟ ▞╸
Oh yeah I only just noticed that yeh. That'll definitely be an issue
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
hmm, no point in catching the exception really, I have nothing to do with it. I'll just put in a finally.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
it's fine for this.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
I mean, this is just an echo service. There isn't any way I can "gracefully" tell the client "I'm not going to handle your requests anymore"
SineѶeҀҬOӶ⒉⓸⎤ᚙ▟ ▞╸
It depends on what exceptions are too sometimes. Usually something with a SocketException is an ungraceful disconnect e.g. timeouts and connection resets. With OperationCancelledException. You might want to gracefully disconnect before closing.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
I can call client.Stop in the finally
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
using NetworkStream stream = client.GetStream();
var endpoint = client.Client.RemoteEndPoint;
var buffer = new byte[BUFFER_SIZE];

try {
while (!cancellationToken.IsCancellationRequested) {
int bytesRead = await stream.ReadAsync(buffer, cancellationToken);
if (bytesRead <= 0)
break;

string message = TranslateMessage(buffer.AsSpan()[..bytesRead]);
OnMessage(endpoint, $"Received: {message}");

await stream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}
} finally {
client.Close();
OnMessage(endpoint, "Client disconnected.");
}
}
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
using NetworkStream stream = client.GetStream();
var endpoint = client.Client.RemoteEndPoint;
var buffer = new byte[BUFFER_SIZE];

try {
while (!cancellationToken.IsCancellationRequested) {
int bytesRead = await stream.ReadAsync(buffer, cancellationToken);
if (bytesRead <= 0)
break;

string message = TranslateMessage(buffer.AsSpan()[..bytesRead]);
OnMessage(endpoint, $"Received: {message}");

await stream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}
} finally {
client.Close();
OnMessage(endpoint, "Client disconnected.");
}
}
SineѶeҀҬOӶ⒉⓸⎤ᚙ▟ ▞╸
That'll just completely throw a SocketException on the client side.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
no particular reason, I just don't really have anything to do with the exception here.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
the exception isn't bubling out of here.
Unknown User
Unknown User3mo ago
Message Not Public
Sign In & Join Server To View
maxmahem
maxmahem3mo ago
ahhhh I know why. But that's okay as well. hmm... finally does let me avoid some code duplication (catch path isn't taken on a normal client exit). But hmm... maybe I'll look into just registering some cleanup methods in the token instead.