【问题标题】:Handling exceptions in asynchronous socket callbacks处理异步套接字回调中的异常
【发布时间】:2015-05-07 19:09:19
【问题描述】:

我一直在努力解决套接字通信问题,并且我已经组装了一个小型 Windows 窗体应用程序作为测试。这基本上是一个客户端,它将连接到服务器,发送一些字节并断开连接。最终,我也会收到来自服务器的响应,但我现在基本上已经把它去掉了。

据我所知,这段代码可以正常工作,但由于数据的发送是由于在我的 UI 中单击按钮而发生的,因此它需要是异步的。我有一个名为SendDataToServer(byte[] bytesToSend) 的方法,它连接到服务器并传输数据。在我的按钮单击事件处理程序中,我创建了一个将调用此方法的后台工作程序。

如果我的服务器没有启动并运行,我显然会得到一个套接字异常,当然还有其他原因可能会在尝试连接和传输数据的过程中引发异常。使用 backgroundworker 和异步套接字回调(ClientConnectCallback()ClientSendCallback()),确保任何异常都冒泡并在我的 UI 中正确显示的最佳方法是什么?

现在我在异步回调本身的 catch 块内有 MessageBox,但我想知道这是否真的是显示消息的地方,或者是否应该将它们传递回来?

我的代码如下所示:

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.");
        }
    }
}

【问题讨论】:

标签: c# sockets exception-handling backgroundworker


【解决方案1】:

“我如何报告异常情况?”是一个相当广泛的问题。很难确定哪种具体建议最适合您的情况,因为解决问题有很多选择。

也就是说,恕我直言,您应该始终遵循一些一般规则,这些规则可以帮助指导您的设计:

  1. 不要使用 UI 阻塞 I/O 线程。

这意味着您的代码示例当然应该得到改进,因为您在等待用户确认错误消息时会延迟用于调用回调的任何 I/O 线程。

  1. 遵守“关注点分离”的一般 OOP 原则。

换句话说,即使显示错误消息的线程不是 I/O 线程,在主要目的是处理网络 I/O 的类中包含用户交互逻辑仍然不是一个好主意.

当然还有其他有用的编程规则,但考虑到您当前的代码示例,我认为上述两个最相关。那么,如何解决这些问题呢?


至少,我会更改您当前的实现,以便所有 I/O 代码都不在 MyForm 类中。 MyForm 类用于用户界面;它不应该涉及网络 I/O 的机制,而应该将这项工作委托给其他一些类。

现在,其他类仍然需要与MyForm 通信。但它应该以独立于MyForm 的方式这样做,即不依赖于(或“耦合”)类。就报告异常而言,实现此目的的一种常见方法是声明一个在发生异常时引发的事件。

这将解决第二点,但第一点仍然是一个问题。可以通过Control.BeginInvoke()方法执行form对象中的事件处理代码来解决,使该代码在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 方法中“冒泡”。

在这种方法中,您不需要ExceptionReport 事件。相反,您上面的 MyNetworkClient 类将提供控制器类(例如您的 MyForm 对象)可以调用以启动 I/O 的 async 方法。如果发生异常,调用堆栈中的每个 async 方法都可以处理(如果需要)异常,然后重新抛出它(即使用普通的 throw; 语句),一直返回到 UI 代码,异常将在 UI 线程中引发并可以在那里呈现给用户,而不会阻塞 I/O 线程。


我认为总的来说,关键是记住通常的规则并坚持下去。最终,您会发现代码更易于编写和理解,并且在您选择的 UI 框架(例如 Winforms)中表现更好。

【讨论】:

  • 非常感谢彼得的所有建议。
猜你喜欢
  • 2013-07-13
  • 2023-03-30
  • 2012-08-20
  • 1970-01-01
  • 1970-01-01
  • 2014-08-01
  • 2019-08-29
  • 1970-01-01
  • 2019-10-04
相关资源
最近更新 更多