处理异步套接字回调中的异常
Handling exceptions in asynchronous socket callbacks
我一直在努力研究套接字通信,并整理了一个小型 Windows Forms 应用程序作为测试。这基本上是一个将连接到服务器、发送一些字节并断开连接的客户端。最终,我也会收到来自服务器的响应,但我现在已经基本删除了它。
据我所知,此代码可以正常工作,但由于在我的 UI 中单击按钮会导致数据发送,因此它需要异步。我有一个名为 SendDataToServer(byte[] bytesToSend)
的方法,它连接到服务器并传输数据。在我的按钮单击事件处理程序中,我创建了一个将调用此方法的后台工作程序。
如果我的服务器未启动并且 运行,我显然会收到套接字异常,当然还有其他原因可能会在尝试连接和传输数据的过程中抛出异常。使用 backgroundworker 和异步套接字回调(ClientConnectCallback()
和 ClientSendCallback()
),确保任何异常都冒出并在我的 UI 中正确显示的最佳方法是什么?
现在我在异步回调本身的 catch 块中有 MessageBoxes,但我想知道这是否真的是显示消息的地方,或者它们是否应该传回?
我的代码如下所示:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Forms;
namespace ClientTest
{
public partial class MyForm : Form
{
private string ServerIpAddress = "x.x.x.x";
private int ServerPort = 59176;
public Socket ClientSocket;
private static ManualResetEvent connectDone = new ManualResetEvent(false);
private static ManualResetEvent sendDone = new ManualResetEvent(false);
// state object for reading client data asynchronously
public class StateObject
{
// client socket
public Socket socket = null;
// size of receive buffer
public const int BufferSize = 1024;
// receive buffer
public byte[] buffer = new byte[BufferSize];
// all bytes received get added to this
public List<byte> bytes = new List<byte>();
}
public MyForm()
{
InitializeComponent();
}
private void ClientConnectCallback(IAsyncResult asyncResult)
{
try
{
// retrieve the socket from the state object
Socket client = (Socket)asyncResult.AsyncState;
// complete the connection
client.EndConnect(asyncResult);
// signal that the connection has been made
connectDone.Set();
}
catch (SocketException sockEx)
{
// if the server isn't running, we'll get a socket exception here
MessageBox.Show(sockEx.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch (ObjectDisposedException)
{
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void ClientSendCallback(IAsyncResult asyncResult)
{
try
{
// retrieve the socket from the state object
Socket client = (Socket)asyncResult.AsyncState;
// complete sending the data to the server
int bytesSent = client.EndSend(asyncResult);
// signal that all bytes have been sent
sendDone.Set();
}
catch (ObjectDisposedException objDispEx)
{
Debug.WriteLine(objDispEx.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
public void SendDataToServer(byte[] bytesToSend)
{
try
{
connectDone.Reset();
sendDone.Reset();
ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Parse(ServerIpAddress);
IPEndPoint remoteEndPoint = new IPEndPoint(ipAddress, ServerPort);
ClientSocket.BeginConnect(remoteEndPoint, new AsyncCallback(ClientConnectCallback), ClientSocket);
connectDone.WaitOne();
ClientSocket.BeginSend(bytesToSend, 0, bytesToSend.Length, 0, new AsyncCallback(ClientSendCallback), ClientSocket);
sendDone.WaitOne();
}
catch (ObjectDisposedException objDispEx)
{
Debug.WriteLine(objDispEx.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
ClientSocket.Shutdown(SocketShutdown.Both);
ClientSocket.Close();
}
}
private void buttonSendDataToServer_Click(object sender, EventArgs e)
{
BackgroundWorker bwSendDataToServer = new BackgroundWorker();
bwSendDataToServer.DoWork += bwSendDataToServer_DoWork;
bwSendDataToServer.RunWorkerCompleted += bwSendDataToServer_RunWorkerCompleted;
bwSendDataToServer.RunWorkerAsync();
}
private void bwSendDataToServer_DoWork(object sender, DoWorkEventArgs e)
{
byte[] bytesToSend = new byte[100];
SendDataToServer(bytesToSend);
}
private void bwSendDataToServer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Debug.WriteLine("BackgroundWorker has completed.");
}
}
}
"How do I report exceptions?" 是一个相当宽泛的问题。很难确定 具体 什么建议最适合您的情况,因为解决问题的方法有很多。
也就是说,恕我直言,您应该始终遵循一些通用规则,这些规则可以帮助指导您的设计:
- Do not block I/O threads with UI.
这意味着您的代码示例肯定应该得到改进,因为您在等待用户确认错误消息时延迟了用于调用回调的任何 I/O 线程。
- Observe the general OOP principle of "separation of concerns".
换句话说,即使显示错误消息的线程不是 I/O 线程,将用户交互逻辑放在主要的 class 中仍然不是一个好主意目的是处理网络 I/O.
当然还有其他有用的编程规则,但考虑到您当前的代码示例,我认为以上两个是最相关的。那么,如何解决这些问题呢?
至少,我会更改您当前的实现,以便 I/O 代码的 none 位于 MyForm
class 中。 MyForm
class 用于用户界面;它不应该让自己参与网络机制 I/O,而是应该将这项工作委托给其他人 class.
现在,另一个 class 仍然需要与 MyForm
通信。但它应该以独立于 MyForm
的方式这样做,即不依赖于(或 "coupled")class。在报告异常方面,一种常见的实现方式是声明一个事件,只要发生异常就会引发该事件。
这将解决第二点,但第一点仍然是一个问题。它可以通过使用 Control.BeginInvoke()
方法执行表单对象中的事件处理代码来解决,以便该代码在 UI 线程中执行,并且相对于 I/O 是异步的线程(即 BeginInvoke()
方法在返回之前不等待调用的代码完成)。
把所有这些放在一起,你可能会得到类似这样的东西:
class ExceptionReportEventArgs : EventArgs
{
public Exception Exception { get; private set; }
public ExceptionEventArgs(Exception exception)
{
Exception = exception;
}
}
class MyNetworkClient
{
public event EventHandler<ExceptionReportEventArgs> ExceptionReport;
private void OnExceptionReport(Exception exception)
{
EventHandler<ExceptionReportEventArgs> handler = ExceptionReport;
if (handler != null)
{
handler(this, new ExceptionReportEventArgs(exception));
}
}
// For example...
private void ClientConnectCallback(IAsyncResult asyncResult)
{
try
{
/* code omitted */
}
catch (SocketException sockEx)
{
// if the server isn't running, we'll get a socket exception here
OnExceptionReport(sockEx);
}
catch (ObjectDisposedException)
{
}
catch (Exception ex)
{
OnExceptionReport(ex);
}
}
}
partial class MyForm : Form
{
private MyNetworkClient _client;
public MyForm()
{
InitializeComponent();
_client = new MyNetworkClient();
_client.ExceptionReport += (sender, e) =>
{
BeginInvoke((MethodInvoker)(() =>
MessageBox.Show(e.Exception.Message, Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Error)));
};
}
}
现在,综上所述:正如我提到的,有很多方法可以解决基本问题。上述方法的替代方法是使用 async
/await
模式,它提供了一种机制,可以以一种易于阅读、命令式的方式干净地编写异步逻辑,同时仍然允许正确的在必须在 UI 线程中执行的代码和不需要执行的代码之间的转换。
我不会在这一点上详细介绍 — 周围有很多示例,并且需要对您发布的代码进行更大的返工 — 但基本思想是使用例如NetworkStream
,它有内置的 async
方法来处理 I/O,并允许 I/O 方法中的异常 "bubble up"。
在这种方法中,您不需要 ExceptionReport
事件。相反,您上面的 MyNetworkClient
class 将提供 async
方法,控制器-class(例如您的 MyForm
对象)可以调用这些方法来启动 I/O .如果发生异常,调用堆栈中的每个 async
方法都可以处理(如果需要)异常,然后重新抛出它(即使用普通的 throw;
语句),一直返回到 UI 代码,其中异常将在 UI 线程中引发并可以在那里呈现给用户,而不会阻塞 I/O 线程。
我认为总的来说,关键是要牢记通常的规则并遵守它们。最终,您会发现代码更易于编写和理解,并且在您选择的 UI 框架(例如 Winforms)中表现更好。
我一直在努力研究套接字通信,并整理了一个小型 Windows Forms 应用程序作为测试。这基本上是一个将连接到服务器、发送一些字节并断开连接的客户端。最终,我也会收到来自服务器的响应,但我现在已经基本删除了它。
据我所知,此代码可以正常工作,但由于在我的 UI 中单击按钮会导致数据发送,因此它需要异步。我有一个名为 SendDataToServer(byte[] bytesToSend)
的方法,它连接到服务器并传输数据。在我的按钮单击事件处理程序中,我创建了一个将调用此方法的后台工作程序。
如果我的服务器未启动并且 运行,我显然会收到套接字异常,当然还有其他原因可能会在尝试连接和传输数据的过程中抛出异常。使用 backgroundworker 和异步套接字回调(ClientConnectCallback()
和 ClientSendCallback()
),确保任何异常都冒出并在我的 UI 中正确显示的最佳方法是什么?
现在我在异步回调本身的 catch 块中有 MessageBoxes,但我想知道这是否真的是显示消息的地方,或者它们是否应该传回?
我的代码如下所示:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Forms;
namespace ClientTest
{
public partial class MyForm : Form
{
private string ServerIpAddress = "x.x.x.x";
private int ServerPort = 59176;
public Socket ClientSocket;
private static ManualResetEvent connectDone = new ManualResetEvent(false);
private static ManualResetEvent sendDone = new ManualResetEvent(false);
// state object for reading client data asynchronously
public class StateObject
{
// client socket
public Socket socket = null;
// size of receive buffer
public const int BufferSize = 1024;
// receive buffer
public byte[] buffer = new byte[BufferSize];
// all bytes received get added to this
public List<byte> bytes = new List<byte>();
}
public MyForm()
{
InitializeComponent();
}
private void ClientConnectCallback(IAsyncResult asyncResult)
{
try
{
// retrieve the socket from the state object
Socket client = (Socket)asyncResult.AsyncState;
// complete the connection
client.EndConnect(asyncResult);
// signal that the connection has been made
connectDone.Set();
}
catch (SocketException sockEx)
{
// if the server isn't running, we'll get a socket exception here
MessageBox.Show(sockEx.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch (ObjectDisposedException)
{
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void ClientSendCallback(IAsyncResult asyncResult)
{
try
{
// retrieve the socket from the state object
Socket client = (Socket)asyncResult.AsyncState;
// complete sending the data to the server
int bytesSent = client.EndSend(asyncResult);
// signal that all bytes have been sent
sendDone.Set();
}
catch (ObjectDisposedException objDispEx)
{
Debug.WriteLine(objDispEx.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
public void SendDataToServer(byte[] bytesToSend)
{
try
{
connectDone.Reset();
sendDone.Reset();
ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Parse(ServerIpAddress);
IPEndPoint remoteEndPoint = new IPEndPoint(ipAddress, ServerPort);
ClientSocket.BeginConnect(remoteEndPoint, new AsyncCallback(ClientConnectCallback), ClientSocket);
connectDone.WaitOne();
ClientSocket.BeginSend(bytesToSend, 0, bytesToSend.Length, 0, new AsyncCallback(ClientSendCallback), ClientSocket);
sendDone.WaitOne();
}
catch (ObjectDisposedException objDispEx)
{
Debug.WriteLine(objDispEx.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
ClientSocket.Shutdown(SocketShutdown.Both);
ClientSocket.Close();
}
}
private void buttonSendDataToServer_Click(object sender, EventArgs e)
{
BackgroundWorker bwSendDataToServer = new BackgroundWorker();
bwSendDataToServer.DoWork += bwSendDataToServer_DoWork;
bwSendDataToServer.RunWorkerCompleted += bwSendDataToServer_RunWorkerCompleted;
bwSendDataToServer.RunWorkerAsync();
}
private void bwSendDataToServer_DoWork(object sender, DoWorkEventArgs e)
{
byte[] bytesToSend = new byte[100];
SendDataToServer(bytesToSend);
}
private void bwSendDataToServer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Debug.WriteLine("BackgroundWorker has completed.");
}
}
}
"How do I report exceptions?" 是一个相当宽泛的问题。很难确定 具体 什么建议最适合您的情况,因为解决问题的方法有很多。
也就是说,恕我直言,您应该始终遵循一些通用规则,这些规则可以帮助指导您的设计:
- Do not block I/O threads with UI.
这意味着您的代码示例肯定应该得到改进,因为您在等待用户确认错误消息时延迟了用于调用回调的任何 I/O 线程。
- Observe the general OOP principle of "separation of concerns".
换句话说,即使显示错误消息的线程不是 I/O 线程,将用户交互逻辑放在主要的 class 中仍然不是一个好主意目的是处理网络 I/O.
当然还有其他有用的编程规则,但考虑到您当前的代码示例,我认为以上两个是最相关的。那么,如何解决这些问题呢?
至少,我会更改您当前的实现,以便 I/O 代码的 none 位于 MyForm
class 中。 MyForm
class 用于用户界面;它不应该让自己参与网络机制 I/O,而是应该将这项工作委托给其他人 class.
现在,另一个 class 仍然需要与 MyForm
通信。但它应该以独立于 MyForm
的方式这样做,即不依赖于(或 "coupled")class。在报告异常方面,一种常见的实现方式是声明一个事件,只要发生异常就会引发该事件。
这将解决第二点,但第一点仍然是一个问题。它可以通过使用 Control.BeginInvoke()
方法执行表单对象中的事件处理代码来解决,以便该代码在 UI 线程中执行,并且相对于 I/O 是异步的线程(即 BeginInvoke()
方法在返回之前不等待调用的代码完成)。
把所有这些放在一起,你可能会得到类似这样的东西:
class ExceptionReportEventArgs : EventArgs
{
public Exception Exception { get; private set; }
public ExceptionEventArgs(Exception exception)
{
Exception = exception;
}
}
class MyNetworkClient
{
public event EventHandler<ExceptionReportEventArgs> ExceptionReport;
private void OnExceptionReport(Exception exception)
{
EventHandler<ExceptionReportEventArgs> handler = ExceptionReport;
if (handler != null)
{
handler(this, new ExceptionReportEventArgs(exception));
}
}
// For example...
private void ClientConnectCallback(IAsyncResult asyncResult)
{
try
{
/* code omitted */
}
catch (SocketException sockEx)
{
// if the server isn't running, we'll get a socket exception here
OnExceptionReport(sockEx);
}
catch (ObjectDisposedException)
{
}
catch (Exception ex)
{
OnExceptionReport(ex);
}
}
}
partial class MyForm : Form
{
private MyNetworkClient _client;
public MyForm()
{
InitializeComponent();
_client = new MyNetworkClient();
_client.ExceptionReport += (sender, e) =>
{
BeginInvoke((MethodInvoker)(() =>
MessageBox.Show(e.Exception.Message, Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Error)));
};
}
}
现在,综上所述:正如我提到的,有很多方法可以解决基本问题。上述方法的替代方法是使用 async
/await
模式,它提供了一种机制,可以以一种易于阅读、命令式的方式干净地编写异步逻辑,同时仍然允许正确的在必须在 UI 线程中执行的代码和不需要执行的代码之间的转换。
我不会在这一点上详细介绍 — 周围有很多示例,并且需要对您发布的代码进行更大的返工 — 但基本思想是使用例如NetworkStream
,它有内置的 async
方法来处理 I/O,并允许 I/O 方法中的异常 "bubble up"。
在这种方法中,您不需要 ExceptionReport
事件。相反,您上面的 MyNetworkClient
class 将提供 async
方法,控制器-class(例如您的 MyForm
对象)可以调用这些方法来启动 I/O .如果发生异常,调用堆栈中的每个 async
方法都可以处理(如果需要)异常,然后重新抛出它(即使用普通的 throw;
语句),一直返回到 UI 代码,其中异常将在 UI 线程中引发并可以在那里呈现给用户,而不会阻塞 I/O 线程。
我认为总的来说,关键是要牢记通常的规则并遵守它们。最终,您会发现代码更易于编写和理解,并且在您选择的 UI 框架(例如 Winforms)中表现更好。