Linux C/C++编程:setsockopt、getsockopt
文章目錄
- 概敘
- 理論
- 實踐:當前系統支持哪些socket選項
- 通用套接字選項
- TCP_DEFER_ACCEPT
- TCP_NODELAY
- TCP_FASTOPEN
- SO_REUSEADDR、SO_REUSEPORT
- SO_ACCEPTCONN
- SO_SNDBUF和SO_RCVBUF
- 理論
- 含義
- 與實際使用內存的關系?
- 與滑動窗口的關系?
- 接收緩存區和接收滑動窗口關系
- 發送緩存區和發送滑動窗口關系
- 緩沖區大小預估
- 1. 獲取接收緩沖區的大小
- 2. 封裝
- SO_RCVLOWAT、SO_SNDLOWAT
- SO_BROADCAST套接字選項
- SO_DEBUG 套接字選項
- SO_DONTROUTE套接字選項
- SO_ERROR套接字選項
- SO_KEEPALIVE套接字選項
- SO_LINGER
- SO_ERROR
- SO_RCVTIMEO, SO_SNDTIMEO
- 示例1:設置connect超時時間
- 示例2:超時接收(服務器數據)
概敘
理論
如果說fcntl系統調用是控制文件描述符屬性的通用POSIX方法,那么setsockopt、getsockopt就是專門用來讀取和設置socket文件描述符屬性的方法
#include <sys/socket.h> /* * 參數: sockfd: 指向一個打開的套接字描述符 * level: 指定系統中解釋選項的代碼或者通用套接字,或者指定要操作哪個協議的選項(比如IPV4,IPV6,TCP, SCTP) * optname:指定選項的名字 * optval、optlen:被操作選項的值和長度 * setsockopt從*optval中取得選項待設置的新值 * getsockopt則把已獲取的選項當前值放到*optval中。 * 返回值: 成功0,出錯-1并設置error */ int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); int getsockopt(int socket, int level, int option_name,void *restrict optval, socklen_t *restrict option_len);下面匯總了所有可以由setsockopt和getsockopt設置的選項。其中“數據類型”給出了指針optval必須指向的每個選項的數據類型,比如linger{}表示struct linger
實踐:當前系統支持哪些socket選項
檢查上表選項是否得到支持,如果有,則輸出默認值
#include <netinet/tcp.h> /* for TCP_xxx defines */ #include <stddef.h> #include <netinet/in.h> #include <stdio.h> #include <unp.h>//在Union類型中枚舉每一個getsockopt的每個可能的返回值 union val {int i_val;long l_val;struct linger linger_val;struct timeval timeval_val; } val;// 為用戶輸出給定套接字選項的4個函數定義原型 static char *sock_str_flag(union val *, int); static char *sock_str_int(union val *, int); static char *sock_str_linger(union val *, int); static char *sock_str_timeval(union val *, int);// sock_opts結構包含給每個套接字選項調用getsockopt并輸出其當前值所需要的信息 // sock_opts數組里面的每個元素代表一個套接字選項 struct sock_opts {const char *opt_str;int opt_level;int opt_name;char *(*opt_val_str)(union val *, int); } sock_opts[] = {{ "SO_BROADCAST", SOL_SOCKET, SO_BROADCAST, sock_str_flag },{ "SO_DEBUG", SOL_SOCKET, SO_DEBUG, sock_str_flag },{ "SO_DONTROUTE", SOL_SOCKET, SO_DONTROUTE, sock_str_flag },{ "SO_ERROR", SOL_SOCKET, SO_ERROR, sock_str_int },{ "SO_KEEPALIVE", SOL_SOCKET, SO_KEEPALIVE, sock_str_flag },{ "SO_LINGER", SOL_SOCKET, SO_LINGER, sock_str_linger },{ "SO_OOBINLINE", SOL_SOCKET, SO_OOBINLINE, sock_str_flag },{ "SO_RCVBUF", SOL_SOCKET, SO_RCVBUF, sock_str_int },{ "SO_SNDBUF", SOL_SOCKET, SO_SNDBUF, sock_str_int },{ "SO_RCVLOWAT", SOL_SOCKET, SO_RCVLOWAT, sock_str_int },{ "SO_SNDLOWAT", SOL_SOCKET, SO_SNDLOWAT, sock_str_int },{ "SO_RCVTIMEO", SOL_SOCKET, SO_RCVTIMEO, sock_str_timeval },{ "SO_SNDTIMEO", SOL_SOCKET, SO_SNDTIMEO, sock_str_timeval },{ "SO_REUSEADDR", SOL_SOCKET, SO_REUSEADDR, sock_str_flag }, #ifdef SO_REUSEPORT{ "SO_REUSEPORT", SOL_SOCKET, SO_REUSEPORT, sock_str_flag }, #else{ "SO_REUSEPORT", 0, 0, NULL }, #endif{ "SO_TYPE", SOL_SOCKET, SO_TYPE, sock_str_int },// { "SO_USELOOPBACK", SOL_SOCKET, SO_USELOOPBACK, sock_str_flag },{ "IP_TOS", IPPROTO_IP, IP_TOS, sock_str_int },{ "IP_TTL", IPPROTO_IP, IP_TTL, sock_str_int }, #ifdef IPV6_DONTFRAG{ "IPV6_DONTFRAG", IPPROTO_IPV6,IPV6_DONTFRAG, sock_str_flag }, #else{ "IPV6_DONTFRAG", 0, 0, NULL }, #endif #ifdef IPV6_UNICAST_HOPS{ "IPV6_UNICAST_HOPS", IPPROTO_IPV6,IPV6_UNICAST_HOPS,sock_str_int }, #else{ "IPV6_UNICAST_HOPS", 0, 0, NULL }, #endif #ifdef IPV6_V6ONLY{ "IPV6_V6ONLY", IPPROTO_IPV6,IPV6_V6ONLY, sock_str_flag }, #else{ "IPV6_V6ONLY", 0, 0, NULL }, #endif{ "TCP_MAXSEG", IPPROTO_TCP,TCP_MAXSEG, sock_str_int },{ "TCP_NODELAY", IPPROTO_TCP,TCP_NODELAY, sock_str_flag }, #ifdef SCTP_AUTOCLOSE{ "SCTP_AUTOCLOSE", IPPROTO_SCTP,SCTP_AUTOCLOSE,sock_str_int }, #else{ "SCTP_AUTOCLOSE", 0, 0, NULL }, #endif #ifdef SCTP_MAXBURST{ "SCTP_MAXBURST", IPPROTO_SCTP,SCTP_MAXBURST, sock_str_int }, #else{ "SCTP_MAXBURST", 0, 0, NULL }, #endif #ifdef SCTP_MAXSEG{ "SCTP_MAXSEG", IPPROTO_SCTP,SCTP_MAXSEG, sock_str_int }, #else{ "SCTP_MAXSEG", 0, 0, NULL }, #endif #ifdef SCTP_NODELAY{ "SCTP_NODELAY", IPPROTO_SCTP,SCTP_NODELAY, sock_str_flag }, #else{ "SCTP_NODELAY", 0, 0, NULL }, #endif{ NULL, 0, 0, NULL } }; /* *INDENT-ON* */ /* end checkopts1 *//* include checkopts2 */ int main(int argc, char **argv) {int fd;socklen_t len;struct sock_opts *ptr;for (ptr = sock_opts; ptr->opt_str != NULL; ptr++) {printf("%s: ", ptr->opt_str);if (ptr->opt_val_str == NULL)printf("(undefined)\n");else {switch(ptr->opt_level) {case SOL_SOCKET:case IPPROTO_IP:case IPPROTO_TCP:fd = Socket(AF_INET, SOCK_STREAM, 0);break; #ifdef IPV6case IPPROTO_IPV6:fd = Socket(AF_INET6, SOCK_STREAM, 0);break; #endif #ifdef IPPROTO_SCTPcase IPPROTO_SCTP:fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);break; #endifdefault:err_quit("Can't create fd for level %d\n", ptr->opt_level);}len = sizeof(val);if (getsockopt(fd, ptr->opt_level, ptr->opt_name,&val, &len) == -1) {err_ret("getsockopt error");} else {printf("default = %s\n", (*ptr->opt_val_str)(&val, len));}close(fd);}}exit(0); }static char strres[128];static char *sock_str_int(union val *ptr, int len) {if (len != sizeof(int))snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);elsesnprintf(strres, sizeof(strres), "%d", ptr->i_val);return(strres); } static char *sock_str_flag(union val *ptr, int len) {if (len != sizeof(int))snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);elsesnprintf(strres, sizeof(strres),"%s", (ptr->i_val == 0) ? "off" : "on");return(strres); } static char *sock_str_linger(union val *ptr, int len) {struct linger *lptr = &ptr->linger_val;if (len != sizeof(struct linger))snprintf(strres, sizeof(strres),"size (%d) not sizeof(struct linger)", len);elsesnprintf(strres, sizeof(strres), "l_onoff = %d, l_linger = %d",lptr->l_onoff, lptr->l_linger);return(strres); }static char *sock_str_timeval(union val *ptr, int len) {struct timeval *tvptr = &ptr->timeval_val;if (len != sizeof(struct timeval))snprintf(strres, sizeof(strres),"size (%d) not sizeof(struct timeval)", len);elsesnprintf(strres, sizeof(strres), "%d sec, %d usec",tvptr->tv_sec, tvptr->tv_usec);return(strres); }通用套接字選項
TCP_DEFER_ACCEPT
Linux 提供的一個特殊 setsockopt , 在 accept 的 socket 上面,只有當實際收到了數據,才喚醒正在 accept 的進程,可以減少一些無聊的上下文切換
- 經過測試發現,設置TCP_DEFER_ACCEPT選項后,服務器受到一個CONNECT請求后,操作系統不會Accept,也不會創建IO句柄。操作系統應該在若干秒,(但肯定遠遠大于上面設置的1s) 后,會釋放相關的鏈接。但沒有同時關閉相應的端口,所以客戶端會一直以為處于鏈接狀態。如果Connect后面馬上有后續的發送數據,那么服務器會調用Accept接收這個鏈接端口。
- 感覺了一下,這個端口設置對于CONNECT鏈接上來而又什么都不干的攻擊方式處理很有效。我們原來的代碼都是先允許鏈接,然后再進行超時處理,比他這個有點Out了。不過這個選項可能會導致定位某些問題麻煩。
可以通過setsockopt來設置defer accept:setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout, sizeof(timeout)) < 0
- 其中,timeout為超時時間,內核會把此時間轉換為最大重傳次數。
- 單位是秒,注意如果打開這個功能,kernel 在 val 秒之內還沒有收到數據,不會繼續喚醒進程,而是直接丟棄連接。
在不使用此選項的情況下,TCP三次握手建立連接的過程為:
- 使用此選項時,若Client和Server完成三次握手,
- 但Client并沒有數據發來,這時Server并不會把連接的狀態置為ESTABLISHED,而是會忽略最后一次ACK,保持SYN_RECV狀態不變,應用也就沒有機會accept這個連接,
- 這樣可以避免Client建立連接卻不發送請求,導致Server的資源浪費,
- 尤其對于像Apache這樣的應用,接收到連接之后會一直阻塞等待Client的請求數據,直到超時,嚴重影響服務的性能和穩定性。
然,不同的內核版本對此選項的支持卻大不一樣,比如 2.6.18 和 2.6.32的行為就大不相同,對比內核源碼:
- 2.6.18:丟棄ack的條件是設置了defer accept選項:
- 2.6.32:丟棄ack的條件是重傳ack的次數小于在設置defer accept時指定的值(內核會根據timeout參數的值計算最大重傳次數):
- 2.6.18:重傳SYN+ACK,若Client及時響應ACK但沒有數據到來,Server就會定時重傳SYN+ACK,保持連接狀態SYN_RECV不變。
- 2.6.32:實際上并不會多次重傳SYN+ACK,雖然中間會定時計算是否需要重傳,但只會重傳超時時間之內的最后一次SYN+ACK,看下面代碼,注意注釋:
- 2.6.18:由于Server端連接狀態保持為SYN_RECV,超過最大重傳次數后,Server會刪掉SYN_RECV狀態的連接,應用也就沒有機會Accept這個新連接。Client看到的狀態仍然是ESTABLISHED,但此連接并沒有占用Server的資源。
- 2.6.32:若Client響應最后一次重傳的SYN+ACK,由于中間會定時計算是否需要重傳并對重傳次數計數(req->restrans++),因此上面代碼中丟棄ACK的條件就不成立,Server就會把連接置為ESTABLISHED,交給應用Accept這個新連接準備讀取數據。下面的代碼顯示雖然中間不重傳SYN+ACK,但仍然計數:
結論:- 2.6.18內核:若三次握手完成后Client沒有數據發來,Server的連接狀態一直是SYN_RECV,不會Accept新連接,直到把SYN_RECN狀態的連接刪除,但Client仍然顯示ESTABLISHED。
- 2.6.32內核:若三次握手完成后Client沒有數據發來,Server的連接狀態最終會變成ESTABLISHED,會Accept新連接,Client和Server連接的狀態均為ESTABLISHED。
至于2.6.18之后從哪個版本開始變成了2.6.32的行為,沒有考證,2.6.32及之后的版本都是同樣的處理方式。
此外,當三次握手完成,且沒有數據到來,Server的連接狀態處于SYN_RECV狀態時,如果Client發送FIN,則Server會響應ack及后續的FIN,正常終止連接;在2.6.18上,若Server端已超時,SYN_RECV狀態的連接被移除后,Client發送FIN時,Server響應RST。
相關參數:
TCP_NODELAY
糊涂窗口綜合征:是指當發送端應用進程產生數據很慢、或接收端應用進程處理接收緩沖區數據很慢,或二者兼而有之;就會使應用進程間傳送的報文段很小,特別是有效載荷很小; 極端情況下,有效載荷可能只有1個字節;傳輸開銷有40字節(20字節的IP頭+20字節的TCP頭) 這種現象。
如果發送端為產生數據很慢的應用程序服務(典型的有telnet應用),例如,一次產生一個字節。這個應用程序一次將一個字節的數據寫入發送端的TCP的緩存。如果發送端的TCP沒有特定的指令,它就產生只包括一個字節數據的報文段。結果有很多41字節的IP數據報就在互連網中傳來傳去。
解決的方法是防止發送端的TCP逐個字節地發送數據。必須強迫發送端的TCP收集數據,然后用一個更大的數據塊來發送。發送端的TCP要等待多長時間呢?如果它等待過長,它就會使整個的過程產生較長的時延。如果它的等待時間不夠長,它就可能發送較小的報文段。Nagle找到了一個很好的解決方法,發明了Nagle算法。
用TCP_NODELAY選項可以禁止Negale 算法.
TCP_FASTOPEN
TCP建立連接需要三次握手,這個大家都知道。但是三次握手會導致傳輸效率下降,尤其是HTTP這種短連接的協議,雖然HTTP有keep-alive來讓一些請求頻繁的HTTP提高性能,避免了一些三次握手的次數,但是還是希望能繞過三次握手提高效率,或者說在三次握手的同時就把數據傳輸的事情給做了。TCP Fast Open(簡稱TFO)就是來干這樣的事情的
首先我們回顧一下三次握手的過程:
這里客戶端在最后ACK的時候,完全可以將想要發送的第一條數據也一起帶過去,這是TFO做的其中一個優化方案。
然后TFO還參考了HTTP登錄態的流程,采用cookie的方案,讓客戶知道某個客戶端之前已經登陸過了,那么它發過來的數據就可以直接接收了,不需要一開始必須三次握手再發送數據
當客戶端第一次連接服務端時,是沒有Cookie的,所以會發送一個空的Cookie,意味著要請求Cookie,如下圖:
這樣服務端就會將Cookie通過SYN+ACK的路徑返回給客戶端,客戶端保存后,將發送的數據三次握手的最后一步ACK同時發送給服務端。
當客戶端斷開連接,下一次請求同一個服務端的時候,會帶上之前存儲的Cookie和要發送的數據,在SYN的路徑上一起發送給服務端,如下圖:
這樣之后每次握手的時候還同時發送了數據信息,將數據傳輸提前了。服務端只要驗證了Cookie,就會將發送的數據接收,否則會丟棄并且再通過SYN+ACK路徑返回一個新的Cookie,這種情況一般是Cookie過期導致的。
TFO是需要開啟的,開啟參數在:
/proc/sys/net/ipv4/tcp_fastopen 0:關閉 1:作為客戶端使用Fast Open功能,默認值 2:作為服務端使用Fast Open功能 3:無論是客戶端還是服務端都使用Fast Open功能并且如果之前的代碼沒有做這方面的處理,也是不能使用的,從上面的流程圖就能看到,客戶端是在連接的過程就發送數據,但是之前客戶端都是先調用connect成功后,才用send發送數據的。
服務端需要對listen的socket設置如下選項:
//需要的頭文件 #include <netinet/tcp.h>int qlen = 5; //fast open 隊列 setsockopt(m_listen_socket, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));客戶端則直接使用sendto方法進行連接和發送數據,示例代碼如下:
#include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <errno.h> #include <string.h> #include <stdio.h> #include <unistd.h>int main(){struct sockaddr_in serv_addr;struct hostent *server;const char *data = "Hello, tcp fast open";int data_len = strlen(data); int sfd = socket(AF_INET, SOCK_STREAM, 0);server = gethostbyname("10.104.1.149");bzero((char *) &serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr,server->h_length);serv_addr.sin_port = htons(5556);int len = sendto(sfd, data, data_len, MSG_FASTOPEN/*MSG_FASTOPEN*/, (struct sockaddr *) &serv_addr, sizeof(serv_addr));if(errno != 0){printf("error: %s\n", strerror(errno));}char buf[100] = {0};recv(sfd, buf, 100, 0);printf("%s\n", buf);close(sfd);}經過試驗,客戶端存儲的Cookie是跟服務端的IP綁定的,而不是跟進程或端口綁定。當客戶端程序發送到同一個IP但是不同端口的進程時,使用的是同一個Cookie,而且服務端也認證成功。
SO_REUSEADDR、SO_REUSEPORT
1、SO_REUSEADDR重用處于TIME_WAIT的socket
- SO_REUSEADDR用于對TCP套接字處于TIME_WAIT狀態下的socket,才可以重復綁定使用。server程序總是應該在調用bind()之前設置SO_REUSEADDR套接字選項。TCP,先調用close()的一方會進入TIME_WAIT狀態
- 另外一個方法:通過修改內核參數/proc/sys/net/ipv4/tcp_tw_recycle來快速回收被關閉的socket,從而使得TCP連接根據不進入TIME_WAIT狀態,進而運行應用程序立即重用本地的socket地址
SO_REUSEADDR提供如下四個功能:
- SO_REUSEADDR允許啟動一個監聽服務器并捆綁其眾所周知的端口,即使以前建立的將此端口用作他們的本地端口的連接仍舊存在。這通常是重啟監聽服務器時出現,如果不設置此項,則bind出錯
- 假如一個systemd托管的service異常退出了,留下了TIME_WAIT狀態的socket,那么systemd將會嘗試重啟這個service。但是因為端口被占用,會導致啟動失敗,造成兩分鐘的服務空檔期,systemd也可能在這期間放棄重啟服務。
- 但是在設置了SO_REUSEADDR以后,處于TIME_WAIT狀態的地址也可以被綁定,就杜絕了這個問題。因為TIME_WAIT其實本身就是半死狀態,雖然這樣重用TIME_WAIT可能會造成不可預料的副作用,但是在現實中問題很少發生,所以也忽略了它的副作用。
- SO_REUSEADDR允許在同一端口上啟動同一服務器的多個實例,只要每個實例捆綁一個不同的本地IP地址即可。對于TCP,我們根本不可能啟動捆綁相同IP地址和相同端口號的多個服務器。
- SO_REUSEADDR允許單個進程捆綁在同一端口的多個套接字上,只要每個捆綁指定不同的本地IP地址即可。這一般不用于TCP服務器。
- SO_REUSEADDR允許完全重復的捆綁:當一個IP地址和端口綁定到某個套接口上時,還允許此IP地址和端口捆綁到另一個套接口上。一般來說,這個特性僅在支持多播的系統上才有,而且只對UDP套接口而言(TCP不支持多播)
2、SO_REUSEPORT端口重用
Linux kernel 3.9之前的版本,一個ip+port組合,只能被監聽bind一次。這樣在多核環境下,往往只能有一個線程(或者進程)是listener,在高并發情況下,往往這就是性能瓶頸。于是Linux kernel 3.9之后,Linux推出了端口重用SO_REUSEPORT選項。
SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務器程序的性能,解決的問題:
-
允許多個套接字 bind()/listen() 同一個TCP/UDP端口
- 每一個線程擁有自己的服務器套接字
- 在服務器套接字上沒有了鎖的競爭
-
內核層面實現負載均衡
-
安全層面,監聽同一個端口的套接字只能位于同一個用戶下面
其核心的實現主要有三點:
- 擴展 socket option,增加 SO_REUSEPORT 選項,用來設置 reuseport。
- 修改 bind 系統調用實現,以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實現,查找 listener 的時候,能夠支持在監聽相同 IP 和端口的多個 sock 之間均衡選擇。
有了SO_RESUEPORT后,每個進程可以自己創建socket、bind、listen、accept相同的地址和端口,各自是獨立平等的。讓多進程監聽同一個端口,各個進程中accept socket fd不一樣,有新連接建立時,內核只會喚醒一個進程來accept,并且保證喚醒的均衡性。
4、使用這兩個套接字選項的定義
- 在所有的TCP服務器上,在調用bind之前設置SO_REUSEADDR選項
- 當編寫一個同一時刻在同一主機上可運行多次的多播應用程序時,設置SO_REUSEADDR選項,并將本組的多播地址作為本地IP地址捆綁。
SO_REUSEADDR選項的本質?
這個套接字選項通知內核:
- 如果端口忙,但是TCP狀態位于TIME_WAIT ,可以重用端口。
- 如果端口忙,而TCP狀態位于其他狀態,重用端口時依舊得到一個錯誤信息,指明"地址已經使用中"。
一個套接字由相關五元組構成,協議、本地地址、本地端口、遠程地址、遠程端口。SO_REUSEADDR 僅僅表示可以重用本地本地地址、本地端口,整個相關五元組還是唯一確定的。所以,重啟后的服務程序有可能收到非期望數據。必須慎重使用SO_REUSEADDR 選項。
SO_REUSEPORT和SO_REUSEADDR
從字面意思理解,SO_REUSEPORT是端口重用,SO_REUSEADDR是地址重用。兩者的區別:
(1)SO_REUSEPORT是允許多個socket綁定到同一個ip+port上。SO_REUSEADDR用于對TCP套接字處于TIME_WAIT狀態下的socket,才可以重復綁定使用。
(2)兩者使用場景完全不同。SO_REUSEADDR這個套接字選項通知內核,如果端口忙,但TCP狀態位于TIME_WAIT,可以重用端口。這個一般用于當你的程序停止后想立即重啟的時候,如果沒有設定這個選項,會報錯EADDRINUSE,需要等到TIME_WAIT結束才能重新綁定到同一個ip+port上。而SO_REUSEPORT用于多核環境下,允許多個線程或者進程綁定和監聽同一個ip+port,無論UDP、TCP(以及TCP是什么狀態)。
(3)對于多播,兩者意義相同。
為什么要引入SO_REUSEPORT:
1、驚群效應:簡單來說就是多個進程或者線程等待同一個事件,當事件發生時,所有進程和線程都會被內核喚醒。喚醒之后通常只有一個進程獲得了該事件并進行處理,其他進程發現獲取事件失敗之后又繼續進入了等待狀態,在一定長度上降低了性能。
2、為什么驚群效應會降低系列性能?
- 多線程/多進程的喚醒,會進行上下文切換。頻繁的上下文切換帶來的一個問題是數據將頻繁的在寄存器與運行隊列中流轉。極端情況下,時間更多的小號在進程/線程的調度上,而不是執行
- 為了確保只有一個線程得到資源,用戶必須對資源操作進行加鎖保護,進一步加大了系統開銷。
3、常見的驚群問題
在 Linux 下,我們常見的驚群效應發生于我們使用 accept 以及我們 select 、poll 或 epoll 等系統提供的 API 來處理我們的網絡鏈接。
(1) accept 驚群:
首先我們用一個流程圖來復習下我們傳統的 accept 使用方式
服務器
客戶端
#include <stdlib.h> #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include<arpa/inet.h> #include <unistd.h> #include <errno.h> #define SA struct sockaddr#define SERV_PORT 10086 int main(int argc, char **argv) {int sockfd;struct sockaddr_in servaddr;sockfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);connect(sockfd, (SA *) &servaddr, sizeof(servaddr));exit(0); }為什么這里沒有出現我們想要的現象(一個進程 accept 成功,三個進程 accept 失敗)?原因在于在 Linux 2.6 之后,Accept 的驚群問題從內核上被處理了
(2) select/poll/epoll 驚群:
我們以 epoll 為例,我們來看看傳統的工作模式
服務器:
客戶端同上
怎么使用?
1、封裝
# define ACL_SOCKET int # define ACL_SOCKET_INVALID (int) -1 ACL_SOCKET acl_inet_bind(const struct addrinfo *res, unsigned flag){ACL_SOCKET fd;int on;fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);if (fd == ACL_SOCKET_INVALID) {acl_msg_error("%s(%d): create socket %s",__FILE__, __LINE__, acl_last_serror());return ACL_SOCKET_INVALID;}on = 1;if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,(const void *) &on, sizeof(on)) < 0) {acl_msg_warn("%s(%d): setsockopt(SO_REUSEADDR): %s",__FILE__, __LINE__, acl_last_serror());}on = 1;if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT,(const void *) &on, sizeof(on)) < 0)acl_msg_warn("%s(%d): setsocket(SO_REUSEPORT): %s",__FILE__, __LINE__, acl_last_serror());if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) {close(fd);acl_msg_error("%s(%d): bind error %s",__FILE__, __LINE__, acl_last_serror());return ACL_SOCKET_INVALID;}return fd; }2、udp_reuseport_server/client
參考
SO_ACCEPTCONN
- SO_ACCEPTCONN: 該套接字是否是監聽套接字(listen)
SO_SNDBUF和SO_RCVBUF
理論
含義
- SO_SNDBUF:TCP發送緩沖區的容量上限;
- SO_RCVBUF:TCP接受緩沖區的容量上限;
注意:緩沖區的上限不能無限大,如果超過內核設置的上限值,則以內核設置值為準
$ sysctl -a net.ipv4.tcp_rmem = 8192 87380 16777216 net.ipv4.tcp_wmem = 8192 65536 16777216 net.ipv4.tcp_mem = 8388608 12582912 16777216與實際使用內存的關系?
- SO_SNDBUF和SO_RCVBUF只是規定了讀寫緩沖區大小的上限,在實際使用未達到上限前,SO_SNDBUF和SO_RCVBUF是不起作用的。
- 一個TCP連接占用的內存相當于讀寫緩沖區實際占用內存大小之和。
- 當我們用setsockopt來設置TCP的接收緩沖區和發送緩沖區的大小時,系統都會將其值加倍,并且不得小于某個值。這是為了確保一個TCP連接擁有足夠的空閑緩沖區來處理擁塞。
- 接收緩沖區最小值一般是256
- 發送緩沖區最小值一般是2048
- 我們也可以強制修改內存參數/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem來強制TCP接收緩沖區和發送緩沖區的大小沒有最小值限制
與滑動窗口的關系?
接收緩存區和接收滑動窗口關系
接收緩存區包含了滑動窗口,即接收緩存區大小>= 滑動窗口大小。接受緩沖區的數據主要分為兩部分:
- 接受滑動窗口內的無序的TCP報文;
- 有序的,應用還未讀取的數據(占用比例:1/(2^tcp_adv_win_scale),默認tcp_adv_win_scale配置為2);
因此,當接受緩沖區上限固定后,如果應用程序讀取數據的速率過慢,接收滑動窗口會縮小,從而通知連接的對端降低發送速度,避免無謂的網絡傳輸。
發送緩存區和發送滑動窗口關系
發送緩存區包含了發送滑動窗口,即發送緩存區大小>= 發送滑動窗口大小。發送緩沖區的數據主要分為兩部分:
- 發送窗口內的數據:已發送還未確認的數據;
- 應用寫入的數據;
緩沖區大小預估
一般以BDP來設置最大接收窗口,BDP叫做帶寬時延積,也就是帶寬與網絡時延的乘積。因為BDP就表示了網絡承載能力,最大接收窗口就表示了網絡承載能力內可以不經確認發出的報文。如下圖所示:
根據接受窗口大小的占比1-1/(2^tcp_adv_win_scale),計算出緩沖區大小上限;
舉例:例如若我們的帶寬為2Gbps,時延為10ms,那么帶寬時延積BDP則為2G/8 * 0.01=2.5MB,所以這樣的網絡中可以設最大接收窗口為2.5MB,當tcp_adv_win_scale=2時最大讀緩存可以設為4/3*2.5MB=3.3MB。
1. 獲取接收緩沖區的大小
#include <stdlib.h> #include <stdio.h> #include <getopt.h> #include <zconf.h> #include <sys/socket.h>int main(int argc, char *argv[]) {int fd, val;socklen_t len;char strres[128];len = sizeof(val);fd = socket(AF_INET, SOCK_STREAM, 0);if(getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &val, &len) == -1){printf("getsockopt error");exit(0);}else{if(len != sizeof(int))snprintf(strres, sizeof(strres), "sizeof (%d) not sizeof(int)", len);elsesnprintf(strres, sizeof(strres), "%d", val);printf("SO_RCVBUF default = %s\n", strres); // 87380}close(fd);exit(0);}2. 封裝
// // Created by oceanstar on 2021/9/7. //#include <cerrno> #include <cstring> #include <msg/acl_msg.h> #include <net/acl_getsocktype.h> #include "acl_tcp_ctl.h"namespace oceanstar{/*** 設置 TCP 套接字的寫緩沖區大小* @param fd {ACL_SOCKET} 套接字* @param size {int} 緩沖區設置大小*/void acl_tcp_set_sndbuf(ACL_SOCKET fd, int size){int n = acl_getsocktype(fd);if (n != AF_INET && n != AF_INET6)return;if(setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (char *)&size, sizeof(size)) < 0){acl_msg_error("(%s:%d): FD %d, SIZE %d: %s\n",__LINE__, __FUNCTION__ , fd, size, strerror(errno));}}/*** 獲取 TCP 套接字的寫緩沖區大小* @param fd {ACL_SOCKET} 套接字* @return {int} 緩沖區大小*/int acl_tcp_get_sndbuf(ACL_SOCKET fd){int n = acl_getsocktype(fd);if (n != AF_INET && n != AF_INET6)return 0;int size;socklen_t len;len = (socklen_t) sizeof(size);if (getsockopt(fd, SOL_SOCKET, SO_SNDBUF, (char *) &size, &len) < 0) {acl_msg_error("%s(%d): size(%d), getsockopt error(%s)",__LINE__, __LINE__, size, strerror(errno));return -1;}return size;}/*** 設置 TCP 套接字的讀緩沖區大小* @param fd {ACL_SOCKET} 套接字* @param size {int} 緩沖區設置大小*/void acl_tcp_set_rcvbuf(ACL_SOCKET fd, int size){int n = acl_getsocktype(fd);if (n != AF_INET && n != AF_INET6)return ;if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF,(char *) &size, sizeof(size)) < 0) {acl_msg_error("%s(%d): size(%d), setsockopt error(%s)",__FILE__, __LINE__, size, strerror(errno));}}/*** 獲取 TCP 套接字的讀緩沖區大小* @param fd {ACL_SOCKET} 套接字* @return {int} 緩沖區大小*/int acl_tcp_get_rcvbuf(ACL_SOCKET fd){int n = acl_getsocktype(fd);if (n != AF_INET && n != AF_INET6)return 0;int size;socklen_t len;len = (socklen_t)sizeof(size);if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF,(char *) &size, &len) < 0){acl_msg_error("%s(%d): size(%d), getsockopt error(%s)",__FILE__, __LINE__, size, strerror(errno));return -1;}return size;} }2. 客戶端修改TCP發送緩沖區大小—待研究
SO_RCVLOWAT、SO_SNDLOWAT
SO_RCVLOWAT、SO_SNDLOWAT分別表示TCP接收緩存和發送緩沖區的低水位標記。他們一般被IO復用系統調用用來判斷socket是否可讀可寫:
- 當TCP接收緩沖區中可讀數據的總數大于其低水位標記時,IO復用系統調用將通知應用程序可以從對應的socket上讀取數據;
- 當TCP發送緩沖區中空閑空間的大小大于其低水位標記時,IO復用系統調用將通知應用程序可以往對應的socket上寫數據
默認情況下,TCP接收/發送緩沖區的低水位標記均為1
SO_BROADCAST套接字選項
- 本選項開啟或禁止進程發送廣播消息的能力。只有數據報套接字支持廣播,并且還必須是在支持廣播消息的網絡上(例如以太網,令牌環網等)。
- 我們不可能在點對點鏈路上進行廣播,也不可能在基于連接的傳輸協議(例如TCP和SCTP)之上進行廣播。
SO_DEBUG 套接字選項
- 本選項僅由TCP支持。當給一個TCP套接字開啟本選項時,內核將為TCP在該套接字發送和接受的所有分組保留詳細跟蹤信息。這些信息保存在內核的某個環形緩沖區中,并可使用trpt程序進行檢查。
SO_DONTROUTE套接字選項
SO_ERROR套接字選項
當一個套接字上發生錯誤時,源自Berkeley的內核中的協議模塊將該套接字的名為so_error的變量設置為標準的Unix Exxx值中的一個。我們稱它為該套接字的待處理錯誤。
內核能以下面兩種方式之一立即通知進程這個錯誤:
- 如果進程阻塞在對該套接字的select調用上,那么無論是檢查可讀條件還是可寫條件,select均返回并設置其中一個或者所有兩個條件
- 如果進程使用信號驅動式IO模型,那么給進程或者進程組一個SIGIO信號。
進程然后可以通過訪問SO_ERROR套接字選項獲取so_error的值。由getsockopt返回的整數值就是該套接字的待處理錯誤。so_error隨后由內核復位為0
-
當進程調用read:
- 沒有數據返回時,那么read返回-1而且errno被設置為so_error的值(非0值), so_error設復位為0
- 如果由數據等待被讀取,那么read返回讀取到的數據。
-
如果在進程調用write時so_error為非0值,那么wriet返回-1而且被errno被設為so_error的值(非0值), so_error設復位為0
SO_KEEPALIVE套接字選項
SO_KEEPALIVE 保持連接檢測對方主機是否崩潰,避免(服務器)永遠阻塞于TCP連接的輸入。
設置該選項后,如果2小時內在此套接口的任一方向都沒有數據交換,TCP就自動給對方 發一個保持存活探測分節(keepalive probe)。這是一個對方必須響應的TCP分節.它會導致以下三種情況:
1、對方接收一切正常:以期望的ACK響應,應用程序無動作(因為一切正常),又經過沒有動靜的2小時后,TCP將發出另一個探測分節。
2、對方已崩潰且已重新啟動:以RST響應。套接口的待處理錯誤被置為ECONNRESET,套接 口本身則被關閉。
3、對方無任何響應:源自berkeley的TCP發送另外8個探測分節,相隔75秒一個,試圖得到一個響應。在發出第一個探測分節11分鐘15秒后若仍無響應就放棄。
- 如果該套接字沒有任何響應,套接口的待處理錯誤被置為ETIMEOUT,套接口本身則被關閉。
- 如果該套接字收到ICMP響應,ICMP錯誤是“host unreachable(主機不可達)”,這種情況下待處理錯誤被置為 EHOSTUNREACH。
SO_LINGER
- 作用: 指定close函數對 面對連接的協議(TCP、SCTP,但不是UDP)如何操作
- 說明:在默認情況下,當調用close關閉socket的使用,close會立即返回,但是,如果send buffer中還有數據,系統會試著先把send buffer中的數據發送出去,然后close才返回。SO_LINGER選項則是用來修改這種默認操作的
SO_LINGER要求在用戶進程和內核間傳遞如下結構:
#include <sys/socket.h> struct linger { int l_onoff //0=off, nonzero=on(開關) int l_linger //linger time(延遲時間) }(1)如果l_onoff=0,則關閉這個選項。采用默認操作
(2)如果l_onoff!=0 l_linger =0:
- close調用立即返回
- TCP模塊將通過發送RST(復位)分組而不是用正常的FIN|ACK|FIN|ACK四個分組來關閉該連接。
- 至于發送緩沖區中如果有未發送完的數據,則丟棄。
- 主動關閉一方的TCP狀態則跳過TIMEWAIT,直接進入CLOSED。
網上很多人想利用這一點來解決服務器上出現大量的TIMEWAIT狀態的socket的問題,但是,這并不是一個好主意,這種關閉方式的用途并不在這兒,實際用途在于服務器在應用層的需求。
(3)如果l_onoff!=0 l_linger >0:
此時close的行為取決于兩個條件:
- 被關閉的socket對應的TCP發送緩沖區中是否還有殘留的數據
- 被關閉的socket是阻塞的還是非阻塞的。
- 如果是阻塞的,close將等待l_linger時間,直到TCP模塊發送完殘留數據并得到對方的確認。如果這段時間內TCP模塊沒有確認,那么close返回將返回-1并設置errno為EWOULDBLOCK
- 如果是非阻塞的,close立即返回,我們通過返回值和error來判斷殘留數據是否已經發送完畢
SO_ERROR
當一個套接字上發生錯誤時,內核協議中的協議模塊將此套接字的名為so_error的變量設為標準的Unix Exxx值中的一個,我們稱它為該套接字上的待處理錯誤(pending error)
內核能夠以下面兩種方式之一立即通知進程這個錯誤:
-
如果進程阻塞在對該套接字的select調用上,那么無論是檢查可讀還是可寫條件,select均返回并設置其中一個或者所有兩個條件
-
如果進程使用信號驅動式IO模型,那就給進程或者進程組產生一個SIGIO信號
進程然后可以通過訪問SO_ERROR套接字選項獲取so_error的值。由getsockopt返回的整數值就是該套接字的待處理錯誤。so_error隨后由內核復位為0.
-
當進程調用read且沒有數據返回時,如果so_error為非0值,那么read返回-1且errno被置為so_error的值。so_error隨后被復位為0。如果該套接字上有數據在排隊等待讀取,那么read返回那些數據而不是返回錯誤條件。
-
如果在進程調用write時so_error為非0值,那么write返回-1且errno被設為so_error的值。so_error隨后被復位為0。
這是一個可以獲取但不能設置的套接字選項。
xxxxxwait_write(fe);len = sizeof(err);ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (char *) &err, &len);if (ret == 0 && err == 0) {SOCK_ADDR saddr;struct sockaddr *sa = (struct sockaddr*) &saddr;socklen_t n = sizeof(saddr);if (getpeername(sockfd, sa, &n) == 0) {return 0;}return -1;}return -1;SO_RCVTIMEO, SO_SNDTIMEO
- 套接字選項SO_RCVTIMEO: 用來設置socket接收數據的超時時間;
- 套接字選項SO_SNDTIMEO: 用來設置socket發送數據的超時時間;
問:一般情況下,調用accept/connect/send/secv,進程會阻塞,但是如果對端異常,進程可能無法正常退出等待。如何讓這些調用自動定時退出
答:可以使用比如alarm定時器、IO復用設置定時器,還可以使用socket編程里函數級別的socket套接字選項SO_RCVTIMEO和SO_SNDTIMEO,僅針對于數據接收和發送相關,而無需設置專門的信號捕獲函數。
能夠作用的系統調用包括:send、sendmsg、recv、recvmsg、accept、connect。
| send | SO_SNDTIMEO | 返回-1,設置errno為EAGAIN或EWOULDBLOCK |
| sendmsg | SO_SNDTIMEO | 返回-1,設置errno為EAGAIN或EWOULDBLOCK |
| recv | SO_RCVTIMEO | 返回-1,設置errno為EAGAIN或EWOULDBLOCK |
| recvmsg | SO_RCVTIMEO | 返回-1,設置errno為EAGAIN或EWOULDBLOCK |
| accept | SO_RCVTIMEO | 返回-1,設置errno為EAGAIN或EWOULDBLOCK |
| connect | SO_SNDTIMEO | 返回-1,設置errno為EINPROGRESS |
注意:
- EAGAIN通常和EWOULDBLOCK是同一個值;
- SO_RCVTIMEO, SO_SNDTIMEO不要求系統調用對應fd是非阻塞(nonblocking)的,但是使用了該套接字選項的sock fd,會成為nonblocking(即使之前是blocking)的
示例1:設置connect超時時間
根據系統調用accept的返回值,以及errno判斷超時時間是否已到,從而決定是否開始處理超時定時任務。
客戶端程序:超時連接服務器
/*** 客戶端程序* 連接服務器,超時報錯、返回* build:* $ gcc timeout_connect.c*/ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <assert.h>/* 超時連接 */ int timeout_connect (const char *ip, int port, int time) {int ret = 0;struct sockaddr_in servaddr;printf("client start...\n");bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, ip, &servaddr.sin_addr);servaddr.sin_port = htons(port);int sockfd = socket(AF_INET, SOCK_STREAM, 0);assert(sockfd >= 0);/* 通過選項SO_RCVTIMEO和SO_SNDTIMEO設置的超時時間的類型時timeval, 和select系統調用的超時參數類型相同 */struct timeval timeout;timeout.tv_sec = time;timeout.tv_usec = 0;socklen_t len = sizeof(timeout);ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);if (ret == -1) {perror("setsockopt error");return -1;}if ((ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) < 0) {/* 超時對于errno 為EINPROGRESS. 下面條件如果成立,就可以處理定時任務了 */if (errno == EINPROGRESS) {perror("connecting timeout, process timeout logic");return -1;}perror("error occur when connecting to server\n");}return sockfd; }int main(int argc, char *argv[]) {if (argc <= 2) {printf("usage: %s ip_address port_number\n", argv[0]);return 1;}const char *ip = argv[1];int port = atoi(argv[2]);printf("connect %s:%d...\n", ip, port);int sockfd = timeout_connect(ip, port, 10);if (sockfd < 0) {perror("timeout_connect error");return 1;}return 0; }運行結果(隨意輸入一個服務器IP、端口):
$ ./timeout_connect 192.168.0.105 8000 connect 192.168.0.105:8000... client start... connecting timeout, process timeout logic: Operation now in progress timeout_connect error: Operation now in progress可以看到,本來阻塞的connect調用,10秒后返回-1,并且errno設置為EINPROGRESS。
示例2:超時接收(服務器數據)
服務端
監聽本地任意IP地址,端口8001
從鍵盤輸入一行數據,就發送給用戶;如果沒有數據,就阻塞。
客戶端
設置10秒超時,接收服務器數據。
客戶端10秒以內,接收到服務器數據,則直接打印;超過10秒,就報錯退出。
/*** 客戶端程序* 示例:超時接收服務器數據,超時時間例程中設置為10秒* 編譯: $ gcc timeout_recv_client.c -o client* 運行方式:* 如本地運行(對應服務器實際監聽的IP地址和端口號) $ ./client 127.0.0.1 8001*/ #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <arpa/inet.h>int timeout_recv(int fd, char *buf, int len, int nsec) {struct timeval timeout;timeout.tv_sec = nsec;timeout.tv_usec = 0;printf("timeout_recv called, timeout %d seconds\n", nsec);if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {perror("setsockopt error");exit(1);}int n = recv(fd, buf, len, 0);return n; }int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s <ip address> <port>\n", argv[0]);}char *ip = argv[1];uint16_t port = atoi(argv[2]);printf("client start..\n");printf("connect to %s:%d\n", ip, port);int sockfd;if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket error");exit(1);}struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, ip, &servaddr.sin_addr);servaddr.sin_port = htons(port);int connfd;if ((connfd = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) < 0) {perror("connect error");exit(1);}printf("success to connect server %s:%d\n", ip, port);printf("wait for server's response\n");char buf[100];while (1) {int nread;nread = timeout_recv(sockfd, buf, sizeof(buf), 10);if (nread < 0) {perror("timeout_recv error");exit(1);}else if (nread == 0) {shutdown(sockfd, SHUT_RDWR);break;}write(STDOUT_FILENO, buf, nread);}return 0; }客戶端運行結果:
可以看到,超過10秒后,客戶端自動退出程序,而不再阻塞在recv。
總結
以上是生活随笔為你收集整理的Linux C/C++编程:setsockopt、getsockopt的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 苏州市RFID客运车辆资产管理系统:RF
- 下一篇: linux的passive用法,get的