使用任务取消令牌停止 TCP 侦听器
Posted
技术标签:
【中文标题】使用任务取消令牌停止 TCP 侦听器【英文标题】:Stop a TCP Listener using Task Cancellation Token 【发布时间】:2019-09-10 17:00:45 【问题描述】:我无法使用取消令牌来停止 TCP 侦听器。第一个代码提取是一个示例,我可以在另一个类的方法中成功停止测试 while 循环。所以我不明白为什么我不能将这种类似的逻辑应用于 TCP 侦听器类。花了很多天阅读有关此主题的令人费解的答案,但找不到合适的解决方案。
我的软件应用程序要求 TCP 侦听器必须让用户能够从服务器端而不是客户端停止它。如果用户想要重新配置此侦听器的端口号,那么他们目前必须关闭软件才能让 Windows 关闭底层套接字,这不好,因为会影响在我的应用程序中运行的其他服务。
第一个代码摘录只是一个示例,我可以在其中停止 while 循环的运行,这可以正常工作,但除了我希望这适用于我的 TCP 侦听器的 faat 之外没有那么相关:
public void Cancel(CancellationToken cancelToken) // EXAMPLE WHICH IS WORKING
Task.Run(async () =>
while (!cancelToken.IsCancellationRequested)
await Task.Delay(500);
log.Info("Test Message!");
, cancelToken);
下面是我正在努力解决的实际 TCP 侦听器代码
public void TcpServerIN(string inboundEncodingType, string inboundIpAddress, string inboundLocalPortNumber, CancellationToken cancelToken)
TcpListener listener = null;
Task.Run(() =>
while (!cancelToken.IsCancellationRequested)
try
IPAddress localAddr = IPAddress.Parse(inboundIpAddress);
int port = int.Parse(inboundLocalPortNumber);
listener = new TcpListener(localAddr, port);
// Start listening for client requests.
listener.Start();
log.Info("TcpListenerIN listener started");
// Buffer for reading data
Byte[] bytes = new Byte[1024];
String data = null;
// Enter the listening loop.
while (true)
// Perform a blocking call to accept client requests.
TcpClient client = listener.AcceptTcpClient();
// Once each client has connected, start a new task with included parameters.
var task = Task.Run(() =>
// Get a stream object for reading and writing
NetworkStream stream = client.GetStream();
data = null;
int i;
// Loop to receive all the data sent by the client.
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
// Select Encoding format set by string inboundEncodingType parameter.
if (inboundEncodingType == "UTF8") data = Encoding.UTF8.GetString(bytes, 0, i);
if (inboundEncodingType == "ASCII") data = Encoding.ASCII.GetString(bytes, 0, i);
// Use this if you want to echo each message directly back to TCP Client
//stream.Write(msg, 0, msg.Length);
// If any TCP Clients are connected then pass the appended string through
// the rules engine for processing, if not don't send.
if ((listConnectedClients != null) && (listConnectedClients.Any()))
// Pass the appended message string through the SSSCRulesEngine
SendMessageToAllClients(data);
// When the remote client disconnetcs, close/release the socket on the TCP Server.
client.Close();
);
catch (SocketException ex)
log.Error(ex);
finally
// If statement is required to prevent an en exception thrown caused by the user
// entering an invalid IP Address or Port number.
if (listener != null)
// Stop listening for new clients.
listener.Stop();
MessageBox.Show("CancellationRequested");
log.Info("TCP Server IN CancellationRequested");
, cancelToken);
【问题讨论】:
你有while(true) /* block and don’t check cancel token */
。唯一的出路是通过异常或返回。也许你的内部循环应该检查令牌?
之前试过了,没什么区别。
当CancellationToken
被取消时,您必须关闭侦听器套接字。如果您使用阻塞 API(例如,AcceptTcpClient
),那么当CancellationToken
被取消时,您需要关闭侦听器套接字从另一个线程。
不知道该怎么做,在我的情况下,不同的线程和不同的类。 TCP 侦听器将用于多个实例,因此我需要一种方法来独立关闭每个实例。
【参考方案1】:
有趣的是,没有人提出任何解决方案,诚然,我花了很长时间才找到解决方案。使用如下示例的同步阻塞模式时停止 TCP 侦听器的关键是向 TCP 侦听器本身注册取消令牌,以及在触发取消令牌时可能已经连接的 TCP 客户端。 (请参阅标记为重要的 cmets)
示例代码在您自己的环境中可能会略有不同,我已经提取了一些我的项目独有的代码膨胀,但您将了解我们在这里所做的事情。在我的项目中,这个 TCP 服务器是使用 NET Core 5.0 IHosted Services 作为后台服务启动的。我下面的代码改编自 MS Docs 上的注释:https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener?view=net-5.0
MS Docs 和我下面的示例之间的主要区别是我希望允许多个 TCP 客户端连接,因此我每次连接新的 TCP 客户端时都会启动一个新的内部任务。
/// <summary>
/// </summary>
/// <param name="server"></param>
/// <param name="port"></param>
/// <param name="logger"></param>
/// <param name="cancelToken"></param>
public void TcpServerRun(
int pluginId,
string pluginName,
string encoding,
int bufferForReadingData,
string ipAddress,
int port,
bool logEvents,
IServiceScopeFactory _scopeFactory,
CancellationToken cancelToken)
IPAddress localAddrIN = IPAddress.Parse(ipAddress);
TcpListener listener = new TcpListener(localAddrIN, port);
Task.Run(() =>
// Dispose the DbContext instance when the task has completed. 'using' = dispose when finished...
using var scope = _scopeFactory.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TcpServer>>();
try
listener.Start();
cancelToken.Register(listener.Stop); // THIS IS IMPORTANT!
string logData = "TCP Server with name [" + pluginName + "] started Succesfully";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Started", pluginName, logData, null, _scopeFactory);
while (!cancelToken.IsCancellationRequested)
TcpClient client = listener.AcceptTcpClient();
logData = "A TCP Client with IP Address [" + client.Client.RemoteEndPoint.ToString() + "] connected to the TCP Server with name: [" + pluginName + "]";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Connected", pluginName, logData, null, _scopeFactory);
// Once each client has connected, start a new task with included parameters.
var task = Task.Run(async () =>
// Get a stream object for reading and writing
NetworkStream stream = client.GetStream();
// Buffer for reading data
Byte[] bytes = new Byte[bufferForReadingData]; // Bytes variable
String data = null;
int i;
cancelToken.Register(client.Close); // THIS IS IMPORTANT!
// Checks CanRead to verify that the NetworkStream is readable.
if (stream.CanRead)
// Loop to receive all the data sent by the client.
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0 & !cancelToken.IsCancellationRequested)
data = Encoding.ASCII.GetString(bytes, 0, i);
logData = "TCP Server with name [" + pluginName + "] received data [" + data + "] from a TCP Client with IP Address [" + client.Client.RemoteEndPoint.ToString() + "]";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Receive", pluginName, logData, null, _scopeFactory);
// Shutdown and end connection
client.Close();
logData = "A TCP Client disconnected from the TCP Server with name: [" + pluginName + "]";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Disconnected", pluginName, logData, null, _scopeFactory);
, cancelToken);
catch (SocketException ex)
// When the cancellation token is called, we will always encounter
// a socket exception for the listener.AcceptTcpClient(); blocking
// call in the while loop thread. We want to catch this particular exception
// and mark the exception as an accepted event without logging it as an error.
// A cancellation token is passed usually when the running thread is manually stopped
// by the user from the UI, or will occur when the IHosted service Stop Method
// is called during a system shutdown.
// For all other unexpected socket exceptions we provide en error log underneath
// in the else statement block.
if (ex.SocketErrorCode == SocketError.Interrupted)
string logData = "TCP Server with name [" + pluginName + "] was stopped due to a CancellationTokenSource cancellation. This event is triggered when the SMTP Server is manually stopped from the UI by the user or during a system shutdown.";
WriteLogEvent("Information", "TCP Servers", "Stopped", pluginName, logData, null, _scopeFactory);
else
string logData = "TCP Server with name [" + pluginName + "] encountered a socket exception error and exited the running thread.";
WriteLogEvent("Error", "TCP Servers", "Socket Exception", pluginName, logData, ex, _scopeFactory);
finally
// Call the Stop method to close the TcpListener.
// Closing the listener does not close any exisiting connections,
// simply stops listening for new connections, you are responsible
// closing the existing connections which we achieve by registering
// the cancel token with the listener.
listener.Stop();
);
【讨论】:
以上是关于使用任务取消令牌停止 TCP 侦听器的主要内容,如果未能解决你的问题,请参考以下文章