C# 取消阻止 AcceptTcpClient 调用
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/12231789/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Cancel blocking AcceptTcpClient call
提问by user1641096
As everyone may already know, the simplest way to accept incoming TCP connections in C# is by looping over TcpListener.AcceptTcpClient(). Additionally this way will block code execution until a connection is obtained. This is extremely limiting to a GUI, so I want to listen for connections in either a seperate thread or task.
每个人都可能已经知道,在 C# 中接受传入 TCP 连接的最简单方法是循环 TcpListener.AcceptTcpClient()。此外,这种方式将阻止代码执行,直到获得连接。这对 GUI 有极大的限制,所以我想在单独的线程或任务中监听连接。
I have been told, that threads have several disadvantages, however nobody explained me what these are. So instead of using threads, I used tasks. This works great, however since the AcceptTcpClient method is blocking execution, I can't find any way of handling a task cancellation.
有人告诉我,线程有几个缺点,但是没有人向我解释这些是什么。所以我没有使用线程,而是使用任务。这很好用,但是由于 AcceptTcpClient 方法正在阻止执行,我找不到任何处理任务取消的方法。
Currently the code looks like this, but I have no idea how I would want to cancel the task when I want the program to stop listening for connections.
目前代码看起来像这样,但是当我希望程序停止侦听连接时,我不知道如何取消任务。
First off the function executed in the task:
首先关闭任务中执行的函数:
static void Listen () {
// Create listener object
TcpListener serverSocket = new TcpListener ( serverAddr, serverPort );
// Begin listening for connections
while ( true ) {
try {
serverSocket.Start ();
} catch ( SocketException ) {
MessageBox.Show ( "Another server is currently listening at port " + serverPort );
}
// Block and wait for incoming connection
if ( serverSocket.Pending() ) {
TcpClient serverClient = serverSocket.AcceptTcpClient ();
// Retrieve data from network stream
NetworkStream serverStream = serverClient.GetStream ();
serverStream.Read ( data, 0, data.Length );
string serverMsg = ascii.GetString ( data );
MessageBox.Show ( "Message recieved: " + serverMsg );
// Close stream and TcpClient connection
serverClient.Close ();
serverStream.Close ();
// Empty buffer
data = new Byte[256];
serverMsg = null;
}
}
Second, the functions starting and stopping the listening service:
二、启动和停止监听服务的功能:
private void btnListen_Click (object sender, EventArgs e) {
btnListen.Enabled = false;
btnStop.Enabled = true;
Task listenTask = new Task ( Listen );
listenTask.Start();
}
private void btnStop_Click ( object sender, EventArgs e ) {
btnListen.Enabled = true;
btnStop.Enabled = false;
//listenTask.Abort();
}
I just need something to replace the listenTask.Abort() call (Which I commented out because the method doesn't exist)
我只需要一些东西来替换 listenTask.Abort() 调用(我注释掉了,因为该方法不存在)
采纳答案by th1rdey3
The following code will close/abort AcceptTcpClient when isRunning variable becomes false
当 isRunning 变量变为 false 时,以下代码将关闭/中止 AcceptTcpClient
public static bool isRunning;
delegate void mThread(ref book isRunning);
delegate void AccptTcpClnt(ref TcpClient client, TcpListener listener);
public static main()
{
isRunning = true;
mThread t = new mThread(StartListening);
Thread masterThread = new Thread(() => t(this, ref isRunning));
masterThread.IsBackground = true; //better to run it as a background thread
masterThread.Start();
}
public static void AccptClnt(ref TcpClient client, TcpListener listener)
{
if(client == null)
client = listener.AcceptTcpClient();
}
public static void StartListening(ref bool isRunning)
{
TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, portNum));
try
{
listener.Start();
TcpClient handler = null;
while (isRunning)
{
AccptTcpClnt t = new AccptTcpClnt(AccptClnt);
Thread tt = new Thread(() => t(ref handler, listener));
tt.IsBackground = true;
// the AcceptTcpClient() is a blocking method, so we are invoking it
// in a separate dedicated thread
tt.Start();
while (isRunning && tt.IsAlive && handler == null)
Thread.Sleep(500); //change the time as you prefer
if (handler != null)
{
//handle the accepted connection here
}
// as was suggested in comments, aborting the thread this way
// is not a good practice. so we can omit the else if block
// else if (!isRunning && tt.IsAlive)
// {
// tt.Abort();
//}
}
// when isRunning is set to false, the code exits the while(isRunning)
// and listner.Stop() is called which throws SocketException
listener.Stop();
}
// catching the SocketException as was suggested by the most
// voted answer
catch (SocketException e)
{
}
}
回答by BitMask777
Cancelling AcceptTcpClient
取消 AcceptTcpClient
Your best bet for cancelling the blocking AcceptTcpClientoperation is to call TcpListener.Stopwhich will throw a SocketExceptionthat you can catch if you want to explicitly check that the operation was cancelled.
取消阻塞AcceptTcpClient操作的最佳选择是调用TcpListener.Stop,它会抛出一个SocketException,如果您想明确检查操作是否被取消,您可以捕获该异常。
TcpListener serverSocket = new TcpListener ( serverAddr, serverPort );
...
try
{
TcpClient serverClient = serverSocket.AcceptTcpClient ();
// do something
}
catch (SocketException e)
{
if ((e.SocketErrorCode == SocketError.Interrupted))
// a blocking listen has been cancelled
}
...
// somewhere else your code will stop the blocking listen:
serverSocket.Stop();
Whatever wants to call Stop on your TcpListener will need some level of access to it, so you would either scope it outside of your Listen method or wrap your listener logic inside of an object that manages the TcpListener and exposes Start and Stop methods (with Stop calling TcpListener.Stop()).
任何想要在您的 TcpListener 上调用 Stop 都需要对它进行某种级别的访问,因此您可以将其范围限定在您的 Listen 方法之外,或者将您的侦听器逻辑包装在管理 TcpListener 并公开 Start 和 Stop 方法的对象中(使用 Stop打电话TcpListener.Stop())。
Async Termination
异步终止
Because the accepted answer uses Thread.Abort()to terminate the thread it might be helpful to note here that the best way to terminate an asynchronous operation is by cooperative cancellation rather than a hard abort.
由于接受的答案用于Thread.Abort()终止线程,因此在此处注意终止异步操作的最佳方法是通过协作取消而不是硬中止可能会有所帮助。
In a cooperative model, the target operation can monitor a cancellation indicator which is signaled by the terminator. This allows the target to detect a cancellation request, clean up as needed, and then at an appropriate time communicate status of the termination back to the terminator. Without an approach like this, abrupt termination of the operation can leave the thread's resources and possibly even the hosting process or app domain in a corrupt state.
在协作模型中,目标操作可以监视由终止符发出的信号的取消指示符。这允许目标检测取消请求,根据需要进行清理,然后在适当的时间将终止状态返回给终止者。如果没有这样的方法,操作的突然终止可能会使线程的资源甚至托管进程或应用程序域处于损坏状态。
From .NET 4.0 onward, the best way to implement this pattern is with a CancellationToken. When working with threads the token can be passed in as a parameter to the method executing on the thread. With Tasks, support for CancellationTokens is built into several of the Task constructors. Cancellation tokes are discussed in more detail in this MSDN article.
从 .NET 4.0 开始,实现此模式的最佳方法是使用CancellationToken。使用线程时,可以将令牌作为参数传递给在线程上执行的方法。使用 Tasks,对 CancellationTokens 的支持内置于多个Task 构造函数中。这篇MSDN 文章中更详细地讨论了取消令牌。
回答by Luaan
Well, in the olden days before properly working asynchronous sockets (the best way today IMO, BitMask talks about this), we've used a simple trick: set the isRunningto false (again, ideally, you want to use CancellationTokeninstead, public static bool isRunning;is nota thread-safe way to terminate a background worker :)) and start a new TcpClient.Connectto yourself - this will return you from the Acceptcall and you can terminate gracefully.
那么,在昔日之前正常工作异步插座中,我们使用一个简单的一招(IMO今天,这个位掩码谈判的最佳方式):设置isRunning为false(再次,理想情况下,你要使用CancellationToken替代,public static bool isRunning;是不是一线程安全的方式来终止后台工作者 :)) 并TcpClient.Connect为自己开始一个新的- 这将使您从Accept调用中返回,您可以优雅地终止。
As BitMask already said, Thread.Abortmost definitely isn't a safe approach at termination. In fact, it wouldn't work at all, given that Acceptis handled by native code, where Thread.Aborthas no power. The only reason it works is because you're not actually blocking in the I/O, but rather running an infinite loop while checking for Pending(non-blocking call). This looks like a great way to have 100% CPU usage on one core :)
正如 BitMask 已经说过的,Thread.Abort在终止时绝对不是一种安全的方法。事实上,它根本不起作用,因为它Accept是由Thread.Abort没有能力的本机代码处理的。它工作的唯一原因是因为您实际上并没有在 I/O 中阻塞,而是在检查Pending(非阻塞调用)时运行无限循环。这看起来是在一个内核上 100% CPU 使用率的好方法:)
Your code has a lot of other issues too, which don't blow up in your face only because you're doing very simple stuff, and because of .NET being rather nice. For example, you're always doing GetStringon the whole buffer you're reading into - but that's wrong. In fact, that's a textbook example of a buffer overflow in e.g. C++ - the only reason it seems to work in C# is because it pre-zeroes the buffer, so GetStringignores the data after the "real" string you read. Instead, you need to take the return value of the Readcall - that tells you how many bytes you've read, and as such, how many you need to decode.
您的代码也有很多其他问题,这些问题不会仅仅因为您正在做非常简单的事情,并且因为 .NET 相当不错而在您面前炸毁。例如,你总是在GetString你正在读入的整个缓冲区上做- 但这是错误的。事实上,这是例如 C++ 中缓冲区溢出的教科书示例 - 它似乎在 C# 中工作的唯一原因是因为它预先将缓冲区清零,因此GetString忽略了您读取的“真实”字符串之后的数据。相反,您需要获取Read调用的返回值——它告诉您读取了多少字节,因此,您需要解码多少字节。
Another very important benefit of this is it means you no longer have to recreate the byte[]after each read - you can simply reuse the buffer over and over again.
另一个非常重要的好处是它意味着您不再需要byte[]在每次读取后重新创建- 您可以简单地一遍又一遍地重用缓冲区。
Don'twork with the GUI from other thread than the GUI thread (yes, your Taskis running in a separate thread pool thread). MessageBox.Showis a dirty hack that in fact does work from other threads, but that really isn't what you want. You need to invoke the GUI actions on the GUI thread (for example using Form.Invoke, or by using a task that has a synchronization context on the GUI thread). That will mean the message box will be the proper dialog you'd expect.
不要使用来自 GUI 线程以外的其他线程的 GUI(是的,您Task正在单独的线程池线程中运行)。MessageBox.Show是一个肮脏的黑客,实际上可以从其他线程工作,但这确实不是您想要的。您需要在 GUI 线程上调用 GUI 操作(例如,使用 Form.Invoke,或使用在 GUI 线程上具有同步上下文的任务)。这意味着消息框将是您期望的正确对话框。
There's many more issues with the snippet you posted, but given that this isn't Code Review, and that it's an old thread, I'm not going to make this any longer :)
您发布的代码段还有很多问题,但鉴于这不是代码,而且是一个旧线程,我不会再做这个了:)
回答by Vlad
For completeness, async counterpart of the answer above:
为了完整起见,上面答案的异步对应物:
async Task<TcpClient> AcceptAsync(TcpListener listener, CancellationToken ct)
{
using (ct.Register(listener.Stop))
{
try
{
return await listener.AcceptTcpClientAsync();
}
catch (SocketException e)
{
if (e.SocketErrorCode == SocketError.Interrupted)
throw new OperationCanceledException();
throw;
}
}
}
Update: As @Mitch suggests in comments (and as this discussionconfirms), awaiting AcceptTcpClientAsyncmay throw ObjectDisposedExceptionafter Stop(which we are calling anyway), so it makes sense to catch ObjectDisposedExceptiontoo:
更新:由于@米奇暗示的意见(和讨论确认),等待AcceptTcpClientAsync可能会抛出ObjectDisposedException后Stop(这是我们无论如何调用),因此是很有意义赶上ObjectDisposedException太:
async Task<TcpClient> AcceptAsync(TcpListener listener, CancellationToken ct)
{
using (ct.Register(listener.Stop))
{
try
{
return await listener.AcceptTcpClientAsync();
}
catch (SocketException e) when (e.SocketErrorCode == SocketError.Interrupted)
{
throw new OperationCanceledException();
}
catch (ObjectDisposedException) when (ct.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
}
回答by bmigette
Here's how I overcame this. Hope this help. Might not be the cleanest, but works for me
这就是我克服这个问题的方法。希望这有帮助。可能不是最干净的,但对我有用
public class consoleService {
private CancellationTokenSource cts;
private TcpListener listener;
private frmMain main;
public bool started = false;
public bool stopped = false;
public void start() {
try {
if (started) {
stop();
}
cts = new CancellationTokenSource();
listener = new TcpListener(IPAddress.Any, CFDPInstanceData.Settings.RemoteConsolePort);
listener.Start();
Task.Run(() => {
AcceptClientsTask(listener, cts.Token);
});
started = true;
stopped = false;
functions.Logger.log("Started Remote Console on port " + CFDPInstanceData.Settings.RemoteConsolePort, "RemoteConsole", "General", LOGLEVEL.INFO);
} catch (Exception E) {
functions.Logger.log("Error starting remote console socket: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
}
}
public void stop() {
try {
if (!started) { return; }
stopped = false;
cts.Cancel();
listener.Stop();
int attempt = 0;
while (!stopped && attempt < GlobalSettings.ConsoleStopAttempts) {
attempt++;
Thread.Sleep(GlobalSettings.ConsoleStopAttemptsDelayMS);
}
} catch (Exception E) {
functions.Logger.log("Error stopping remote console socket: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
} finally {
started = false;
}
}
void AcceptClientsTask(TcpListener listener, CancellationToken ct) {
try {
while (!ct.IsCancellationRequested) {
try {
TcpClient client = listener.AcceptTcpClient();
if (!ct.IsCancellationRequested) {
functions.Logger.log("Client connected from " + client.Client.RemoteEndPoint.ToString(), "RemoteConsole", "General", LOGLEVEL.DEBUG);
ParseAndReply(client, ct);
}
} catch (SocketException e) {
if (e.SocketErrorCode == SocketError.Interrupted) {
break;
} else {
throw e;
}
} catch (Exception E) {
functions.Logger.log("Error in Remote Console Loop: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
}
}
functions.Logger.log("Stopping Remote Console Loop", "RemoteConsole", "General", LOGLEVEL.DEBUG);
} catch (Exception E) {
functions.Logger.log("Error in Remote Console: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
} finally {
stopped = true;
}
functions.Logger.log("Stopping Remote Console", "RemoteConsole", "General", LOGLEVEL.INFO);
}
}

