7

我目前正在开发一个可以接受来自多台客户端计算机的多个连接的 C# Socket 服务器。服务器的目标是允许客户端“订阅”和“取消订阅”服务器事件。

到目前为止,我已经在这里愉快地看了一下:http: //msdn.microsoft.com/en-us/library/5w7b7x5f (v=VS.100).aspx和http://msdn.microsoft.com/ en-us/library/fx6588te.aspx的想法。

我发送的所有消息都是加密的,因此我将我希望发送的字符串消息转换为 byte[] 数组,然后在将消息长度预先附加到数据并通过连接发送出去之前加密数据.

让我印象深刻的一个问题是:在接收端,当只收到一半的消息时,Socket.EndReceive()(或相关的回调)似乎有可能返回。有没有一种简单的方法来确保每条消息都被“完整”接收并且一次只接收一条消息?

编辑:例如,我认为 .NET / Windows 套接字不会“包装”消息以确保在一次 Socket.Receive() 调用中接收到使用 Socket.Send() 发送的单个消息?或者是吗?

到目前为止我的实现:

private void StartListening()
{
    IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
    IPEndPoint localEP = new IPEndPoint(ipHostInfo.AddressList[0], Constants.PortNumber);

    Socket listener = new Socket(localEP.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
    listener.Bind(localEP);
    listener.Listen(10);

    while (true)
    {
        // Reset the event.
        this.listenAllDone.Reset();

        // Begin waiting for a connection
        listener.BeginAccept(new AsyncCallback(this.AcceptCallback), listener);

        // Wait for the event.
        this.listenAllDone.WaitOne();
    }
}

private void AcceptCallback(IAsyncResult ar)
{
    // Get the socket that handles the client request.
    Socket listener = (Socket) ar.AsyncState;
    Socket handler = listener.EndAccept(ar);

    // Signal the main thread to continue.
    this.listenAllDone.Set();

    // Accept the incoming connection and save a reference to the new Socket in the client data.
    CClient client = new CClient();
    client.Socket = handler;

    lock (this.clientList)
    {
        this.clientList.Add(client);
    }

    while (true)
    {
        this.readAllDone.Reset();

        // Begin waiting on data from the client.
        handler.BeginReceive(client.DataBuffer, 0, client.DataBuffer.Length, 0, new AsyncCallback(this.ReadCallback), client);

        this.readAllDone.WaitOne();
    }
}

private void ReadCallback(IAsyncResult asyn)
{
    CClient theClient = (CClient)asyn.AsyncState;

    // End the receive and get the number of bytes read.
    int iRx = theClient.Socket.EndReceive(asyn);
    if (iRx != 0)
    {
        // Data was read from the socket.
        // So save the data 
        byte[] recievedMsg = new byte[iRx];
        Array.Copy(theClient.DataBuffer, recievedMsg, iRx);

        this.readAllDone.Set();

        // Decode the message recieved and act accordingly.
        theClient.DecodeAndProcessMessage(recievedMsg);

        // Go back to waiting for data.
        this.WaitForData(theClient);
    }         
}
4

4 回答 4

10

是的,有可能每次接收只有部分消息,在传输过程中可能会更糟,只发送部分消息。通常,您可以在网络状况不佳或网络负载较重的情况下看到这一点。

要明确网络级别的 TCP 保证以指定的顺序传输您的数据,但不能保证部分数据与您发送的数据相同。该软件(例如,看看Nagle 的算法)、硬件(跟踪中的不同路由器)、操作系统实现有很多原因,所以通常你不应该假设哪些部分数据已经传输或接收。

抱歉介绍太长,以下是一些建议:

  1. 尝试为高性能套接字服务器使用相关的“新”API,这里是 .NET v4.0 的示例网络示例

  2. 不要假设您总是发送完整的数据包。Socket.EndSend()返回实际计划发送的字节数,在网络负载较重的情况下甚至可以是 1-2 个字节。因此,您必须在需要时重新发送缓冲区的其余部分。

    MSDN上有警告:

    无法保证您发送的数据会立即出现在网络上。为了提高网络效率,底层系统可能会延迟传输,直到收集到大量传出数据。BeginSend 方法的成功完成意味着底层系统有空间来缓冲您的数据以进行网络发送。

  3. 不要假设您总是收到完整的数据包。将接收到的数据加入某种缓冲区,并在有足够数据时对其进行分析。

  4. 通常,对于二进制协议,我添加字段来指示有多少数据传入,带有消息类型的字段(或者您可以使用每个消息类型的固定长度(通常不好,例如版本控制问题)),版本字段(如果适用)并添加 CRC -字段到消息的结尾。

  5. 它不需要阅读,有点旧,直接适用于 Winsock,但可能值得研究:Winsock Programmer's FAQ

  6. 看看ProtocolBuffers,值得学习:http ://code.google.com/p/protobuf-csharp-port/ , http://code.google.com/p/protobuf-net/

希望能帮助到你。

PS 可悲的是,您提到的 MSDN 上的示例有效地破坏了其他答案中所述的异步范式。

于 2011-03-24T15:53:50.963 回答
5

您的代码非常错误。做这样的循环违背了异步编程的目的。异步 IO 用于不阻塞线程,而是让它们继续做其他工作。通过这样循环,您正在阻塞线程。

void StartListening()
{
    _listener.BeginAccept(OnAccept, null);
}

void OnAccept(IAsyncResult res)
{
    var clientSocket = listener.EndAccept(res);

    //begin accepting again
    _listener.BeginAccept(OnAccept, null);

   clientSocket.BeginReceive(xxxxxx, OnRead, clientSocket);
}

void OnReceive(IAsyncResult res)
{
    var socket = (Socket)res.Asyncstate;

    var bytesRead = socket.EndReceive(res);
    socket.BeginReceive(xxxxx, OnReceive, socket);

    //handle buffer here.
}

请注意,我已删除所有错误处理以使代码更清晰。该代码不会阻塞任何线程,因此效率更高。我会将代码分为两类:服务器处理代码和客户端处理代码。它使维护和扩展更容易。

接下来要了解的是 TCP 是一种流协议。它不保证消息在一个 Receive 中到达。因此,您必须知道消息有多大或何时结束。

第一个解决方案是在每条消息前面加上一个你首先解析的标题,然后继续阅读,直到你得到完整的正文/消息。

第二种解决方案是在每条消息的末尾放置一些控制字符并继续读取,直到读取控制字符为止。请记住,如果该字符可以存在于实际消息中,则应对该字符进行编码。

于 2011-03-24T15:38:45.393 回答
2

您需要发送固定长度的消息或在标头中包含消息的长度。试着有一些东西可以让你清楚地识别数据包的开始。

于 2011-03-23T16:33:25.860 回答
2

我发现了 2 个非常有用的链接:

http://vadmyst.blogspot.com/2008/03/part-2-how-to-transfer-fixed-sized-data.html

C# 异步 TCP 套接字:处理缓冲区大小和大量传输

于 2011-07-31T02:35:26.067 回答