【问题标题】:TCP/IP chat program is sending me mixed messagesTCP/IP 聊天程序向我发送混合消息
【发布时间】:2011-01-12 19:55:55
【问题描述】:

我正在构建一个简单的 tcp/ip 聊天程序,但我很难单独发送消息。例如,如果我发送两条消息并且两者的内容都大于可以容纳 20 个字符的缓冲区,则发送第一条消息的 20 个字符,然后发送下一条消息的 20 个字符,然后发送第一条消息的其余部分,然后最后一条消息的其余部分。因此,当我解析和连接字符串时,我得到两条消息,分别是第一条消息的开头和第二条消息的开头以及第一条消息的结尾和第二条消息的结尾。我想知道如何发送消息,并将下一条消息排队,直到第一条消息已经发送。作为旁注,我使用的是异步方法调用而不是线程。

我的代码:

客户:

protected virtual void Write(string mymessage)
{


               var buffer = Encoding.ASCII.GetBytes(mymessage);
               MySocket.BeginSend(buffer, 0, buffer.Length, 
SocketFlags.None,EndSendCallBack, null);

               if (OnWrite != null)
               {
                   var target = (Control) OnWrite.Target;
                   if (target != null && target.InvokeRequired)
                   {
                       target.Invoke(OnWrite, this, new EventArgs());
                   }
                   else
                   {
                       OnWrite(this, new EventArgs());
                   }
               }
        }

还有两个混在一起的电话:

 client.SendMessage("CONNECT",Parser<Connect>.TextSerialize(connect));
  client.SendMessage("BUDDYLIST","");

最后是读取函数(我在每条消息的开头使用一个数字来知道消息何时结束,用括号括起来):

private void Read(IAsyncResult ar)
        {

            string content;
            var buffer = ((byte[]) ar.AsyncState);
            int len = MySocket.EndReceive(ar);
            if (len > 0)
            {
                string cleanMessage;
                content = Encoding.ASCII.GetString(buffer, 0, len);
                if (MessageLength == 0)
                {
                    MessageLength = int.Parse(content.Substring(1, content.IndexOf("]", 1) - 1));
                    cleanMessage = content.Replace(content.Substring(0, content.IndexOf("]", 0) + 1), "");
                }
                else
                    cleanMessage = content;

                if(cleanMessage.Length <1)
                {
                    if(MySocket.Connected)
                        MySocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Read), buffer);
                    return;
                }

                MessageLength = MessageLength > cleanMessage.Length? MessageLength - cleanMessage.Length : 0;
                amessage += cleanMessage;

                if(MessageLength == 0)
                {
                    if (OnRead != null)
                    {
                        var e = new CommandEventArgs(this, amessage);
                        Control target = null;
                        if (OnRead.Target is Control)
                            target = (Control)OnRead.Target;
                        if (target != null && target.InvokeRequired)
                            target.Invoke(OnRead, this, e);
                        else
                            OnRead(this, e);
                    }
                    amessage = String.Empty;
                }
                MySocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Read), buffer);
                    return;
            }
        }

【问题讨论】:

  • TCP 不适用于消息的概念。它提供了一个流,仅此而已。您需要在此基础上编写一种机制来将流拆分为消息。
  • 我在字符串开头的括号中有一个数字,用于知道消息何时结束,即[3]嘿。问题是我会得到 [3]h 然后是 [3]y nad 然后是 'ow' 然后是 'you' 如果我发送了 '[3]how' 和 '[3]' 你。仅当缓冲区为 4 时。
  • 所以你发送“[3]how[3]you”并接收“[3]h[3]yowyou”?对我来说听起来难以置信。但我通常用单线程编写我的网络代码。我怀疑您的线程代码编写不正确。
  • 但是 TCP 允许在流中的任何字节之间插入数据包边界,它也可以连接多条消息。对 Send 和 Receive 的调用之间没有 1to1 映射。除非您以不正确的方式使用线程,否则顺序是保留的。
  • 这种情况总是发生在 TCP/IP 上,不可能知道她的感受。

标签: c# .net sockets tcp


