1.Socket和TCP/IP的關系
"All problems in computer science can be solved by another level of indirection."
為滿足應用層需求,系統對TCP/IP層進行細節屏蔽和抽象,Socket層就相當於TCP/IP和應用層之間的中間層。
常用的socket/bind/accept/connect就是抽象出來的接口,使用它們可以快速進行網絡程序開發,可見Socket中間層的重要性。
Socket選項就是為滿足用戶的定制化需求而生的。我們經常遇到的情況包括地址復用、端口復用、讀寫超時時間、讀寫緩沖區大小等。
在Linux的TCP/IP協議棧中包括很多Socket選項,它們會出現在TCP層、IP層、Socket層等,為此在讀取和設置socket選項時需要指定level。
如圖可以看到Socket層作為中間層以及各層支持的部分Socket選項:
注:可通過man 7 tcp/man 7 ip查看tcp/ip各層Socket選項詳細定義和添加內核版本等信息。
2.操作Socket選項的API
讀取和設置Socket選項的API包括:
getsockopt、setsockopt、fcntl、ioctl等;
其中fcntl和ioctl用來設置socket的阻塞和非阻塞狀態。
通過man獲得的函數定義:
//ioctl函數定義
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
//fcntl函數定義
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
//get/setsockopt函數定義
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
3.get/setsockopt使用說明
使用時需要按照函數要求的形參格式進行傳遞,顯式指明其所在的level以及選項名稱optname、optval類型和長度optlen。
level參數說明
從sys/socket.h的源碼中可以看到對於level的說明如下:
/* Setsockoptions(2) level. Thanks to BSD these must match IPPROTO_xxx */
#define SOL_IP 0
#define SOL_IPX 256
#define SOL_AX25 257
#define SOL_ATALK 258
#define SOL_NETROM 259
#define SOL_TCP 6
#define SOL_UDP 17
#define SOL_SOCKET 0xffff
optval和optlen參數說明
optval和optlen均為指針類型,這兩個參數與當前操作的option有直接關系,可以看到optval使用void*類型,optlen使用socklen_t*類型。
socklen_t類型說明:socklen_t和int應該具有相同的長度,否則會破壞 BSD套接字層的填充,POSIX開始時候用的是size_t。
Linus Torvalds 向他們解釋使用size_t是完全錯誤的,因為在64位結構中 size_t和int的長度是不一樣的,而這個參數的長度必須和int一致,最終POSIX的那幫家伙找到了解決的辦法,創造了 一個新的類型socklen_t。
Linux Torvalds說這是由於他們發現了自己的錯誤但又不好意思承認,所以另外創造了一個新的數據類型。
指針使用:optval和optlen兩個指針類型是缺一不可的,optval為void*類型如果沒有長度說明,系統函數在調用時就無法獲取邊界,optlen為底層調用指明內存起始地址對應的偏移量,這是C中常用的指針操作模式。
Socket選項多是int和bool類型 但是也有一些復合類型比如linger,因此在讀寫選項是對於optval和optlen的編寫要根據實際而定。
4. SO_REUSEADDR選項
典型場景:在《Unix網絡編程》卷一中指出了SO_REUSEADDR的重要使用場景:當有一個有相同本地地址和端口的socket1處於TIME_WAIT狀態時,而你啟動的程序的socket2要占用該地址和端口,你的程序就要用到該選項。
TIME_WAIT :如何優雅關閉Socket是個值得思考的問題, TIME_WAIT狀態是TCP協議為了保證全雙工連接可靠性設置的,感興趣可以查閱TIME_WAIT的作用,並不要一味的談TIME_WAIT色變,這里就不展開了。
設置方法:未設置SO_REUSEADDR,在重啟時就會綁定失敗顯示資源被占用,需要等待該IP+Port被釋放才可以重啟成功,該問題對於線上服務不可接受。
因此需要將服務端的socket設置為地址復用:
int enable = 1;
setsocketopt(sockfd,SOL_SOCKET,SO_REUSEADDR,(void*)&enable,sizeof(enable));
5. SO_REUSEPORT選項
作用效果:端口復用選項SO_REUSEPORT是在SO_REUSEADDR之后於Linux3.9版本加入的,並不是所有系統都支持該選項。 SO_REUSEPORT 允許多個進程監聽相同的IP和Port,但是為了防止端口劫持增加了對進程所屬用戶的限制。
內核支持:端口復用選項是個非常大的進步,有利於服務端程序擴展、提高並發能力。值得一提的是SO_REUSEPORT在內核層面實現了簡單的負載均衡,為監聽的多個進程進行流量分發。
Nginx應用:Nginx的1.9.1版本引入了SO_REUSEPORT套接字選項,對於Nginx而言,啟用該選項可以減少在某些場景下的鎖競爭而改善性能。
Linux 3.9版本和Nginx1.9.1版本(含)之后的版本,Nginx已經無需再使用 互斥鎖 ngx_use_accept_mutex,引入SO_REUSEPORT選項由內核層面實現負責均衡來解決驚群問題。
設置方法:
int enable = 1;
setsocketopt(sockfd,SOL_SOCKET,SO_REUSEPORT,(void*)&enable,sizeof(enable));
電腦刺綉綉花廠 http://www.szhdn.com 廣州品牌設計公司https://www.houdianzi.com
6. TCP_NODELAY選項
簡單背景 : 為解決福特公司局域網擁塞問題 ,Nagle算法由福特公司的John Nagle 在1984年提出。同時代的其他網絡也存在這種情況,因此Naggle算法被引入到協議棧。
算法原則:盡可能發送大塊數據,避免網絡中充斥着許多小數據塊,任意時刻最多只能有一個未被確認的小段。未被確認是指一個數據塊發送出去后,沒有收到對方發送的ACK確認。
通俗解釋:就是在兩座城市的高速路上之前充斥着非常多的貨車,貨車的車廂中可能是一根羽毛、一個玩具熊或者一台機器等,造成了高速路的擁堵。為此要求每次最多只有一輛未被授權的貨車行駛且每個貨車裝載盡可能多的東西,從而提高單次運輸效率和降低貨車數量,緩解高速路的擁堵。
算法弊端:上世紀80年代網絡帶寬有限,Nagle算法有效改善了網絡擁塞情況,但是隨着網絡帶寬的增加和通信基礎設施水平的提高,最多只能有一個未被確認的小段的限制導致了無意義的等待,無法有效利用當前的網絡帶寬。
算法禁用:TCP_NODELAY可以解決Nagle算法帶來的問題,開啟TCP_NODELAY意味着允許小包的發送且不強制等待,對時效高且數據量小的應用非常實用。從應用程序的角度來說應該盡量避免寫小包,從而實現數據包大小和數據包數量的效率最大化。
設置方法 :
int enable = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable,sizeof(enable));
注:CORK 算法與 Nagle 算法非常類似,感興趣可自行查閱。