在异步服务器套接字之外传递内容

Passing content outside of an asynchronous server socket

我什至不确定这是如何工作的,因为就其本质而言,异步服务器套接字可以接受多个连接。

我想做的是捕获符合特定格式的内容并将其传递到服务器套接字之外,以便其他 类 可以引用它。我遵循了构建异步服务器套接字的 MSDN 代码。这是读取内容的回调。

public static void ReadCallback(IAsyncResult ar)
{
    string content = String.Empty;

    // Retrieve the state object and the handler socketH:\JCI\BWSI\Integrations\Middleware\Jci\Jci\Framework.Jci.EventEngine\EventEngineRTLSEvents.cs
    // from the asynchronous state object.
    StateObject state = (StateObject)ar.AsyncState;
    Socket handler = state.workSocket;

    // Read data from the client socket
    int bytesRead = handler.EndReceive(ar);

    if (bytesRead > 0)
    {
        // There might be more data, so store the data recieved so far.
        state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));

        // Check for end-of-file tag. If it is not there, read more data.
        content = state.sb.ToString();
        if (content.IndexOf('\u001c') > -1)
        {
            // ALl the data has been read from the
            // client. Display it on the console.
            Console.WriteLine(
                "Read {0} bytes from socket. \n Data : {1}", content.Length, content);
            // Echo the data back to the client.
            Send(handler, content);
        }
        else
        {
            // Not all data recieived. Get more.
            handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                        new AsyncCallback(ReadCallback), state);
        }
    }
}

如果我对问题的理解正确,基本问题是这样的:当在套接字上接收到一些数据时,这会导致在您的程序中创建一些新对象,并且您希望一些代码对该对象进行操作以以某种方式处理它。

那么,让我们这样想:当你想要执行一些代码时,你是如何实现的?由于在C#中所有的代码都存在于方法中,所以需要调用一个方法让代码执行。

现在,在异步处理套接字的情况下,您有一些选择。代码总是在一个线程的上下文中执行,所以你需要考虑你想要哪个线程来执行这个特定的代码。这实际上相当于只有两个选项:1) 在当前线程中执行,或 2) 在不同的线程中执行。

好的,现在我们开始了。如果我们选择选项#1,那会如何发生?很简单...只需从您的 ReadCallback() 方法中调用该方法即可。如果您创建了一个对象,您希望调用的方法对其进行处理,只需在调用它时将该对象传递给该方法即可。

真的就是这么简单(*).

现在,如果您想选择#2 怎么办?嗯……这有点复杂。首先,你需要找到一个线程来执行代码,其次,你需要获取数据到那个线程。

我至少可以想到三种明显的方法来解决这个问题:

  1. 使用现有的 UI 线程。在这种情况下,您将使用例如Control.Invoke() 方法或 Dispatcher.Invoke()(分别用于 Winforms 或 WPF)。 ASP.NET.
  2. 中提供了类似的机制
  3. 使用线程池,例如通过任务并行库。例如,您可以使用 Task.Run() 方法开始新任务。
  4. 使用 producer/consumer 实现,您之前在其中启动了一个专用于使用接收数据时创建的数据对象的线程。例如,您可以使用仅使用 foreachBlockingCollection<T> 实例中提取项目的方法启动一个新线程,而 ReadCallback() 方法将项目添加到同一实例。

在上面的前两个选项中,数据通过被调用方法的参数移动到另一个线程,或者作为被调用的匿名方法中的捕获变量(我发现后者比前者更方便,但要么工作正常)。在第三个选项中,显然数据通过共享集合从套接字线程移动到消费线程。

我希望以上内容足以让您指明正确的方向。正如所问的那样,这个问题相当广泛(可能太广泛了),但我认为我在这里写的内容仍然相当简洁,只是足够模糊以保持适用于您的任何场景,而不会模糊到非 -有用。 :)


(*) 实际上,它比这稍微复杂一点,因为您有许多调用方法的机制。最简单的是方法名被硬编码到你的 ReadCallback() 方法中;你只需调用一个你知道你总是想调用的方法。但这限制了代码的可重用性,并将其耦合到不相关的代码,这使得它更难维护。

其他选项包括:

  1. 在存在 ReadCallback() 方法的套接字客户端对象上声明一个事件,并让应该实际处理数据的对象订阅该事件。 ReadCallback() 方法将引发事件,将对象作为事件参数的一部分传递给处理。
  2. 只需将回调委托传递给套接字客户端对象,这与您当前将代表 ReadCallback() 方法的委托传递给套接字 class.
  3. 的方式非常相似

首先,这并不是真正的答案,但我发现写很多文本作为评论是有问题的。

我现在已经真正查看了您的代码,也许我开始理解您的问题了。你对短语 "pass it outside the server socket" 的使用部分让我感到困惑 - 你的意思是 "give the data to a method that is not a dedicated part of the Socket processing code",对吧?

执行此操作的简单方法是,一旦您积累了完整的 "logical message" 就是从您的异步 ReadCallback 方法调用一个方法来处理它。所以业务逻辑其实就是运行作为Socket代码的一个子程序。但这仅适用于不会阻塞任何时间长度的琐碎处理。

一种更常见的技术是使用多线程和跨线程调度技术。那么业务逻辑是半独立于Socket代码的。有许多可能的方法可以做到这一点。我已经编写了显式使用 AutoResetEvent 和消息队列(带锁)的代码,但它有点混乱。现代方式(我个人没有使用过)是通过 C# async 和 await 工具。

最后,对您提供的代码提出一些意见。更安全的做法是不使用文件结尾标记,而是在每个逻辑消息前加上消息长度,例如通过 BitConverter 将 Int32 encoded/decoded 添加到 4 字节数组(注意大端与 4 字节数组)。小端)。文件结束标记的问题在于,可以想象标记可以在对 ReadCallback 方法的两次调用之间拆分。

同样,最好先将整个消息累积为原始字节,然后再将整个消息解码回字符串。同样,问题可能是 UTF-8 两字节序列可能会在两次调用 ReadCallback 时被拆分。

希望对您有所帮助。

编辑:

只是想提一下,TCP/IP 输入被认为是字节流这一事实确实使处理变得棘手。我已经指出长度前缀比文件结束标记更安全,并且在从 UTF-8 转换为字符串之前累积整个消息​​比转换单个段更安全。

但除此之外,在尝试将其转换为长度之前,您必须注意至少有 4 个字节。可以想象得到一条消息加上下一条消息的前 2 个字节,所以你只有第二条消息长度前缀的一半。然后你必须保存这 2 个字节并等待下一次调用,然后才能转换长度。

通常您会在每次通话时收到完整的消息,这种流式传输导致问题的情况非常罕见。而且它在测试期间永远不会发生。但根据墨菲定律,它最终会发生,而且是在最坏的时候。