實際上,默認的情況下,如果一個網絡應用程序的一個套接字 綁定了一個端口( 占用了 8000 ),這時候,別的套接字就無法使用這個端口( 8000 ), 驗證例子如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int sockfd_one;
int err_log;
sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //創建UDP套接字one
if(sockfd_one < 0)
{
perror("sockfd_one");
exit(-1);
}
// 設置本地網絡信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000); // 端口為8000
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 綁定,端口為8000
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_one");
close(sockfd_one);
exit(-1);
}
int sockfd_two;
sockfd_two = socket(AF_INET, SOCK_DGRAM, 0); //創建UDP套接字two
if(sockfd_two < 0)
{
perror("sockfd_two");
exit(-1);
}
// 新套接字sockfd_two,繼續綁定8000端口,綁定失敗
// 因為8000端口已被占用,默認情況下,端口沒有釋放,無法綁定
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_two");
close(sockfd_two);
exit(-1);
}
close(sockfd_one);
close(sockfd_two);
return 0;
}
程序編譯運行后結果如下:
那如何讓sockfd_one, sockfd_two兩個套接字都能成功綁定8000端口呢?這時候就需要要到端口復用了。端口復用允許在一個應用程序可以把 n 個套接字綁在一個端口上而不出錯。
設置socket的SO_REUSEADDR選項,即可實現端口復用:
int opt = 1;
// sockfd為需要端口復用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));
SO_REUSEADDR可以用在以下四種情況下。 (摘自《Unix網絡編程》卷一,即UNPv1)
1、當有一個有相同本地地址和端口的socket1處於TIME_WAIT狀態時,而你啟動的程序的socket2要占用該地址和端口,你的程序就要用到該選項。
2、SO_REUSEADDR允許同一port上啟動同一服務器的多個實例(多個進程)。但每個實例綁定的IP地址是不能相同的。在有多塊網卡或用IP Alias技術的機器可以測試這種情況。
3、SO_REUSEADDR允許單個進程綁定相同的端口到多個socket上,但每個socket綁定的ip地址不同。這和2很相似,區別請看UNPv1。
4、SO_REUSEADDR允許完全相同的地址和端口的重復綁定。但這只用於UDP的多播,不用於TCP。
需要注意的是,設置端口復用函數要在綁定之前調用,而且只要綁定到同一個端口的所有套接字都得設置復用:
// sockfd_one, sockfd_two都要設置端口復用
// 在sockfd_one綁定bind之前,設置其端口復用
int opt = 1;
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, (const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
// 在sockfd_two綁定bind之前,設置其端口復用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,(const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
2020/2/27更新
SO_REUSEPORT解決了什么問題
linux man文檔中一段文字描述其作用:
The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.
SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務器程序的性能,解決的問題:
- 允許多個套接字 bind()/listen() 同一個TCP/UDP端口
- 每一個線程擁有自己的服務器套接字
- 在服務器套接字上沒有了鎖的競爭
- 內核層面實現負載均衡
- 安全層面,監聽同一個端口的套接字只能位於同一個用戶下面
其核心的實現主要有三點:
- 擴展 socket option,增加 SO_REUSEPORT 選項,用來設置 reuseport。
- 修改 bind 系統調用實現,以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實現,查找 listener 的時候,能夠支持在監聽相同 IP 和端口的多個 sock 之間均衡選擇。
代碼分析,可以參考引用資料 [多個進程綁定相同端口的實現分析[Google Patch]]。
CPU之間平衡處理,水平擴展
以前通過fork
形式創建多個子進程,現在有了SO_REUSEPORT,可以不用通過fork
的形式,讓多進程監聽同一個端口,各個進程中accept socket fd
不一樣,有新連接建立時,內核只會喚醒一個進程來accept
,並且保證喚醒的均衡性。
模型簡單,維護方便了,進程的管理和應用邏輯解耦,進程的管理水平擴展權限下放給程序員/管理員,可以根據實際進行控制進程啟動/關閉,增加了靈活性。
這帶來了一個較為微觀的水平擴展思路,線程多少是否合適,狀態是否存在共享,降低單個進程的資源依賴,針對無狀態的服務器架構最為適合了。
新特性測試或多個版本共存
可以很方便的測試新特性,同一個程序,不同版本同時運行中,根據運行結果決定新老版本更迭與否。
針對對客戶端而言,表面上感受不到其變動,因為這些工作完全在服務器端進行。
服務器無縫重啟/切換
想法是,我們迭代了一版本,需要部署到線上,為之啟動一個新的進程后,稍后關閉舊版本進程程序,服務一直在運行中不間斷,需要平衡過度。這就像Erlang語言層面所提供的熱更新一樣。
想法不錯,但是實際操作起來,就不是那么平滑了,還好有一個hubtime開源工具,原理為SIGHUP信號處理器+SO_REUSEPORT+LD_RELOAD
,可以幫助我們輕松做到,有需要的同學可以檢出試用一下。