Winsock2 tcp/ip - 一些数据包被忽略可能是由于前一个数据包的空终止符

Posted

技术标签:

【中文标题】Winsock2 tcp/ip - 一些数据包被忽略可能是由于前一个数据包的空终止符【英文标题】:Winsock2 tcp/ip - some data packets are ignored probably due to null terminator from the previous packet 【发布时间】:2013-12-20 10:40:05 【问题描述】:

我写了一个简单的客户端-服务器程序。 Network.h 是一个头文件,它使用 Winsock2.h(TCP/IP 模式)创建套接字,在阻塞模式下接受/连接,在非阻塞模式下发送/接收。我这样做是为了让函数string TNetwork::Recv(int size) 在收到 WSAWOULDBLOCK 错误(尚未收到数据)时返回字符串“Nothing”

这是我的主要功能:

int main()
    string Ans;
    TNetwork::StartUp(); //WSA start up, etc
    cin >> Ans;
    if (Ans == "0") // 0 --> server
        TNetwork::SetupAsServer(); //accept connection (in blocking mode!)
        while (true)
            TNetwork::Send("\nAss" + '\0'); //without null terminator, the client may read extra bytes, causing undefined behavior (?)
            TNetwork::Send("embly" + '\0');
            cin >> Ans;
        
    
    else // others --> regard Ans as IP address. e.g. I can type "127.0.0.1"
        TNetwork::SetupAsClient(Ans);
        string Rec;
        while (true)
            Rec = TNetwork::Recv(1000);
            if (Rec != "Nothing")
                cout << Rec;
            
        
    
    system("PAUSE");

假设客户端在连接时会打印“Assembly”,并且当服务器在其控制台窗口中输入任何内容时。但有时,客户端只会在控制台中打印出“\nAss”,而没有“embly”。

据我了解,TCP/IP 可确保以正确的顺序发送所有数据,因此我猜会发生两个数据包同时到达的情况,这在不稳定的互联网上经常发生。由于这个空终止符,客户端会忽略“embly”,因为 Recv() 函数在遇到空终止符时会停止读取。

那么,如何确保客户端始终正确读取所有数据包?

【问题讨论】:

如果有一个解决方案可以发送没有空终止符的数据包,不知何故,我也不介意。如果它有效,我会更改代码的任何部分。我对winsock2(以及套接字编程)相对较新,所以我可能有一些基本错误。 Null 终止和 TCP 没有任何关系。你在这里叫错树了。 TCP 发送和接收 API 采用指针和长度参数,它们不对内容进行任何解释。 【参考方案1】:

是的,网络堆栈会以正确的顺序发送数据,而不关心您使用什么终止类型。这与您接收和处理数据 stream 的方式有关(注意:不是数据包,stream)。如果接收到全部 11 个字节并将其打印到屏幕上,print 函数将在它到达零时停止,但其余数据仍然存在。

注意:既然它是一个流,如果你只从流中接收到 10 个字节的数据会发生什么?您需要扫描您收到的内容是否为零,以了解您是否收到了完整的“以零结尾的字符串”,如果这就是您想要传达数据的方式。

编辑:另外,我不认为"\nAss" + '\0' 正在做你认为的那样。它不是在字符串末尾添加 0 字符(顺便说一下,它已经有一个),而是在字符串指针中添加 0。

【讨论】:

感谢您指出我的错误!早些时候,我尝试打印出我传递给 recv 的 *char 变量并且不发送空终止符,我会收到随机字节,但我没有意识到它来自打印函数,所以我认为 winsock 需要一个空终止符经验值。另外我应该知道所有字符串对象都已经有空终止符,但我错过了这个事实。现在一切都清楚了:D。【参考方案2】:

正如@mark 所指出的,TCP 是关于流的,而不是数据包。 TCP 负责确保数据从 A 可靠地传输到 B,并按照传输顺序将数据交付给消费者。是的,数据是在线上打包的,但是系统上的 TCP 堆栈获取这些数据包并构建它通过recv() 函数提供给您的流。 TCP 堆栈处理乱序数据、丢失数据和重复数据,这样当您的应用程序看到它时,流就是发送方发送时间的镜像副本。

要正确接收 TCP 数据,您通常需要某种循环,以便在套接字可用时从套接字读取数据。我通常这样做的方式是有一个专用于服务套接字的线程。在线程函数中是一个循环,当套接字可用时从套接字读取数据,否则空闲。这个循环将数据读入一个 1 KB 的缓冲区。一旦从套接字接收到数据到这个缓冲区,缓冲区就会被复制到另一个线程进行处理。在处理线程的线程函数中有一个循环,它从套接字线程接收 1 KB 缓冲区并将它们添加到例如 1 MB 的主缓冲区的后端。然后,处理线程处理来自这个主缓冲区的消息,并使它们可供应用程序使用。

对于一个简单的演示应用程序,两个线程可能是多余的。我描述的两个线程当然可以合并为一个,但是对于我的应用程序,拥有两个线程并利用我系统上的多个内核会更有效。关键是,如果您要拥有一个前端 UI,那么就没有办法绕过使用至少一个线程并仍然让 UI 具有响应性。

另一件事。协议设计有两种常用的机制。您正在使用一个标记(例如,空终止符等)来表示消息的开始/结束。我不喜欢这种机制,主要是因为标记实际上可能需要在某些时候成为消息的一部分。另一种机制是在每条消息上都有一个标头,该标头至少可以说明消息的长度。我更喜欢这种机制,并在我的标题中包含一个同步词和消息类型。例如,

struct Header

    __int16 _sync;  // a hex pattern, e.g., 0xABCD
    __int16 _type;
    __int32 _length;

总共 8 个字节。因此,当从主缓冲区进行处理时,我读取了前 8 个字节,验证了同步字,并获得了长度。我确定主缓冲区中是否有可用的“长度”字节。如果没有,我必须等到套接字线程为我提供更多数据后才能再次检查。如果是这样,我从主缓冲区中提取“长度”字节并将其传递给根据指定类型创建的对象,该对象知道如何解释该特定消息。然后重复。

正如我提到的,我使用 1 MB 左右的主缓冲区。在处理消息时,将它们从主缓冲区中删除很重要,这样后端就有额外的空间可用于 new 数据。这涉及简单地将未处理的数据(如果有)复制到缓冲区的开头。如果数据传入的速度超过您的处理速度,则主缓冲区可能需要能够调整自身大小以容纳额外的数据。

我希望这不是压倒性的。从简单开始,随手添加。

【讨论】:

谢谢,这也是一个很好的答案!我的方法与您描述的几乎相同,只是我使用了一个字符串作为主缓冲区,这在接收空终止符时会导致问题。感谢您指出消息中标题的使用。我应该考虑在未来的应用程序中使用它。

以上是关于Winsock2 tcp/ip - 一些数据包被忽略可能是由于前一个数据包的空终止符的主要内容,如果未能解决你的问题,请参考以下文章

WSARecv 钩子:防止数据包被可执行文件接收

tcp/ip协议丢失

计算机网络——网络层02

计算机网络——网络层02

15.IP数据报格式详解

WebSocket客户端和WinSock2服务器,可以吗?