使用read/write通信时,如何让read单次读取单个write payload?
兄弟,你遇到的是TCP协议里经典的粘包问题——本质原因是TCP是面向流的协议,它完全不关心你在发送端调用了几次write(),只会把所有数据当成一段连续的字节流来传输。哪怕你分两次发8字节,到了接收端,操作系统的TCP栈可能会因为Nagle算法优化、网络延迟或者接收缓冲区的特性,把这两个数据包合并成一个16字节的整块交给应用层,所以你的read()一调用就直接把16字节全读走了。
要实现你期望的“每次read()只读取一次write()的payload”,而且不想缩小缓冲区,核心思路是在应用层自己定义消息边界,让接收端能准确区分开两次发送的数据。这里给你几个靠谱的方案:
给每个消息加固定长度的头部:
比如每次发送数据前,先写一个固定大小的头部(比如2字节或者4字节),用来记录后续payload的长度。举个例子:
服务器端先调用write(fd, &len, 2)(这里len是8,转成网络字节序),再调用write(fd, payload, 8);
客户端先调用read(fd, &len, 2)解析出要读的字节数,然后循环调用read()直到凑够8字节(哪怕你的缓冲区是1024字节,也只取前8字节或者循环读取到满足长度)。这样就能精准拆分两次发送的数据,完全不影响你用大缓冲区。使用特殊分隔符标记消息结束:
如果你的payload里不会出现某个特殊字符(比如换行符\n、或者自定义的一个特殊字节序列),可以在每次write()的payload末尾加上这个分隔符。接收端调用read()后,在缓冲区里查找分隔符,把分隔符之前的内容当成一个完整的消息,剩下的留到下一次处理。这个方法适合文本类数据,同样不需要缩小缓冲区。禁用Nagle算法(仅减少粘包概率,不保证彻底解决):
你可以在发送端的socket上设置TCP_NODELAY选项,禁用Nagle算法(这个算法会延迟发送小数据包,凑够一定大小再发,是粘包的常见原因之一)。调用代码大概是:int optval = 1; setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));但要注意,这个方法只能降低粘包的可能性,没法100%保证每次
read()都能拿到单独的write()payload——因为接收端的TCP缓冲区还是可能把先后到达的两个数据包合并。所以这个方法适合对可靠性要求不那么高的场景,或者作为辅助优化手段。
总结一下:最可靠的方案是前两种(加头部/分隔符),因为它们从应用层协议层面解决了消息边界的问题,完全符合你的需求,也不需要改动缓冲区大小。TCP本身不提供消息边界的能力,所以必须靠应用层自己来定义~
内容的提问来源于stack exchange,提问作者Thomas Banderas

