Socket发送接收缓冲区实验疑问:write未阻塞及读取字节数异常
我来帮你拆解这两个实验中遇到的问题,都是TCP套接字缓冲区和字节流特性的常见误区:
问题1:发送缓冲区与接收缓冲区总大小小于100000,为何客户端的write函数未发生阻塞?
这个问题的核心是你对write()行为和套接字缓冲区实际工作机制的理解有几个关键偏差:
write()的返回逻辑不是“等对方接收”
TCP套接字的write()操作只负责把应用层的数据拷贝到内核的发送缓冲区(send buffer),只要拷贝完成就会返回,完全不会等待数据被服务端接收或处理。哪怕服务端在sleep(10),只要客户端的发送缓冲区还有空间,write()就会成功返回,不会阻塞。你设置的SO_SNDBUF不等于实际可用的缓冲区大小
在Linux等主流系统中,通过setsockopt()设置SO_SNDBUF的数值后,内核实际分配的缓冲区大小通常是这个值的两倍。这是因为内核会为每个套接字维护“高水位”和“低水位”标记,SO_SNDBUF设置的是高水位,实际可用的缓冲区空间是该值的两倍。比如你设置的是16384,实际内核给的发送缓冲区可能是32768字节。发送缓冲区的空间是动态释放的
当服务端的接收缓冲区还没满时,客户端的TCP会持续把发送缓冲区中的数据发往服务端,这会不断释放发送缓冲区的空间。只有当服务端的接收缓冲区被填满(此时服务端TCP会告知客户端“接收窗口为0”),客户端的TCP才会停止发送,发送缓冲区才会逐渐被填满。只有当发送缓冲区完全没有空间容纳你要写入的数据时,write()才会阻塞。可能你的缓冲区设置时机错误
如果是在connect()之后才调用setsockopt()设置SO_SNDBUF,这个设置是无效的——套接字缓冲区大小必须在连接建立之前设置才会生效。如果你的代码里没有在connect()前设置,那发送缓冲区用的是系统默认值,可能远大于16384。
另外,建议你检查客户端printf("Write %d bytes.\n", wn);的输出,看看wn是不是真的等于100000。如果wn小于100000,说明write()只写入了部分数据,剩下的会在TCP后台自动发送,但此时write()不会阻塞,因为它已经完成了当前能做的拷贝操作。
问题2:服务端为何读取到8192+6808=15000字节,而非持续读取8192字节?
这是TCP字节流协议的特性和read()函数的行为共同导致的,具体原因如下:
TCP是无消息边界的字节流
TCP不会为应用层的数据划分“数据包”,它只是把数据当作连续的字节流传输。服务端的read()函数不会等待凑够你指定的缓冲区大小(8192字节)才返回,而是会读取当前内核接收缓冲区中所有可用的字节,最多不超过你指定的缓冲区大小。服务端sleep期间,数据已经全部到达接收缓冲区
在服务端sleep(10)的这段时间里,客户端已经完成了数据发送,并且可能已经关闭了套接字(进程退出时内核会自动关闭)。当服务端醒来调用read()时:- 第一次
read()读取了接收缓冲区中可用的8192字节(这是你设置的buf大小); - 第二次
read()读取了接收缓冲区中剩下的6808字节(此时剩余字节数小于8192); - 第三次
read()会返回0,因为客户端已经关闭了连接,没有更多数据可读,循环结束。
- 第一次
总字节数为15000的可能原因
这大概率是因为服务端的接收缓冲区实际可用空间加上客户端发送缓冲区的限制,导致客户端只能发送15000字节就被TCP流量控制机制暂停(服务端接收缓冲区满了)。当服务端sleep时,客户端无法继续发送剩余数据,直到服务端醒来读取——但如果客户端已经关闭了套接字,TCP会先把发送缓冲区中已有的数据发送完毕,再发送FIN包,所以你看到的15000就是客户端实际成功发送的总字节数。
内容的提问来源于stack exchange,提问作者Greetlist

