在具體分析之前,我們先看下socket(7)的man文檔對這個參數是怎么介紹的:
~~~
SO_REUSEADDR
Indicates that the rules used in validating addresses supplied
in a bind(2) call should allow reuse of local addresses.For
AF_INET sockets this means that a socket may bind, except when
there is an active listening socket bound to the address.
When the listening socket is bound to INADDR_ANY with a spe‐
cific port then it is not possible to bind to this port for
any local address.Argument is an integer boolean flag.
~~~
### 從這段文檔中我們可以知道三個事:
1. 使用這個參數后,bind操作是可以重復使用local address的,注意,這里說的是local address,即ip加端口組成的本地地址,也就是說,兩個本地地址,如果有任意ip或端口部分不一樣,它們本身就是可以共存的,不需要使用這個參數。
2. 當local address被一個處于listen狀態的socket使用時,加上該參數也不能重用這個地址。
3. 當處于listen狀態的socket監聽的本地地址的ip部分是INADDR\_ANY,即表示監聽本地的所有ip,即使使用這個參數,也不能再bind包含這個端口的任意本地地址,這個和 2 中描述的其實是一樣的。
好,接下來我們看幾個例子。
上文 1 中說,只要本地地址不一樣(ip或端口不一樣),即使沒有這個參數,兩個地址也是可以同時使用的,我們來看下是不是這樣。
下面是客戶端的測試代碼:
~~~
#include <arpa/inet.h>
#include <assert.h>
#include <netinet/in.h>
#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int tcp_connect(char *ip, int port) {
int sfd, err;
struct sockaddr_in addr;
sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(sfd != -1);
// 先bind本地地址
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
assert(!err);
// 再連接目標服務器
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(7777);
err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr));
assert(!err);
return sfd;
}
int main(int argc, char *argv[]) {
// bind本地地址:127.0.0.1:8888
tcp_connect("127.0.0.1", 8888);
// bind本地地址:192.168.3.187:8888
tcp_connect("192.168.3.187", 8888);
printf("兩個連接同時建立成功\n");
sleep(100);
return 0;
}
~~~
該段代碼中,會先綁定本地地址,再連接目標服務器,由上可見,兩次連接bind的本地地址中,ip部分是不同的,所以這兩個bind操作應該是成功的。
我們用以下ncat命令模擬服務端:
~~~
$?ncat?-lk4?7777
~~~
用ss命令查看有關7777端口的所有socket狀態:
~~~
$?ss?-antp?|?grep?7777
LISTEN 0 10 0.0.0.0:7777 0.0.0.0:\* users:(("ncat",pid=19208,fd=3))
~~~
由上可見,此時只有ncat服務端在監聽7777端口,沒有任何其他連接。
我們執行上面的程序,然后再次查看7777端口所有socket狀態:
~~~
$?ss?-antp?|?grep?7777
LISTEN 0 10 0.0.0.0:7777 0.0.0.0 : *users : (("ncat", pid = 19208, fd = 3))
ESTAB 0 0 127.0.0.1 : 7777 192.168.3.187 : 8888 users : (("ncat", pid = 19208, fd = 5))
ESTAB 0 0 127.0.0.1 : 7777 127.0.0.1 : 8888 users : (("ncat", pid = 19208, fd = 4))
ESTAB 0 0 192.168.3.187 : 8888 127.0.0.1 : 7777 users : (("a.out", pid = 19340, fd = 4))
ESTAB 0 0 127.0.0.1 : 8888 127.0.0.1 : 7777 users : (("a.out", pid = 19340, fd = 3))
~~~
由上可以看到,這兩個連接的確是建立成功了。
上面命令輸出中,有4個ESTAB狀態的連接,這是正常的,因為這分別是從服務端角度和客戶端的角度得到的輸出。
前三行是從服務器角度來看的,后兩行是從客戶端角度來看的,這個從后面的進程名也可以看出。
對客戶端來說,在connect之前可以bind不同本地地址,然后連同一目標,對服務端來說也是可以的,在listen之前,完全可以bind不同的本地地址,不需要SO\_REUSEADDR參數也可以成功,由于程序代碼差不多,這里我們就不演示了。
我們下面再來看下connect之前,bind相同地址的情況,下面是測試代碼:
~~~
#include <arpa/inet.h>
#include <assert.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int tcp_connect(char *ip, int port) {
int sfd, err;
char buf[1024];
struct sockaddr_in addr;
sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(sfd != -1);
// 先bind本地地址
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8888);
err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
sprintf(buf, "bind(%s:%d)", ip, port);
perror(buf);
exit(-1);
}
// 再連接目標服務器
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr));
assert(!err);
return sfd;
}
int main(int argc, char *argv[]) {
// 連接的目標地址:127.0.0.1:7777
tcp_connect("127.0.0.1", 7777);
// 連接的目標地址:127.0.0.1:7778
tcp_connect("127.0.0.1", 7778);
printf("兩個連接同時建立成功\n");
sleep(100);
return 0;
}
~~~
該程序會在connect之前,bind本地地址到127.0.0.1:8888,然后再連接目標地址,兩次目標地址分別是127.0.0.1:7777和127.0.0.1:7778。
還是用ncat模擬服務端,只是這次要開兩個。
服務端7777:
~~~
$ ncat -lk4 7777
~~~
服務端7778:
~~~
$?ncat?-lk4?7778
~~~
運行客戶端代碼:
~~~
$?gcc?client.c?&&?./a.out
bind(127.0.0.1:7778): Address already in use
~~~
由上可見,第二次連接是失敗了的,因為127.0.0.1:8888本地地址已經被第一次connect使用過了。
此時,加上SO\_REUSEADDR參數應該是可以解決這個問題的。
~~~
#include <arpa/inet.h>
#include <assert.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int tcp_connect(char *ip, int port) {
int sfd, opt, err;
char buf[1024];
struct sockaddr_in addr;
sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(sfd != -1);
// 先設置SO_REUSEADDR
opt = 1;
err = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
assert(!err);
// 再bind本地地址
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8888);
err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
sprintf(buf, "bind(%s:%d)", ip, port);
perror(buf);
exit(-1);
}
// 然后連接目標服務器
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr));
assert(!err);
return sfd;
}
int main(int argc, char *argv[]) {
// 連接的目標地址:127.0.0.1:7777
tcp_connect("127.0.0.1", 7777);
// 連接的目標地址:127.0.0.1:7778
tcp_connect("127.0.0.1", 7778);
printf("兩個連接同時建立成功\n");
sleep(100);
return 0;
}
~~~
再次編譯后執行:
~~~
$ gcc client.c && ./a.out
兩個連接同時建立成功
~~~
由上可以看到,這兩次連接都成功了,SO\_REUSEADDR允許我們重復bind相同的本地地址。
**細心的同學可能會發現,為什么兩次連接的目標地址是不同的呢?**
我們來把它改成相同的試下:
~~~
#include <arpa/inet.h>
#include <assert.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int tcp_connect(char *ip, int port) {
int sfd, opt, err;
char buf[1024];
struct sockaddr_in addr;
sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(sfd != -1);
// 先設置SO_REUSEADDR
opt = 1;
err = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
assert(!err);
// 再bind本地地址
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8888);
err = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
sprintf(buf, "bind(%s:%d)", ip, port);
perror(buf);
exit(-1);
}
// 然后連接目標服務器
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
err = connect(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
sprintf(buf, "connect(%s:%d)", ip, port);
perror(buf);
exit(-1);
}
return sfd;
}
int main(int argc, char *argv[]) {
// 連接的目標地址:127.0.0.1:7777
tcp_connect("127.0.0.1", 7777);
// 連接的目標地址:127.0.0.1:7777
tcp_connect("127.0.0.1", 7777);
printf("兩個連接同時建立成功\n");
sleep(100);
return 0;
}
~~~
此時,執行該程序,命令行會有如下輸出:
~~~
$?gcc?client.c?&&?./a.out
connect(127.0.0.1:7777): Cannot assign requested address
~~~
為什么呢?因為這兩次連接都是從127.0.0.1:8888 到 127.0.0.1:7777的,**這個在tcp層面是不允許的,即使加了SO\_REUSEADDR參數也不行。**
本地地址和目標地址組成的元組唯一確定一個tcp連接,上面程中的兩次連接本地地址和目標地址都一樣,已經違背了唯一的原則。
對應內核相應檢查代碼如下:
~~~
// net/ipv4/inet_hashtables.c
static int __inet_check_established(struct inet_timewait_death_row *death_row,
struct sock *sk, __u16 lport,
struct inet_timewait_sock **twp)
{
struct inet_hashinfo *hinfo = death_row->hashinfo;
struct inet_sock *inet = inet_sk(sk);
__be32 daddr = inet->inet_rcv_saddr;
__be32 saddr = inet->inet_daddr;
...
const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport);
unsigned int hash = inet_ehashfn(net, daddr, lport,
saddr, inet->inet_dport);
struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
...
struct sock *sk2;
...
sk_nulls_for_each(sk2, node, &head->chain) {
...
if (likely(INET_MATCH(sk2, net, acookie,
saddr, daddr, ports, dif, sdif))) {
...
goto not_unique;
}
}
...
not_unique:
...
return -EADDRNOTAVAIL;
}
~~~
如果本地地址和目標地址組成的元組之前已經存在了,則返回錯誤碼EADDRNOTAVAIL,這個錯誤碼對應的解釋為:
~~~
// include/uapi/asm-generic/errno.h
#define EADDRNOTAVAIL 99 /\* Cannot assign requested address \*/
~~~
正好和上面執行程序輸出的錯誤信息一樣。
我們再回到對SO\_REUSEADDR參數的討論。
上面代碼中,兩個connect使用相同的本地地址,只要加上SO\_REUSEADDR參數是可以的,那兩個listen行嗎?
**看代碼: **
~~~
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int tcp_listen(char *ip, int port) {
int lfd, opt, err;
char buf[1024];
struct sockaddr_in addr;
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(lfd != -1);
opt = 1;
err = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
assert(!err);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
err = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
sprintf(buf, "bind(%s:%d)", ip, port);
perror(buf);
exit(-1);
}
err = listen(lfd, 8);
assert(!err);
return lfd;
}
int main(int argc, char *argv[]) {
tcp_listen("127.0.0.1", 7777);
tcp_listen("127.0.0.1", 7777);
return 0;
}
~~~
該代碼執行之后輸出如下:
~~~
$ gcc server.c && ./a.out
bind(127.0.0.1:7777): Address already in use
~~~
由上可見,即使加上SO\_REUSEADDR參數,兩個listen也是不行的。
其實,這個在最開始的man文檔中已經說過了,只要有listen占了一個本地地址,其他任何操作都不能再使用這個地址了。
我們對應看下內核源碼:
~~~
// net/ipv4/inet_connection_sock.c
static int inet_csk_bind_conflict(const struct sock *sk,
const struct inet_bind_bucket *tb,
bool relax, bool reuseport_ok)
{
struct sock *sk2;
bool reuse = sk->sk_reuse;
...
sk_for_each_bound(sk2, &tb->owners) {
if (sk != sk2 && ...) {
if ((!reuse || !sk2->sk_reuse ||
sk2->sk_state == TCP_LISTEN) && ...) {
if (inet_rcv_saddr_equal(sk, sk2, true))
break;
}
...
}
}
return sk2 != NULL;
}
~~~
該方法就是用來判斷本地地址是否可以重復使用的代碼。
如果該方法最終sk2不為null,則最終會返回錯誤碼EADDRINUSE給用戶,即我們上面程序執行之后的錯誤輸出。
我們來看下sk2什么時候不為null。
在我們的新socket和sk2本地地址相同時,如果新socket沒有設置SO\_REUSEADDR參數,或者sk2沒設置SO\_REUSEADDR參數,或者sk2為listen狀態,sk2最終都會不為null,也就是說,新socket的本地地址在這些情況下都不可重復使用。
和man文檔中說的基本是一樣的。
那我們在平時寫服務器時,為什么要加上這個參數呢?我們都是先關閉服務器,再開的啊,以前那個listen的socket,以及所有當時正在連接的socket,應該都已經關閉了啊?應該不會存在相同的本地地址了啊?
### 為什么呢?
這要再說起tcp的TIME\_WAIT狀態。
我們知道,在tcp連接中,主動發起關閉請求的那一端會最終進入TIME\_WAIT狀態,被動關閉連接的那一端會直接進入CLOSE狀態,即socket和它占用的資源會直接銷毀。
*****
假設,在我們關閉服務器之前,先把客戶端都關閉掉,再關閉服務器,此時服務器的所有socket都直接進入CLOSE狀態了,它們占用的本地地址等也都立即可用,此時如果我們馬上開服務器,是不會出現 Address already in use 這個錯誤的。
但當我們在有客戶端連接的情況下,直接關閉服務器,也就是說,對所有現有的tcp連接,服務端都主動發起了關閉請求,此時,這些連接就會進入TIME\_WAIT狀態,一直占用服務器使用的本地地址,不讓后續操作使用。
這種情況下,你再開服務器,就會出現上面那個?Address already?in?use 錯誤,這也是我們寫服務器時經常會遇到的錯誤。
*****
**解決這個問題的方法就是設置SO\_REUSEADDR參數。**
由上面的inet\_csk\_bind\_conflict方法可以看到,如果設置了SO\_REUSEADDR參數,新socket和舊socket的reuse值都會為true,而舊socket此時處于TIME\_WAIT狀態,所以后續不會調用inet\_rcv\_saddr\_equal方法,判斷兩個地址是否相同。
這樣最終sk2也會為null,也就是說,內核允許新socket使用這個地址。
用代碼驗證下:
~~~
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int tcp_listen(char *ip, int port) {
int lfd, opt, err;
char buf[1024];
struct sockaddr_in addr;
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(lfd != -1);
opt = 1;
err = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
assert(!err);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
err = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
sprintf(buf, "bind(%s:%d)", ip, port);
perror(buf);
exit(-1);
}
err = listen(lfd, 8);
assert(!err);
return lfd;
}
int main(int argc, char *argv[]) {
int lfd, cfd;
lfd = tcp_listen("127.0.0.1", 7777);
printf("5秒鐘之后將關閉第一次listen的socket,請于此期間發起一次tcp連接\n");
sleep(5);
cfd = accept(lfd, NULL, NULL);
assert(cfd != -1);
close(cfd);
close(lfd);
tcp_listen("127.0.0.1", 7777);
printf("第二次listen操作成功\n");
return 0;
}
~~~
按照程序提示,對服務端發起tcp連接,最終服務端輸出如下:
~~~
$ gcc server.c && ./a.out
5秒鐘之后將關閉第一次listen的socket,請于此期間發起一次tcp連接
~~~
可見,有了SO\_REUSEADDR參數,即使我們先關閉的tcp連接,也是可以再次listen的。
有興趣的朋友可以把設置SO\_REUSEADDR參數的代碼去掉,然后再執行看下,理論上來說是會報錯的。
到此為止,所有有關SO\_REUSEADDR參數內容都講完了,希望對大家有所幫助。
完。
- 前言
- 服務器開發設計
- Reactor模式
- 一種心跳,兩種設計
- 聊聊 TCP 長連接和心跳那些事
- 學習TCP三次握手和四次揮手
- Linux基礎
- Linux的inode的理解
- 異步IO模型介紹
- 20個最常用的GCC編譯器參數
- epoll
- epoll精髓
- epoll原理詳解及epoll反應堆模型
- epoll的坑
- epoll的本質
- socket的SO_REUSEADDR參數全面分析
- 服務器網絡
- Protobuf
- Protobuf2 語法指南
- 一種自動反射消息類型的 Protobuf 網絡傳輸方案
- 微服務
- RPC框架
- 什么是RPC
- 如何科學的解釋RPC
- RPC 消息協議
- 實現一個極簡版的RPC
- 一個基于protobuf的極簡RPC
- 如何基于protobuf實現一個極簡版的RPC
- 開源RPC框架
- thrift
- grpc
- brpc
- Dubbo
- 服務注冊,發現,治理
- Redis
- Redis發布訂閱
- Redis分布式鎖
- 一致性哈希算法
- Redis常見問題
- Redis數據類型
- 緩存一致性
- LevelDB
- 高可用
- keepalived基本理解
- keepalived操做
- LVS 學習
- 性能優化
- Linux服務器程序性能優化方法
- SRS性能(CPU)、內存優化工具用法
- centos6的性能分析工具集合
- CentOS系統性能工具 sar 示例!
- Linux性能監控工具集sysstat
- gdb相關
- Linux 下如何產生core文件(core dump設置)