C#Socket.Receive消息长度

时间:2011-03-23 16:28:12

标签: c# sockets asynchronous tcp asynccallback

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

到目前为止,我已经对此进行了快速的审视:http://msdn.microsoft.com/en-us/library/5w7b7x5f(v=VS.100).aspxhttp://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 个答案:

答案 0 :(得分:10)

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

要明确网络级别,TCP保证按指定顺序传输数据但不保证部分数据与您发送的数据相同。这个软件有很多原因(比如看Nagle's algorithm),硬件(跟踪中的不同路由器),操作系统实现,所以一般情况下你不应该假设已经传输或接收了哪些数据。

很抱歉很长时间的介绍,下面是一些建议:

  1. 尝试为高性能套接字服务器使用相关的“新”API,此处为示例Networking Samples for .NET v4.0

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

    MSDN上有警告:

      

    无法保证数据   你发送的内容将出现在网络上   立即。增加网络   效率,底层系统可能   延迟传输直到显着   收集的传出数据量。   顺利完成了   BeginSend方法意味着   底层系统有空间   缓冲数据以进行网络发送。

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

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

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

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

  7. 希望它有所帮助。

    P.S。遗憾的是,你提到的MSDN样本有效地破坏了异步范式,如其他答案中所述。

答案 1 :(得分:4)

你的代码非常错误。像这样做循环会破坏异步编程的目的。 Async 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。因此,您必须知道消息的大小或消息的结束时间。

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

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

答案 2 :(得分:2)

您需要发送固定长度的消息或在标头中包含消息的长度。尝试使用能够清楚识别数据包开始的内容。

答案 3 :(得分:1)