众所周知,在C#中接受传入TCP连接的最简单方法是循环使用TcpListener.AcceptTcpClient()。此外,这种方式将阻止代码执行,直到获得连接。这对GUI非常有限,所以我想在单独的线程或任务中监听连接。
我被告知,线程有几个缺点,但没有人解释我这是什么。所以我没有使用线程,而是使用了任务。这很好用,但是由于AcceptTcpClient方法阻塞了执行,我找不到任何处理任务取消的方法。
目前代码看起来像这样,但是当我希望程序停止侦听连接时,我不知道如何取消任务。
首先关闭任务中执行的功能:
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;
}
}
其次,启动和停止监听服务的功能:
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();
}
我只需要一些东西来替换listenTask.Abort()调用(我注释掉了,因为该方法不存在)
答案 0 :(得分:34)
取消AcceptTcpClient
取消阻止AcceptTcpClient
操作的最佳选择是调用TcpListener.Stop,如果要明确检查操作是否已取消,则会SocketException抛出一个CancellationToken。< / p>
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();
无论什么想要调用TcpListener上的Stop都需要一定程度的访问权限,因此您可以将其范围限制在Listen方法之外,或者将侦听器逻辑包装在管理TcpListener的对象内并公开Start和Stop方法(停止呼叫TcpListener.Stop()
)。
异步终止
因为接受的答案使用Thread.Abort()
来终止线程,所以在这里注意终止异步操作的最佳方法是通过合作取消而不是硬中止。
在协作模型中,目标操作可以监视由终结器发信号通知的取消指示符。这允许目标检测取消请求,根据需要进行清理,然后在适当的时间将终止的状态传送回终结器。如果没有这样的方法,突然终止操作可能会使线程的资源,甚至托管进程或应用程序域处于损坏状态。
从.NET 4.0开始,实现此模式的最佳方法是使用Task constructors。使用线程时,令牌可以作为参数传递给在线程上执行的方法。使用Tasks,对MSDN article.的几个内置了对CancellationTokens的支持。在此{{3}}
中更详细地讨论了取消令状答案 1 :(得分:8)
为了完整性,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;
}
}
}
更新:正如@Mitch在评论中建议(以及this discussion确认),等待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();
}
}
}
答案 2 :(得分:1)
好吧,在正常使用异步套接字之前的过去(今天最好的方式是IMO,BitMask谈论这个),我们使用了一个简单的技巧:将isRunning
设置为false(理想情况下,再次,你想要使用CancellationToken
代替,public static bool isRunning;
不是一种线程安全的方式来终止后台工作者:))并为自己开始一个新的TcpClient.Connect
- 这个将从Accept
电话返回您,您可以优雅地终止。
正如BitMask已经说过的那样,Thread.Abort
绝对不是终止时的安全方法。实际上,它根本不起作用,因为Accept
由本地代码处理,Thread.Abort
没有权力。它工作的唯一原因是因为你实际上并没有在I / O中阻塞,而是在检查Pending
(非阻塞调用)时运行无限循环。这看起来是在一个核心上实现100%CPU使用率的好方法:)
你的代码还有很多其他的问题,只是因为你做的很简单,而且因为.NET相当不错,所以你的代码不会爆炸。例如,你总是在你正在阅读的整个缓冲区上做GetString
- 但这是错误的。实际上,这是一个例如缓冲区溢出的教科书示例。 C ++ - 它似乎在C#中工作的唯一原因是因为它预先将缓冲区归零,因此GetString
忽略了您读取的“真实”字符串后的数据。相反,您需要获取Read
调用的返回值 - 它会告诉您已读取的字节数,以及需要解码的字节数。
另一个非常重要的好处是,它意味着您不必再在每次读取后重新创建byte[]
- 您可以一次又一次地重复使用缓冲区。
不要从GUI线程以外的其他线程使用GUI(是的,您的Task
正在一个单独的线程池线程中运行)。 MessageBox.Show
是一个肮脏的黑客,实际上可以从其他线程工作,但这真的不是你想要的。您需要在GUI线程上调用GUI操作(例如,使用Form.Invoke,或使用在GUI线程上具有同步上下文的任务)。这意味着消息框将是您期望的正确对话框。
你发布的代码片段还有很多问题,但鉴于这不是Code Review,而且它是一个旧帖子,我不会再这样做了:)
答案 3 :(得分:1)
以下是我克服这个问题的方法。希望这有帮助。可能不是最干净的,但对我有用
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);
}
}
答案 4 :(得分:-3)
当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)
{
}
}