【解决方案1】:

TCP保证您会在一次阅读中收到整个消息。因此,您需要能够检测消息的开始位置和结束位置。

您通常通过在消息末尾添加一些特殊字符来做到这一点。或者在实际消息之前使用长度标头。

您通常不需要在客户端中使用BeginSend。发送应该足够快,并且还会降低复杂性。另外,我通常也不会在服务器中使用BeginSend,除非服务器应该非常高效。

更新

实际的套接字实现永远不会混合您的消息,只有您的代码才能做到这一点。您不能通过调用多个发送来发送消息,因为如果您的应用程序是多线程的,那么您的消息将被混合。

换句话说,这是行不通的:

_socket.BeginSend(Encoding.ASCII.GetBytes("[" + message.Length + "]"))
_socket.BeginSend(Encoding.ASCII.GetBytes(message));

您必须一次发送所有内容。

更新 2

您的读取实现没有考虑到两条消息可以进入同一个读取。这很可能是导致您的信息混乱的原因。

如果您发送:

[11]Hello world
[5]Something else

他们可以到达:

[11]Hello World[5]Some
thing else

换句话说,第二条消息的一部分可以到达第一条BeginRead。您应该始终构建一个包含所有接收到的内容的缓冲区(使用StringBuilder)并移除处理过的部分。

伪代码:

method OnRead
    myStringBuilder.Append(receivedData);
    do while gotPacket(myStringBuilder)
        var length = myStringBuilder.Get(2, 5)
        if (myStringBuilder.Length < 7 + length)
           break;

        var myMessage = myStringBuilder.Get(7, length);
        handle(myMessage);

        myStringBuilder.Remove(0, 7+length);
    loop
end method

你看到我在做什么了吗?我总是将接收到的数据附加到字符串生成器中,然后删除完整的消息。我正在使用循环,因为多条消息可以同时到达。

【讨论】:

  • 我复制并粘贴了上面的回复:'我在字符串开头的括号中有一个数字,可以知道消息何时结束,即[3]嘿。问题是我会得到 [3]h 然后是 [3]y nad 然后是 'ow' 然后是 'you' 如果我发送了 '[3]how' 和 '[3]' 你。仅当缓冲区为 4 时。'
  • 我有一个机制,我知道消息何时结束,问题是消息的混合。
  • 我只是想检查一下我对您的 cmets 问题的理解......异步调用是否有效地在不同的线程中运行,所以它们轮流写入 IP 流和因此消息变得混乱?我假设解决方案是在代码中有一个队列来包含消息,并且只有一个线程访问它。或者,我假设如果您使用 send 而不是 beginend 它将使其全部成为单线程(就我们看到的代码而言),因此它会正常工作吗?
  • BeginSend 不会混合消息,即使从许多不同的线程同时调用,只要整个消息都使用一个 BeginSend 发送。如果您的消息与您的代码示例一样使用 BeginSend 发送,则问题很可能是我在第二次更新中描述的问题。
【解决方案2】:

如果您在一次发送调用中发送整条消息,它将按顺序到达。您需要一个分隔符来分隔您的消息。在接收端,您需要将消息缓冲到缓冲区中。如果你不发送大量数据,你可以让自己有点草率,但很容易编码来处理传入的消息:

// Note: Untested pseudocode
buffer += stringDataRead;
while (buffer.Contains("\r\n")) {
  // You may want to compensate for \r\n by doing a -2 here and +2 on next line
  line = buffer.Substring(0, buffer.IndexOf("\r\n"));
  buffer = buffer.Remove(0, line.Length);

  DoSomehingWithThisLine(line);
}

不需要魔法。 :)

【讨论】:

  • 我不会为此使用字符串,在相当繁忙的服务器中内存消耗会很大。但是 +1 用于制作比我更清晰的样本。
猜你喜欢
  • 1970-01-01
  • 2017-09-27
  • 1970-01-01
  • 1970-01-01
  • 2011-08-22
  • 2014-08-27
  • 1970-01-01
  • 1970-01-01
  • 2015-12-01
相关资源
最近更新 更多