最近看redis源码,看到redis的网络模型,借机对socket编程和TCP/IP协议做了进一步的巩固和熟悉。其中对socket选项SO_REUSEADDR和SO_REUSEPORT写了一些demo,文章根据测试结果对SO_REUSEADDR选项和SO_REUSEPORT选项做一个总结,同时对博客的总结做一个纠正。
先来了解一下socket默认的行为:
·每个TCP连接都是由唯一的五元组<协议、源IP、源PORT、目的IP、目的PORT>进行标识,任何两条有效连接不可能具有完全相同的五元组。
·socket编程存在通配绑定(IPV4的0.0.0.0:PORT和IPV6的:::PORT)和特定的绑定(比如127.0.0.1:3602),无论我们先进行特殊的绑定然后进行通配绑定,还是先进行通配绑定然后进行特殊绑定,只要二者的IP+PORT存在冲突,就不可能绑定成功,通常出现Address already in use的错误。
·通常情况下,当连接主动关闭的一方进入到TIME_WAIT的状态时,该连接占用的IP+PORT被认为是有效的且不能立即被再次绑定并占用,需要等到2MSL时间之后才能够被复用。
当然,SO_RESUEADDR和SO_REUSEPORT的目的是改变IP+PORT的绑定和占用的默认行为,但是在不同的系统上却有不同的表现。文章对BSD(选取Mac)和Linux系统下的测试结果进行总结。以两个IP地址为例,分别是通配地址0.0.0.0、回旋地址127.0.0.1,使用3602的端口。
SO_REUSEADDR选项目的是在以下两个方面改变socket默认的行为:
·绑定的IP+PORT存在冲突时(部分重合,但不是完全相同),允许进行绑定。
·主动关闭连接的一方处于TIME_WAIT的时候,允许新的连接重新绑定与TIME_WAIT状态的连接有冲突的IP+PORT,并立即接收数据。
首先,对下面表格中使用的标识进行说明,“First”表示先启动进程,“After”表示后启动;“是”表示设置了对应的标识,“否”表示没有设置;“TIME_WAIT”表示连接处于TIME_WAIT状态;“S”表示绑定成功,“F”表示绑定失败。
下面,我们以表格的形式给出不同系统下SO_REUSEADDR对绑定有冲突的IP+PORT的影响。
BSD系统
首先来看BSD系统下SO_REUSEADDR的对于socket绑定行为的影响.
表1 BSD系统使用SO_REUSEADDR选项
127.0.0.1(After,否) | 0.0.0.0(After,否) | 127.0.0.1(After,是) | 0.0.0.0(After,是) | |
127.0.0.1(First,否) | F | F | F | S |
0.0.0.0(First,否) | F | F | S | F |
127.0.0.1(First,是) | F | F | F | S |
0.0.0.0(First,是) | F | F | S | F |
通过对比,我们得出在BSD系统中使用SO_REUSEADDR的以下几点结论:
1、SO_REUSEADDR只是解决了通配绑定和具体的IP+PORT绑定的冲突问题;无论是否使用SO_REUSEADDR,当一个IP+PORT组合已经被使用之后,另一个连接无法绑定完全相同(两个都是统配绑定或者两个都是具体绑定)的IP+PORT。
2、解决通配绑定和具体的IP+PORT绑定之间并没有顺序的限制;不使用SO_REUSEADDR的时候,无论先进行通配绑定还是先进行具体绑定,另一种绑定都不可能成功;第一个启动的程序可以不用设置SO_REUSEADDR选项,但是只要后面启动的程序(无论是使用通配绑定还是使用具体的IP+PORT绑定)设置了SO_REUSEADDR选项,如果存在IP+PORT的冲突(不是完全相同),那么绑定也能成功。
这里需要说明一下,如果我们启动两个进程分别绑定127.0.0.1、0.0.0.0和相同的端口,如果一个启动client进程连接到127.0.0.1的话,会连接到绑定127.0.0.1地址的进程;如果只有绑定0.0.0.0的进程存在,那么client连接到该进程。也就是说,client连接server的时候,首先使用具体的具体IP+PORT对应的进程,然后使用通配的IP+PORT对应的进程。
Linux系统
Linux系统下SO_REUSEADDR对socket绑定行为的影响,使用的Linux系统版本为Linux 2.6.32。
表2 Linux系统使用SO_REUSEADDR对绑定行为的影响
127.0.0.1(After,否) | 0.0.0.0(After,否) | 127.0.0.1(After,是) | 0.0.0.0(After,是) | |
127.0.0.1(First,否) | F | F | F | F |
0.0.0.0(First,否) | F | F | F | F |
127.0.0.1(First,是) | F | F | F | F |
0.0.0.0(First,是) | F | F | F | F |
在Linux下,SO_REUSEADDR对于绑定的地址冲突并没有什么影响,无论是否使用SO_REUSEADDR,只要IP+PORT之间存在冲突,后面的绑定就会失败,而博客中却说先进行特定绑定再进行通配绑定才能成功,否则不能成功(X)。
每一个TCP连接的socket描述符都有各自的发送缓冲区和接收缓冲区,且都可以分别使用setsockopt进行设置。连接建立之后,我们使用send、write等函数发送数据时,都是将数据写入到socket缓冲区中就立即返回,此时数据并没有立即被发送到网络上,至于什么时候将数据发送到网络上是由操作系统的调度、网络情况和接收端的通告窗口等各种因素决定。因此,从写入数据进socket的缓冲区直到数据实际被发送出去可能存在一定时间的延迟。TCP是一种可靠的连接,当主动关闭连接的一方close一个socket描述符的时候,该方将进入TIME_WAIT状态,以等待缓冲区的数据发送到对端、已经发送出的数据离开网络或者重传数据(重传断开连接时四次握手过程中的最后一个ACK),因此TIME_WAIT状态的连接使用的IP+PORT仍然被认为是一个有效的IP+PORT组合,相同机器上不能够在该IP+PORT组合上进行绑定。这个状态的持续时间可以通过改变内核参数进行设置,Linux下通过sudo sysctl -w net.ipv4.tcp.fin.timeout=value 进行设置。表3和表4总结了SO_REUSEADDR对于处于TIME_WAIT状态的连接占用的IP+PORT的影响。
在下面的例子中,我们先在server端关闭连接,然后再在client端关闭连接,那么server端将进入到TIME_WAIT状态,然后在server端启动测试的进程,观察IP+PORT有冲突时SO_REUSEADDR对启动新进程的影响。
BSD系统
表3 TIME_WAIT状态下SO_REUSEADDR对重新绑定的影响
127.0.0.1(After,否) | 0.0.0.0(After,否) | 127.0.0.1(After,是) | 0.0.0.0(After,是) | |
127.0.0.1(First,否,TIME_WAIT) | F | F | S | S |
0.0.0.0(First,否,TIME_WAIT) | F | F | S | S |
127.0.0.1(First,是,TIME_WAIT) | F | F | S | S |
0.0.0.0(First,是,TIME_WAIT) | F | F | S | S |
BSD系统下,无论原来的进程是否使用SO_REUSEADDR选项,如果当前启动进程绑定的IP+PORT与处于TIME_WAIT状态的连接占用的IP+PORT存在冲突,但是新启动的进程使用了SO_REUSEADDR选项,那么该进程就可以成功启动,并且能够立即接收数据;否则,新启动的进程无法绑定与处于TIME_WAIT状态的连接占用的IP+PORT有冲突的IP+PORT。此时通过netstat -an -p tcp | grep 3602查看,可以看到一个处于TIME_WAIT状态的连接和一个处于ESTABLISHED状态的连接,如图1所示。
图1 BSD系统SO_REUSEADDR对处于TIME_WAIT状态的连接占用的IP+PORT的复用影响
Linux系统
表4 TIME_WAIT状态下SO_REUSEADDR对重新绑定的影响
127.0.0.1(After,否) | 0.0.0.0(After,否) | 127.0.0.1(After,是) | 0.0.0.0(After,是) | |
127.0.0.1(First,否,TIME_WAIT) | F | F | F | F |
0.0.0.0(First,否,TIME_WAIT) | F | F | F | F |
127.0.0.1(First,是,TIME_WAIT) | F | F | S | S |
0.0.0.0(First,是,TIME_WAIT) | F | F | S | S |
Linux系统下,只有当处于TIME_WAIT状态的连接对应的进程在创建时设置了SO_REUSEADDR,并且当前要启动的绑定了有冲突的IP+PORT的进程也使用了SO_REUSEADDR选项,那么进程才能够绑定并启动成功。启动成功之后使用netstat -anpt | grep 3602查看可以看到如图2的连接状态。
图2 Linux系统中SO_REUSEADDR对处于TIME_WAIT状态的IP+PORT的复用
SO_REUSEADDR解决了通配IP+PORT和具体的IP+PORT绑定之间的冲突,但是完全相同的IP+PORT绑定(无论是具体的IP还是通配)仍然出现Address already in use的错误,使用SO_REUSEPORT选项可以避免此错误。
BSD系统
表5 BSD系统SO_REUSEPORT对绑定行为的影响
127.0.0.1(After,否) | 0.0.0.0(After,否) | 127.0.0.1(After,是) | 0.0.0.0(After,是) | |
127.0.0.1(First,否) | F | F | F | S |
0.0.0.0(First,否) | F | F | S | F |
127.0.0.1(Fitst,是) | F | F | S | S |
0.0.0.0(First,是) | F | F | S | S |
BSD系统下SO_REUSEPORT与SO_REUSEADDR对绑定行为的不同影响在于,如果先启动的进程和后启动的进程都设置了SO_REUSEPORT选项,那么即使是绑定完全相同的IP+PORT也能够启动成功;如果绑定了有冲突的IP+PORT的前面的实例没有设置SO_REUSEPORT选项,但是只要后面的实例设置了SO_REUSEPORT选项,只要IP+PORT不是完全相同,后面的进程能够成功绑定并启动。比如,如果我们启动了一个绑定127.0.0.1+3602的进程且该实例设置了SO_REUSEPORT选项,那么该进程可以再次被重复启动。下面,我们在所有的运行实例中都设置了SO_REUSEPORT选项,那么启动两次绑定127.0.0.1+3602的进程和一次绑定0.0.0.0+3602的进程之后,通过ps aux命令查看到的输出结果如图3所示,通过netstat查看连接的输出结果如图4所示,存在两个127.0.0.1+3602端口的侦听进程。
图3 SO_REUSEPORT对绑定有冲突的IP+PORT的进程的影响
图4 SO_REUSEPORT对绑定的有冲突的IP+PORT的连接的影响
如果所有的实例都设置了SO_REUSEPORT选项,绑定完全相同的IP+PORT的进程也能够重复启动。通过ps命令能够看到所有启动的进程,而且通过netstat命令能够看到所有启动的连接。
Linux系统
表6 Linux系统SO_REUSEPORT对绑定行为的影响
127.0.0.1((After,否) | 0.0.0.0((After,否) | 127.0.0.1((After,是) | 0.0.0.0((After,是) | |
127.0.0.1(First,否) | F | F | F | F |
0.0.0.0(First,否) | F | F | F | F |
127.0.0.1(Fitst,是) | F | F | S | S |
0.0.0.0(First,是) | F | F | S | S |
Linux系统下,如果两个实例绑定的IP+PORT存在冲突,那么只有当前、后启动的实例都设置了SO_REUSEPORT选项,进程才能够启动成功;存在冲突的实例中如果只有一个(无论前、后)设置了SO_REUSEPORT选项,那么后面启动的进程无法进行绑定,出现Address already in use的错误。所有实例都设置了SO_REUSEPORT选项的情况下,重复启动绑定127.0.0.1+3602的实例两次,绑定0.0.0.0+3602的实例一次,通过ps查看到的进程如图5所示;通过netstat查看到的连接如图5所示。
图5 Linux系统SO_REUSEPORT对启动进程的影响
图6 Linux系统SO_REUSEPORT对建立连接的影响
对比图5和图6,发现虽然,成功启动了三个进程,但是netstat却只看到了两个连接,且是后面启动的进程的连接覆盖了前面的连接。关于这一点还是希望明白其中原理的大神解答一下。
BSD系统
表7 BSD系统下TIME_WAIT状态下SO_REUSEPORT选项对启动进程的影响
127.0.0.1(Second,否) | 0.0.0.0(Second,否) | 127.0.0.1(Second,是) | 0.0.0.0(Second,是) | |
127.0.0.1(First,否,TIME_WAIT) | F | F | S | S |
0.0.0.0(First,否,TIME_WAIT) | F | F | S | S |
127.0.0.1(First,是,TIME_WAIT) | F | F | S | S |
0.0.0.0(First,是,TIME_WAIT) | F | F | S | S |
表8 Linux系统下TIME_WAIT状态下SO_REUSEPORT选项对启动进程的影响
127.0.0.1(Second,否) | 0.0.0.0(Second,否) | 127.0.0.1(Second,是) | 0.0.0.0(Second,是) | |
127.0.0.1(First,否,TIME_WAIT) | F | F | F | F |
0.0.0.0(First,否,TIME_WAIT) | F | F | F | F |
127.0.0.1(First,是,TIME_WAIT) | F | F | S | S |
0.0.0.0(First,是,TIME_WAIT) | F | F | S | S |
对比表7和表8,可以发现:
·BSD系统下无论使连接处于TIME_WAIT状态的原来的进程是否设置了SO_REUSEPORT,如果以后启动的进程绑定的IP+PORT与处于TIME_WAIT状态连接的IP+PORT存在冲突(完全相同或者是存在部分重合),那么只要以后启动的进程设置了SO_REUSEPORT,那么可以成功启动。
·Linux系统下,只有使处于TIME_WAIT连接状态的原来进程设置了SO_REUSEPORT选项,并且以后启动的进程绑定了有冲突的IP+PORT时也设置了SO_REUSEPORT选项,才能够成功绑定并启动以后的进程。
以上是个人关于SO_REUSEADDR和SO_REUSEPORT选项的理解,欢迎吐槽。
附测试使用代码:
server端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define WAIT_COUNT 5
#define SERV_PORT 3602
int main(int argc, char** argv)
{
int listen_fd, real_fd;
struct sockaddr_in listen_addr, client_addr;
socklen_t len = sizeof(struct sockaddr_in);
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if(listen_fd == -1)
{
fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
return -1;
}
int result = 1;
socklen_t socklen = sizeof(result);
/* 根据需要对是否设置该选项、以及替换成SO_REUSEPORT选项进行设置 */
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &result, socklen);
char *ip = "0.0.0.0";
bzero(&listen_addr,sizeof(listen_addr));
inet_pton(AF_INET, ip, &listen_addr.sin_addr);
listen_addr.sin_family = AF_INET;
listen_addr.sin_port = htons(SERV_PORT);
if(bind(listen_fd,(struct sockaddr *)&listen_addr, len) < 0)
{
fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
fprintf(stderr, "bind error!\n");
exit(1);
}
listen(listen_fd, WAIT_COUNT);
real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if(real_fd == -1)
{
fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
return -1;
}
while(1)
{
char pcContent[50];
read(real_fd,pcContent,50);
fprintf(stdout, "Read finish!\n");
}
close(real_fd);
close(listen_fd);
return 0;
}
client端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int setSendBuf(int socketFd, int size)
{
int valSize = sizeof(size);
if(setsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, &size, (socklen_t)valSize) < 0)
{
fprintf(stderr, "Set send buf error!\n");
return -1;
}
return 0;
}
int getSendBuf(int fd)
{
int bufSize = 0;
int size = sizeof(bufSize);
if(getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &bufSize, (socklen_t*)&size) < 0)
{
fprintf(stderr, "Get send buf error!\n");
return -1;
}
fprintf(stdout, "Send buffer size = %d\n", bufSize);
return 0;
}
int main(int argc, char** argv)
{
char *serverIp = "127.0.0.1";
int serverPort = 3602;
int send_sk;
struct sockaddr_in s_addr;
socklen_t len = sizeof(s_addr);
send_sk = socket(AF_INET, SOCK_STREAM, 0);
if(send_sk == -1)
{
fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
return -1;
}
int bufSize = 8192;
if(setSendBuf(send_sk, bufSize) == -1 || getSendBuf(send_sk) == -1)
{
exit(1);
}
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
inet_pton(AF_INET,serverIp, &s_addr.sin_addr);
s_addr.sin_port = htons(serverPort);
if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
{
fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
return -1;
}
int bufSize = 8192;
if(setSendBuf(send_sk, bufSize) == -1 || getSendBuf(send_sk) == -1)
{
exit(1);
}
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
inet_pton(AF_INET,serverIp, &s_addr.sin_addr);
s_addr.sin_port = htons(serverPort);
if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
{
fprintf(stderr, "errno=%d,errmsg=%s\n", errno, strerror(errno));
return -1;
}
char pcContent[1024]={0};
for(int cnt = 0; cnt < 6; ++cnt)
{
write(send_sk,pcContent,1024);
}
fprintf(stdout ,"send finish!\n");
sleep(5);
close(send_sk);
}