最近看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不能被再次绑定并占用。
当然,SO_RESUEADDR和SO_REUSEPORT能够改变以上三种默认的行为,但是在不同的系统上却有不同的表现。文章对Linux下和BSD(选取Mac)系统下的测试结果进行总结。以两个IP地址为例,分别是通配地址0.0.0.0、回旋地址127.0.0.1,使用3602的端口。
SO_REUSEADDR选项
SO_REUSEADDR选项目的是在以下两个方面改变socket默认的行为:
·绑定的IP+PORT存在冲突时,允许进行绑定。
·主动关闭连接的一方处于TIME_WAIT的时候,允许新的连接重新绑定与TIME_WAIT状态的连接有冲突的IP+PORT,并立即发送数据。
首先,对下面表格中使用的标识进行说明,“First”表示先启动进程,“After”表示后启动;“是”表示设置了对应的标识,“否”表示没有设置;“TIME_WAIT”表示连接处于TIME_WAIT状态;“S”表示成功,“F”表示失败。
SO_REUSEADDR对绑定的影响
下面,我们以表格的形式给出不同系统不使用SO_REUSEADDR和使用SO_REUSEADDR时,是否允许绑定的情况对比。
BSD系统
首先来看BSD系统下SO_REUSEADDR的对于socket绑定行为的影响.
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连接到该进程。
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)。
TIME_WAIT下SO_REUSEPORT对有冲突的IP+PORT的影响
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存在冲突且使用了SO_REUSEADD选项,那么该进程就可以成功启动,并且能够立即接受数据;否则,不能绑定成功。此时通过netstat -an -p tcp | grep 3602查看,可以看到一个处于TIME_WAIT状态的连接和一个处于ESTABLISHED状态的连接,如图1所示。
图1 BSD系统中SO_REUSEADDR对处于TIME_WAIT状态的IP+PORT的复用
表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的连接状态。
SO_REUSEPORT选项
SO_REUSEADDR解决了通配IP+PORT和具体的IP+PORT绑定之间的冲突,但是完全相同的IP+PORT绑定(无论是具体的IP还是通配)仍然出现Address already in use的错误,使用SO_REUSEPORT选项可以避免此错误。
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_REUSEADDR选项,那么即使是绑定完全相同的IP+PORT也能够启动成功;如果绑定了冲突的IP+PORT的前面的实例没有设置SO_REUSEPORT选项,但是只要后面的实例设置了SO_REUSEPORT选项,只要IP+PORT不是完全相同,后面的进程能够成功绑定并启动。比如,如果我们启动了一个绑定127.0.0.1+3602的进程,那么该进程可以再次被重复启动。下面,我们在所有的运行实例中都设置了SO_REUSEPORT选项,那么启动两次绑定127.0.0.1+3602的进程和一次绑定0.0.0.0+3602的进程之后,通过ps aux命令查看到的输出结果如图3所示,通过netstat查看连接的输出结果如图4所示。
图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却只看到了两个连接,且是后面启动的进程的连接覆盖了前面的连接。关于这一点还是希望明白其中原理的大神解答一下。
TIME_WAIT下SO_REUSEPORT对有冲突的IP+PORT的影响
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 |
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选项的理解,欢迎吐槽。