1. 套接字缓冲区
对于tcp协议来说,客户端和服务端在建立tcp连接的时候,双方会通告自己的接收窗口大小(告诉对方自己能接收的数据大小),然后双方就会根据接收窗口来调整自己的发送窗口大小。
如果你已经忘的差不多了,请参考: 19-tcp连接建立 , 如果你完全看不明白请参考:tcp/ip协议学习目录,结合《tcp/ip详解卷一》把tcp协议系统的学一遍,正所谓万丈高楼平地起,想要建一座高楼大厦,就看你的地基打的扎不扎实。
话不多说,进入正题。
在网络编程中,无论是客户端还是服务端的套接字都有一个接收缓冲区和一个发送缓冲区,而接收缓冲区的可用空间大小限定了接收窗口的大小。但是在不同的操作系统中,套接字默认缓冲区的大小也是不同的,有时候我们希望在建立tcp连接的时候设置接收缓冲区或发送缓冲区的大小,以此来提高通信的效率,当然一般会根据实际的应用场景来调整,可通过SO_RCVBUF和SO_SNDBUF套接字选项来改变默认的接收缓冲区和发送缓冲区的大小。
在设置套接字接收缓冲区大小需要注意的是:因为客户端的接收窗口是在建立tcp连接时确定的,所以必须在客户端调用connect函数之前设置SO_RCVBUF套接字选项。同理,服务端的接收窗口也是在建立tcp连接时确定的,所以服务端也必须在调用listen函数之前设置SO_RCVBUF套接字选项。然后服务端accept函数返回的新的套接字会从监听的套接字继承接收缓冲区大小。
关于设置和获取套接字选项的两个函数setsockopt 和 getsockopt,这两个函数的更多介绍和详细用法请参考:51-套接字选项(概述)
2. 设置接收缓冲区实验
客户端程序:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERV_IP "192.168.0.107"
#define SERV_PORT 10001
int main(void) {
int sfd, len;
struct sockaddr_in serv_addr;
char buf[BUFSIZ];
sfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
serv_addr.sin_port = htons(SERV_PORT);
//设置接收缓冲区为4k左右
int RecvBuf=4*1024;
setsockopt(sfd , SOL_SOCKET , SO_RCVBUF , (char *)&RecvBuf , sizeof(int));
connect(sfd, (struct sockaddr *)&serv_addr , sizeof(serv_addr));
while (1){
sleep(10);
}
close(sfd);
return 0;
}
服务端程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#define SERV_PORT 10001
#define SERV_IP "192.168.0.107"
int main(void) {
int sfd, cfd;
int len, i;
//BUFSIZ是系统内嵌的一个宏,用来指定buf大小
char buf[BUFSIZ], clie_IP[BUFSIZ];
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
sfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_pton(AF_INET , SERV_IP , &serv_addr.sin_addr.s_addr);
serv_addr.sin_port = htons(SERV_PORT);
bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
//设置接收缓冲区为4k
int RecvBuf=4*1024;
setsockopt(sfd , SOL_SOCKET , SO_RCVBUF , (char *)&RecvBuf , sizeof(int));
listen(sfd, 64);
printf("wait for client connect ...\n");
clie_addr_len = sizeof(clie_addr);
//等待客户端发起连接
//新创建的cfd会从sfd继承接收缓冲区大小
cfd = accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
//打印客户端的ip地址和端口号
printf("client IP:%s\tport:%d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),
ntohs(clie_addr.sin_port));
while(1){
sleep(10);
}
//关闭连接
close(sfd);
close(cfd);
return 0;
}
先启动server端,然后再启动client端(104为客户端,107为服务端),通过wireshark抓取到的数据包我们发现,客户端和服务端各自通告的窗口大小实际上是在5840字节,如图1所示:

这里有一个疑问,为什么不是我们设置的4096大小呢?这是因为TCP套接字缓冲区大小至少为MMS值的4倍,从上图我们可以看到MSS的值1460,那么MSS的4倍恰好就是5840字节。
3. 接收窗口和快速恢复算法
这里还有一个问题,为什么TCP套接字缓冲区大小至少为MMS值的4倍?
这就要说到tcp快速恢复算法的工作原理了,首先tcp快速恢复算法需要使用三个重复ACK来检测某一数据分组是否丢失,当发送端发送的某个数据分组在网络中丢失了,那么接收端对接下来新接收到的每一个数据分组都发送一个重复确认,如果接收端的接收窗口大小不足以存放4个数据分组,就不可能连续发送三个重复的确认,有同学可能会说,既然要发送三个重复的确认,那么接收窗口大小只要能存放3个数据分组就行了啊,但这么做的话就不能再接收丢失的数据分组了,因为此时接收窗口大小已经为0了,那么发送方会暂停发送数据了(直到对方通告窗口大于0为止),所以接收窗口大小最少得存放4个分组,这样才能启动快速恢复算法(关于tcp快速恢复算法的详细介绍请参考: 34-tcp拥塞控制——快重传和快恢复)。
到这里,相信你已经知道怎么设置发送缓冲区大小了。