简体   繁体   English

处理异步套接字回调中的异常

[英]Handling exceptions in asynchronous socket callbacks

I've been trying to get my head wrapped around socket communications and I've put together a small Windows Forms application as a test. 我一直在试图让我的脑袋围绕套接字通信,我已经把一个小的Windows Forms应用程序作为测试。 This is basically a client that will connect to a server, send some bytes and disconnect. 这基本上是一个客户端,它将连接到服务器,发送一些字节并断开连接。 Ultimately, I'll receive a response from the server, too, but I've mostly stripped that out for now. 最终,我也会收到来自服务器的回复,但我现在大部分时间都将其删除。

As best I can tell, this code works properly but since the sending of the data occurs as a result of a button click in my UI, it needs to be asynchronous. 我可以说,这段代码工作正常,但由于在我的UI中点击按钮会发送数据,因此需要异步。 I have a method called SendDataToServer(byte[] bytesToSend) that connects to the server and transmits the data. 我有一个名为SendDataToServer(byte[] bytesToSend) ,它连接到服务器并传输数据。 In my button click event handler, I created a backgroundworker which will call this method. 在我的按钮单击事件处理程序中,我创建了一个将调用此方法的后台工作程序。

If my server isn't up and running, I'll obviously get a socket exception and certainly there are other reasons that an exception could get thrown during the process of attempting to connect and transfer data. 如果我的服务器没有启动并运行,我显然会遇到套接字异常,当然还有其他原因导致在尝试连接和传输数据的过程中抛出异常。 With the backgroundworker and the async socket callbacks ( ClientConnectCallback() and ClientSendCallback() ), what's the best way to make sure that any exceptions get bubbled up and displayed properly in my UI? 使用backgroundworker和异步套接字回调( ClientConnectCallback()ClientSendCallback() ),什么是确保在我的UI中冒泡并正确显示任何异常的最佳方法?

Right now I have MessageBoxes inside the catch blocks within the async callbacks themselves but I'm wondering if this is really the place to be displaying the messages or if they should be passed back up? 现在我在异步回调本身的catch块中有MessageBoxes,但我想知道这是否真的是显示消息的地方,还是应该将它们传回去?

Here's what my code looks like: 这是我的代码的样子:

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?" “我如何举报异常?” is a pretty broad question. 是一个非常广泛的问题。 It's hard to know for sure what specific advice would be best in your case, as there are many options for solving the problem. 很难确定哪种具体建议最适合您的情况,因为解决问题有很多选择。

That said, there are some general rules that IMHO you should always follow, and these can help direct your design: 也就是说,你应该总是遵循一些恕我直言的一般规则,这些可以帮助指导你的设计:

  1. Do not block I/O threads with UI. 不要使用UI阻止I / O线程。

This means that for sure, your code example should be improved, as you are delaying any I/O thread used to call your callbacks while waiting for the user to acknowledge the error message. 这意味着肯定会改进您的代码示例,因为您在等待用户确认错误消息时延迟用于调用回调的任何I / O线程。

  1. Observe the general OOP principle of "separation of concerns". 遵守“关注点分离”的一般OOP原则。

In other words, even if the thread that is presenting the error message isn't an I/O thread, it's still not a good idea to have user interaction logic inside a class that's main purpose is to handle network I/O. 换句话说,即使呈现错误消息的线程不是I / O线程,在一个主要目的是处理网络I / O的类中使用用户交互逻辑仍然不是一个好主意。

There are of course other useful rules for programming, but I think the above two are most relevant considering your current code example. 当然还有其他有用的编程规则,但我认为考虑到你当前的代码示例,上面两个是最相关的。 So, how to address these? 那么,如何解决这些问题呢?


At a minimum, I would change your current implementation so that none of the I/O code was in the MyForm class. 至少,我会更改您当前的实现,以便MyForm类中没有任何I / O代码。 The MyForm class is for user-interface; MyForm类用于用户界面; it should not involve itself with the mechanisms of network I/O, but rather should delegate that work to some other class. 它不应该涉及网络I / O的机制,而应该将该工作委托给其他类。

Now, that other class still needs to communicate with MyForm . 现在,其他类仍然需要与MyForm通信。 But it should do so in a way that is independent of MyForm , ie which is not tied to (or "coupled") the class. 但是它应该以独立于MyForm的方式这样做,即不与类相关联(或“耦合”)。 In terms of reporting exceptions, one common way to accomplish this is to declare an event that is raised whenever an exception occurs. 在报告异常方面,实现此目的的一种常见方法是声明在发生异常时引发的事件。

That would address the second point, but the first point is still an issue. 这将解决第二点,但第一点仍然是一个问题。 It can be resolved by using the Control.BeginInvoke() method to execute the event-handling code in the form object, so that that code is executed in the UI thread, and asynchronously relative to the I/O thread (ie the BeginInvoke() method doesn't wait for the invoked code to complete before returning). 可以通过使用Control.BeginInvoke()方法在表单对象中执行事件处理代码来解决它,以便该代码在UI线程中执行,并且相对于I / O线程异步(即BeginInvoke()方法不会在返回之前等待调用的代码完成)。

Putting all that together, you might get something like this for example: 把所有这些放在一起,你可能得到这样的东西,例如:

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


Now, all that said: as I mentioned, there are lots of ways to address the basic issues. 现在,所有这些说:正如我所提到的,有很多方法可以解决基本问题。 An alternative to the above would be to use the async / await pattern, which provides a mechanism to cleanly write asynchronous logic in an easy-to-read, imperative-style way, while still allowing for correct transitions between code that has to execute in the UI thread and code that does not. 上面的替代方法是使用async / await模式,它提供了一种以易于阅读,命令式方式干净地编写异步逻辑的机制,同时仍然允许在必须执行的代码之间进行正确的转换。 UI线程和代码没有。

I won't go into a lot of detail on that point — there are a lot of examples around, and it would require a much larger rework of the code you posted — but the basic idea would be to use eg NetworkStream , which has built-in async methods to handle the I/O, and to let exceptions "bubble up" from the I/O methods. 我不会在这一点上详细介绍 - 有很多例子,它需要对你发布的代码进行更大规模的返工 - 但基本的想法是使用例如已构建的NetworkStream -in async方法来处理I / O,并允许异常从I / O方法“冒泡”。

In this approach, you wouldn't need the ExceptionReport event. 在这种方法中,您不需要ExceptionReport事件。 Instead, your MyNetworkClient class above would offer async methods that a controller-class (eg your MyForm object) can call to initiate I/O. 相反,上面的MyNetworkClient类将提供控制器类(例如您的MyForm对象)可以调用以启动I / O的async方法。 If an exception occurs, each async method in the call stack can handle (if needed) the exception but then rethrow it (ie with a plain throw; statement), all the way back to the UI code, where the exception will be raised in the UI thread and can be presented to the user there, without blocking an I/O thread. 如果发生异常,调用堆栈中的每个async方法都可以处理(如果需要)异常,但随后重新抛出它(即使用普通throw;语句),一直回到UI代码,异常将在其中引发UI线程可以在那里呈现给用户,而不会阻塞I / O线程。


I think in general, the key is to just keep in mind the usual rules and stick with them. 我认为一般来说,关键是要牢记通常的规则并坚持下去。 Ultimately, you will find the code is easier to write and understand, and will behave better in your chosen UI framework (eg Winforms). 最终,您会发现代码更易于编写和理解,并且在您选择的UI框架(例如Winforms)中表现更好。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM