最近看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選項
SO_REUSEADDR選項目的是在以下兩個方面改變socket默認的行爲:
·綁定的IP+PORT存在衝突時(部分重合,但不是完全相同),允許進行綁定。
·主動關閉連接的一方處於TIME_WAIT的時候,允許新的連接重新綁定與TIME_WAIT狀態的連接有衝突的IP+PORT,並立即接收數據。
首先,對下面表格中使用的標識進行說明,“First”表示先啓動進程,“After”表示後啓動;“是”表示設置了對應的標識,“否”表示沒有設置;“TIME_WAIT”表示連接處於TIME_WAIT狀態;“S”表示綁定成功,“F”表示綁定失敗。
SO_REUSEADDR對綁定的影響
下面,我們以表格的形式給出不同系統下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)。
TIME_WAIT下SO_REUSEPORT對有衝突的IP+PORT的影響
每一個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_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_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卻只看到了兩個連接,且是後面啓動的進程的連接覆蓋了前面的連接。關於這一點還是希望明白其中原理的大神解答一下。
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 |
表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<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<netinet/tcp.h>
#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<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<netinet/tcp.h>
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);
}