记一次echo server出现的问题
1. 我做了什么
最近在学习如何用select函数实现echo server。期间遇到了一个关于缓冲区的问题,在这里分享给大家。
在使用read/recv, write/send类函数进行数据传输时,需要程序员手动创建缓冲区。通常(各种书籍中),大家都会将缓冲区的大小设置为1024或其整数倍。但是我在编写echo server时就遇到了一些问题。以下是echo server的图示。缓冲区大小是BUFFLEN=1024。
客户端这里write的长度是strlen(buffer),read的长度是BUFFLEN-1,这块我是参考《TCP/IP网络编程》这本书的。因为随后还有一个buffer[BUFFLEN] = '\0' 的操作,所以read的长度是BUFFLEN-1(为了凑成一个字符串)。(谁能知道就是这里出了问题)。同时,服务端这里write和read的长度都是BUFFLEN。
2. 出现了什么问题
事情是这样的,当我开心参考着书上的代码,在电脑上敲了一遍,一顿编译之后,发现只有第一次能够echo成功,后续客户端的输入并不能被有效的echo回客户端,如下图所示。
客户端:
服务端:
我们可以看到服务端这块的读写还是正常的,但是客户端在第二次的read中,仅仅读了1个字节。我查阅了书中相关的内容,发现我的服务端代码和书上稍有不同。
//服务端代码 char buffer[BUFFLEN]; memset(buffer, 0, BUFFLEN); int strlen = read(i, buffer, BUFFLEN); printf("server read: %s, [%d]bytes\n", buffer, strlen); if (strlen == 0) { FD_CLR(i, &rdset); close(i); } else { //不同的地方: //int wlen = write(i, buffer, strlen); 原书的代码 int wlen = write(i, buffer, BUFFLEN); buffer[strlen] = '\0'; if (wlen < 0) { err_exit("write error"); }
printf("server write: %s, [%d]bytes\n",
buffer, wlen);
我很奇怪为什么仅仅write的长度不同就会导致这么严重的问题。通过查阅write, read的手册,还有抓包,我也没有找到问题的根源在哪里,唯一可以确定的是服务端应该是没有问题的,因为通过wireshark,我发现正常情况下的服务端和异常情况下的服务端的tcp报文的数量和顺序都是相同的。尝试了一些方案,比如 每次I/O前memset清空用户态缓冲区等等都没什么用。这个问题持续了有一天,周一早上我想着会不会是write和read操作的缓冲区长度不一致所导致的。
[图挂了]
服务端write
[图挂了]
客户端read
我尝试修改客户端的read读取的字节数为BUFFLEN竟发现这个问题被解决了。果然就是我预想的那样!!!
修改后的read
之所以会出现这个问题,是因为内核协议栈(或者是page cache?这个还需要进一步探讨)在客户和服务两端都存在相应的内核缓冲区,服务端会write到客户端的buffer中,而客户端的read会清空相应大小的缓冲区内容。试想如果服务端写入了1024字节,但是客户端只读取1023个字节,则此次操作缓存区内会剩余一个字节内容。下一次客户端的read就会将这一字节读到用户态缓冲中。
在客户端的输出中会出现"recv: 1 byte"这样的内容就是这个原因。
那么如何避免这种情况的出现呢?很明显,read/write, send/recv这些都是配对的操作。程序员要保证的就是write写入到内核缓冲区的内容必须能够被匹配的read读取完,不要在缓冲区中遗留陈旧的数据。
3. 遗留问题
肯定不会是我申请的用户缓冲区的问题,到底是那块的缓冲区出现问题,我也不太清楚。
4. 参考
[1]《TCP/IP网络编程》