Linux C/C++编程:setsockopt、getsockopt
文章目錄
- 概敘
- 理論
- 實(shí)踐:當(dāng)前系統(tǒng)支持哪些socket選項(xiàng)
- 通用套接字選項(xiàng)
- TCP_DEFER_ACCEPT
- TCP_NODELAY
- TCP_FASTOPEN
- SO_REUSEADDR、SO_REUSEPORT
- SO_ACCEPTCONN
- SO_SNDBUF和SO_RCVBUF
- 理論
- 含義
- 與實(shí)際使用內(nèi)存的關(guān)系?
- 與滑動(dòng)窗口的關(guān)系?
- 接收緩存區(qū)和接收滑動(dòng)窗口關(guān)系
- 發(fā)送緩存區(qū)和發(fā)送滑動(dòng)窗口關(guān)系
- 緩沖區(qū)大小預(yù)估
- 1. 獲取接收緩沖區(qū)的大小
- 2. 封裝
- SO_RCVLOWAT、SO_SNDLOWAT
- SO_BROADCAST套接字選項(xiàng)
- SO_DEBUG 套接字選項(xiàng)
- SO_DONTROUTE套接字選項(xiàng)
- SO_ERROR套接字選項(xiàng)
- SO_KEEPALIVE套接字選項(xiàng)
- SO_LINGER
- SO_ERROR
- SO_RCVTIMEO, SO_SNDTIMEO
- 示例1:設(shè)置connect超時(shí)時(shí)間
- 示例2:超時(shí)接收(服務(wù)器數(shù)據(jù))
概敘
理論
如果說fcntl系統(tǒng)調(diào)用是控制文件描述符屬性的通用POSIX方法,那么setsockopt、getsockopt就是專門用來讀取和設(shè)置socket文件描述符屬性的方法
#include <sys/socket.h> /* * 參數(shù): sockfd: 指向一個(gè)打開的套接字描述符 * level: 指定系統(tǒng)中解釋選項(xiàng)的代碼或者通用套接字,或者指定要操作哪個(gè)協(xié)議的選項(xiàng)(比如IPV4,IPV6,TCP, SCTP) * optname:指定選項(xiàng)的名字 * optval、optlen:被操作選項(xiàng)的值和長度 * setsockopt從*optval中取得選項(xiàng)待設(shè)置的新值 * getsockopt則把已獲取的選項(xiàng)當(dāng)前值放到*optval中。 * 返回值: 成功0,出錯(cuò)-1并設(shè)置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設(shè)置的選項(xiàng)。其中“數(shù)據(jù)類型”給出了指針optval必須指向的每個(gè)選項(xiàng)的數(shù)據(jù)類型,比如linger{}表示struct linger
實(shí)踐:當(dāng)前系統(tǒng)支持哪些socket選項(xiàng)
檢查上表選項(xiàng)是否得到支持,如果有,則輸出默認(rèn)值
#include <netinet/tcp.h> /* for TCP_xxx defines */ #include <stddef.h> #include <netinet/in.h> #include <stdio.h> #include <unp.h>//在Union類型中枚舉每一個(gè)getsockopt的每個(gè)可能的返回值 union val {int i_val;long l_val;struct linger linger_val;struct timeval timeval_val; } val;// 為用戶輸出給定套接字選項(xiàng)的4個(gè)函數(shù)定義原型 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結(jié)構(gòu)包含給每個(gè)套接字選項(xiàng)調(diào)用getsockopt并輸出其當(dāng)前值所需要的信息 // sock_opts數(shù)組里面的每個(gè)元素代表一個(gè)套接字選項(xiàng) 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); }通用套接字選項(xiàng)
TCP_DEFER_ACCEPT
Linux 提供的一個(gè)特殊 setsockopt , 在 accept 的 socket 上面,只有當(dāng)實(shí)際收到了數(shù)據(jù),才喚醒正在 accept 的進(jìn)程,可以減少一些無聊的上下文切換
- 經(jīng)過測試發(fā)現(xiàn),設(shè)置TCP_DEFER_ACCEPT選項(xiàng)后,服務(wù)器受到一個(gè)CONNECT請(qǐng)求后,操作系統(tǒng)不會(huì)Accept,也不會(huì)創(chuàng)建IO句柄。操作系統(tǒng)應(yīng)該在若干秒,(但肯定遠(yuǎn)遠(yuǎn)大于上面設(shè)置的1s) 后,會(huì)釋放相關(guān)的鏈接。但沒有同時(shí)關(guān)閉相應(yīng)的端口,所以客戶端會(huì)一直以為處于鏈接狀態(tài)。如果Connect后面馬上有后續(xù)的發(fā)送數(shù)據(jù),那么服務(wù)器會(huì)調(diào)用Accept接收這個(gè)鏈接端口。
- 感覺了一下,這個(gè)端口設(shè)置對(duì)于CONNECT鏈接上來而又什么都不干的攻擊方式處理很有效。我們?cè)瓉淼拇a都是先允許鏈接,然后再進(jìn)行超時(shí)處理,比他這個(gè)有點(diǎn)Out了。不過這個(gè)選項(xiàng)可能會(huì)導(dǎo)致定位某些問題麻煩。
可以通過setsockopt來設(shè)置defer accept:setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout, sizeof(timeout)) < 0
- 其中,timeout為超時(shí)時(shí)間,內(nèi)核會(huì)把此時(shí)間轉(zhuǎn)換為最大重傳次數(shù)。
- 單位是秒,注意如果打開這個(gè)功能,kernel 在 val 秒之內(nèi)還沒有收到數(shù)據(jù),不會(huì)繼續(xù)喚醒進(jìn)程,而是直接丟棄連接。
在不使用此選項(xiàng)的情況下,TCP三次握手建立連接的過程為:
- 使用此選項(xiàng)時(shí),若Client和Server完成三次握手,
- 但Client并沒有數(shù)據(jù)發(fā)來,這時(shí)Server并不會(huì)把連接的狀態(tài)置為ESTABLISHED,而是會(huì)忽略最后一次ACK,保持SYN_RECV狀態(tài)不變,應(yīng)用也就沒有機(jī)會(huì)accept這個(gè)連接,
- 這樣可以避免Client建立連接卻不發(fā)送請(qǐng)求,導(dǎo)致Server的資源浪費(fèi),
- 尤其對(duì)于像Apache這樣的應(yīng)用,接收到連接之后會(huì)一直阻塞等待Client的請(qǐng)求數(shù)據(jù),直到超時(shí),嚴(yán)重影響服務(wù)的性能和穩(wěn)定性。
然,不同的內(nèi)核版本對(duì)此選項(xiàng)的支持卻大不一樣,比如 2.6.18 和 2.6.32的行為就大不相同,對(duì)比內(nèi)核源碼:
- 2.6.18:丟棄ack的條件是設(shè)置了defer accept選項(xiàng):
- 2.6.32:丟棄ack的條件是重傳ack的次數(shù)小于在設(shè)置defer accept時(shí)指定的值(內(nèi)核會(huì)根據(jù)timeout參數(shù)的值計(jì)算最大重傳次數(shù)):
- 2.6.18:重傳SYN+ACK,若Client及時(shí)響應(yīng)ACK但沒有數(shù)據(jù)到來,Server就會(huì)定時(shí)重傳SYN+ACK,保持連接狀態(tài)SYN_RECV不變。
- 2.6.32:實(shí)際上并不會(huì)多次重傳SYN+ACK,雖然中間會(huì)定時(shí)計(jì)算是否需要重傳,但只會(huì)重傳超時(shí)時(shí)間之內(nèi)的最后一次SYN+ACK,看下面代碼,注意注釋:
- 2.6.18:由于Server端連接狀態(tài)保持為SYN_RECV,超過最大重傳次數(shù)后,Server會(huì)刪掉SYN_RECV狀態(tài)的連接,應(yīng)用也就沒有機(jī)會(huì)Accept這個(gè)新連接。Client看到的狀態(tài)仍然是ESTABLISHED,但此連接并沒有占用Server的資源。
- 2.6.32:若Client響應(yīng)最后一次重傳的SYN+ACK,由于中間會(huì)定時(shí)計(jì)算是否需要重傳并對(duì)重傳次數(shù)計(jì)數(shù)(req->restrans++),因此上面代碼中丟棄ACK的條件就不成立,Server就會(huì)把連接置為ESTABLISHED,交給應(yīng)用Accept這個(gè)新連接準(zhǔn)備讀取數(shù)據(jù)。下面的代碼顯示雖然中間不重傳SYN+ACK,但仍然計(jì)數(shù):
結(jié)論:- 2.6.18內(nèi)核:若三次握手完成后Client沒有數(shù)據(jù)發(fā)來,Server的連接狀態(tài)一直是SYN_RECV,不會(huì)Accept新連接,直到把SYN_RECN狀態(tài)的連接刪除,但Client仍然顯示ESTABLISHED。
- 2.6.32內(nèi)核:若三次握手完成后Client沒有數(shù)據(jù)發(fā)來,Server的連接狀態(tài)最終會(huì)變成ESTABLISHED,會(huì)Accept新連接,Client和Server連接的狀態(tài)均為ESTABLISHED。
至于2.6.18之后從哪個(gè)版本開始變成了2.6.32的行為,沒有考證,2.6.32及之后的版本都是同樣的處理方式。
此外,當(dāng)三次握手完成,且沒有數(shù)據(jù)到來,Server的連接狀態(tài)處于SYN_RECV狀態(tài)時(shí),如果Client發(fā)送FIN,則Server會(huì)響應(yīng)ack及后續(xù)的FIN,正常終止連接;在2.6.18上,若Server端已超時(shí),SYN_RECV狀態(tài)的連接被移除后,Client發(fā)送FIN時(shí),Server響應(yīng)RST。
相關(guān)參數(shù):
TCP_NODELAY
糊涂窗口綜合征:是指當(dāng)發(fā)送端應(yīng)用進(jìn)程產(chǎn)生數(shù)據(jù)很慢、或接收端應(yīng)用進(jìn)程處理接收緩沖區(qū)數(shù)據(jù)很慢,或二者兼而有之;就會(huì)使應(yīng)用進(jìn)程間傳送的報(bào)文段很小,特別是有效載荷很小; 極端情況下,有效載荷可能只有1個(gè)字節(jié);傳輸開銷有40字節(jié)(20字節(jié)的IP頭+20字節(jié)的TCP頭) 這種現(xiàn)象。
如果發(fā)送端為產(chǎn)生數(shù)據(jù)很慢的應(yīng)用程序服務(wù)(典型的有telnet應(yīng)用),例如,一次產(chǎn)生一個(gè)字節(jié)。這個(gè)應(yīng)用程序一次將一個(gè)字節(jié)的數(shù)據(jù)寫入發(fā)送端的TCP的緩存。如果發(fā)送端的TCP沒有特定的指令,它就產(chǎn)生只包括一個(gè)字節(jié)數(shù)據(jù)的報(bào)文段。結(jié)果有很多41字節(jié)的IP數(shù)據(jù)報(bào)就在互連網(wǎng)中傳來傳去。
解決的方法是防止發(fā)送端的TCP逐個(gè)字節(jié)地發(fā)送數(shù)據(jù)。必須強(qiáng)迫發(fā)送端的TCP收集數(shù)據(jù),然后用一個(gè)更大的數(shù)據(jù)塊來發(fā)送。發(fā)送端的TCP要等待多長時(shí)間呢?如果它等待過長,它就會(huì)使整個(gè)的過程產(chǎn)生較長的時(shí)延。如果它的等待時(shí)間不夠長,它就可能發(fā)送較小的報(bào)文段。Nagle找到了一個(gè)很好的解決方法,發(fā)明了Nagle算法。
用TCP_NODELAY選項(xiàng)可以禁止Negale 算法.
TCP_FASTOPEN
TCP建立連接需要三次握手,這個(gè)大家都知道。但是三次握手會(huì)導(dǎo)致傳輸效率下降,尤其是HTTP這種短連接的協(xié)議,雖然HTTP有keep-alive來讓一些請(qǐng)求頻繁的HTTP提高性能,避免了一些三次握手的次數(shù),但是還是希望能繞過三次握手提高效率,或者說在三次握手的同時(shí)就把數(shù)據(jù)傳輸?shù)氖虑榻o做了。TCP Fast Open(簡稱TFO)就是來干這樣的事情的
首先我們回顧一下三次握手的過程:
這里客戶端在最后ACK的時(shí)候,完全可以將想要發(fā)送的第一條數(shù)據(jù)也一起帶過去,這是TFO做的其中一個(gè)優(yōu)化方案。
然后TFO還參考了HTTP登錄態(tài)的流程,采用cookie的方案,讓客戶知道某個(gè)客戶端之前已經(jīng)登陸過了,那么它發(fā)過來的數(shù)據(jù)就可以直接接收了,不需要一開始必須三次握手再發(fā)送數(shù)據(jù)
當(dāng)客戶端第一次連接服務(wù)端時(shí),是沒有Cookie的,所以會(huì)發(fā)送一個(gè)空的Cookie,意味著要請(qǐng)求Cookie,如下圖:
這樣服務(wù)端就會(huì)將Cookie通過SYN+ACK的路徑返回給客戶端,客戶端保存后,將發(fā)送的數(shù)據(jù)三次握手的最后一步ACK同時(shí)發(fā)送給服務(wù)端。
當(dāng)客戶端斷開連接,下一次請(qǐng)求同一個(gè)服務(wù)端的時(shí)候,會(huì)帶上之前存儲(chǔ)的Cookie和要發(fā)送的數(shù)據(jù),在SYN的路徑上一起發(fā)送給服務(wù)端,如下圖:
這樣之后每次握手的時(shí)候還同時(shí)發(fā)送了數(shù)據(jù)信息,將數(shù)據(jù)傳輸提前了。服務(wù)端只要驗(yàn)證了Cookie,就會(huì)將發(fā)送的數(shù)據(jù)接收,否則會(huì)丟棄并且再通過SYN+ACK路徑返回一個(gè)新的Cookie,這種情況一般是Cookie過期導(dǎo)致的。
TFO是需要開啟的,開啟參數(shù)在:
/proc/sys/net/ipv4/tcp_fastopen 0:關(guān)閉 1:作為客戶端使用Fast Open功能,默認(rèn)值 2:作為服務(wù)端使用Fast Open功能 3:無論是客戶端還是服務(wù)端都使用Fast Open功能并且如果之前的代碼沒有做這方面的處理,也是不能使用的,從上面的流程圖就能看到,客戶端是在連接的過程就發(fā)送數(shù)據(jù),但是之前客戶端都是先調(diào)用connect成功后,才用send發(fā)送數(shù)據(jù)的。
服務(wù)端需要對(duì)listen的socket設(shè)置如下選項(xiàng):
//需要的頭文件 #include <netinet/tcp.h>int qlen = 5; //fast open 隊(duì)列 setsockopt(m_listen_socket, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));客戶端則直接使用sendto方法進(jìn)行連接和發(fā)送數(shù)據(jù),示例代碼如下:
#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);}經(jīng)過試驗(yàn),客戶端存儲(chǔ)的Cookie是跟服務(wù)端的IP綁定的,而不是跟進(jìn)程或端口綁定。當(dāng)客戶端程序發(fā)送到同一個(gè)IP但是不同端口的進(jìn)程時(shí),使用的是同一個(gè)Cookie,而且服務(wù)端也認(rèn)證成功。
SO_REUSEADDR、SO_REUSEPORT
1、SO_REUSEADDR重用處于TIME_WAIT的socket
- SO_REUSEADDR用于對(duì)TCP套接字處于TIME_WAIT狀態(tài)下的socket,才可以重復(fù)綁定使用。server程序總是應(yīng)該在調(diào)用bind()之前設(shè)置SO_REUSEADDR套接字選項(xiàng)。TCP,先調(diào)用close()的一方會(huì)進(jìn)入TIME_WAIT狀態(tài)
- 另外一個(gè)方法:通過修改內(nèi)核參數(shù)/proc/sys/net/ipv4/tcp_tw_recycle來快速回收被關(guān)閉的socket,從而使得TCP連接根據(jù)不進(jìn)入TIME_WAIT狀態(tài),進(jìn)而運(yùn)行應(yīng)用程序立即重用本地的socket地址
SO_REUSEADDR提供如下四個(gè)功能:
- SO_REUSEADDR允許啟動(dòng)一個(gè)監(jiān)聽服務(wù)器并捆綁其眾所周知的端口,即使以前建立的將此端口用作他們的本地端口的連接仍舊存在。這通常是重啟監(jiān)聽服務(wù)器時(shí)出現(xiàn),如果不設(shè)置此項(xiàng),則bind出錯(cuò)
- 假如一個(gè)systemd托管的service異常退出了,留下了TIME_WAIT狀態(tài)的socket,那么systemd將會(huì)嘗試重啟這個(gè)service。但是因?yàn)槎丝诒徽加?#xff0c;會(huì)導(dǎo)致啟動(dòng)失敗,造成兩分鐘的服務(wù)空檔期,systemd也可能在這期間放棄重啟服務(wù)。
- 但是在設(shè)置了SO_REUSEADDR以后,處于TIME_WAIT狀態(tài)的地址也可以被綁定,就杜絕了這個(gè)問題。因?yàn)門IME_WAIT其實(shí)本身就是半死狀態(tài),雖然這樣重用TIME_WAIT可能會(huì)造成不可預(yù)料的副作用,但是在現(xiàn)實(shí)中問題很少發(fā)生,所以也忽略了它的副作用。
- SO_REUSEADDR允許在同一端口上啟動(dòng)同一服務(wù)器的多個(gè)實(shí)例,只要每個(gè)實(shí)例捆綁一個(gè)不同的本地IP地址即可。對(duì)于TCP,我們根本不可能啟動(dòng)捆綁相同IP地址和相同端口號(hào)的多個(gè)服務(wù)器。
- SO_REUSEADDR允許單個(gè)進(jìn)程捆綁在同一端口的多個(gè)套接字上,只要每個(gè)捆綁指定不同的本地IP地址即可。這一般不用于TCP服務(wù)器。
- SO_REUSEADDR允許完全重復(fù)的捆綁:當(dāng)一個(gè)IP地址和端口綁定到某個(gè)套接口上時(shí),還允許此IP地址和端口捆綁到另一個(gè)套接口上。一般來說,這個(gè)特性僅在支持多播的系統(tǒng)上才有,而且只對(duì)UDP套接口而言(TCP不支持多播)
2、SO_REUSEPORT端口重用
Linux kernel 3.9之前的版本,一個(gè)ip+port組合,只能被監(jiān)聽bind一次。這樣在多核環(huán)境下,往往只能有一個(gè)線程(或者進(jìn)程)是listener,在高并發(fā)情況下,往往這就是性能瓶頸。于是Linux kernel 3.9之后,Linux推出了端口重用SO_REUSEPORT選項(xiàng)。
SO_REUSEPORT支持多個(gè)進(jìn)程或者線程綁定到同一端口,提高服務(wù)器程序的性能,解決的問題:
-
允許多個(gè)套接字 bind()/listen() 同一個(gè)TCP/UDP端口
- 每一個(gè)線程擁有自己的服務(wù)器套接字
- 在服務(wù)器套接字上沒有了鎖的競爭
-
內(nèi)核層面實(shí)現(xiàn)負(fù)載均衡
-
安全層面,監(jiān)聽同一個(gè)端口的套接字只能位于同一個(gè)用戶下面
其核心的實(shí)現(xiàn)主要有三點(diǎn):
- 擴(kuò)展 socket option,增加 SO_REUSEPORT 選項(xiàng),用來設(shè)置 reuseport。
- 修改 bind 系統(tǒng)調(diào)用實(shí)現(xiàn),以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實(shí)現(xiàn),查找 listener 的時(shí)候,能夠支持在監(jiān)聽相同 IP 和端口的多個(gè) sock 之間均衡選擇。
有了SO_RESUEPORT后,每個(gè)進(jìn)程可以自己創(chuàng)建socket、bind、listen、accept相同的地址和端口,各自是獨(dú)立平等的。讓多進(jìn)程監(jiān)聽同一個(gè)端口,各個(gè)進(jìn)程中accept socket fd不一樣,有新連接建立時(shí),內(nèi)核只會(huì)喚醒一個(gè)進(jìn)程來accept,并且保證喚醒的均衡性。
4、使用這兩個(gè)套接字選項(xiàng)的定義
- 在所有的TCP服務(wù)器上,在調(diào)用bind之前設(shè)置SO_REUSEADDR選項(xiàng)
- 當(dāng)編寫一個(gè)同一時(shí)刻在同一主機(jī)上可運(yùn)行多次的多播應(yīng)用程序時(shí),設(shè)置SO_REUSEADDR選項(xiàng),并將本組的多播地址作為本地IP地址捆綁。
SO_REUSEADDR選項(xiàng)的本質(zhì)?
這個(gè)套接字選項(xiàng)通知內(nèi)核:
- 如果端口忙,但是TCP狀態(tài)位于TIME_WAIT ,可以重用端口。
- 如果端口忙,而TCP狀態(tài)位于其他狀態(tài),重用端口時(shí)依舊得到一個(gè)錯(cuò)誤信息,指明"地址已經(jīng)使用中"。
一個(gè)套接字由相關(guān)五元組構(gòu)成,協(xié)議、本地地址、本地端口、遠(yuǎn)程地址、遠(yuǎn)程端口。SO_REUSEADDR 僅僅表示可以重用本地本地地址、本地端口,整個(gè)相關(guān)五元組還是唯一確定的。所以,重啟后的服務(wù)程序有可能收到非期望數(shù)據(jù)。必須慎重使用SO_REUSEADDR 選項(xiàng)。
SO_REUSEPORT和SO_REUSEADDR
從字面意思理解,SO_REUSEPORT是端口重用,SO_REUSEADDR是地址重用。兩者的區(qū)別:
(1)SO_REUSEPORT是允許多個(gè)socket綁定到同一個(gè)ip+port上。SO_REUSEADDR用于對(duì)TCP套接字處于TIME_WAIT狀態(tài)下的socket,才可以重復(fù)綁定使用。
(2)兩者使用場景完全不同。SO_REUSEADDR這個(gè)套接字選項(xiàng)通知內(nèi)核,如果端口忙,但TCP狀態(tài)位于TIME_WAIT,可以重用端口。這個(gè)一般用于當(dāng)你的程序停止后想立即重啟的時(shí)候,如果沒有設(shè)定這個(gè)選項(xiàng),會(huì)報(bào)錯(cuò)EADDRINUSE,需要等到TIME_WAIT結(jié)束才能重新綁定到同一個(gè)ip+port上。而SO_REUSEPORT用于多核環(huán)境下,允許多個(gè)線程或者進(jìn)程綁定和監(jiān)聽同一個(gè)ip+port,無論UDP、TCP(以及TCP是什么狀態(tài))。
(3)對(duì)于多播,兩者意義相同。
為什么要引入SO_REUSEPORT:
1、驚群效應(yīng):簡單來說就是多個(gè)進(jìn)程或者線程等待同一個(gè)事件,當(dāng)事件發(fā)生時(shí),所有進(jìn)程和線程都會(huì)被內(nèi)核喚醒。喚醒之后通常只有一個(gè)進(jìn)程獲得了該事件并進(jìn)行處理,其他進(jìn)程發(fā)現(xiàn)獲取事件失敗之后又繼續(xù)進(jìn)入了等待狀態(tài),在一定長度上降低了性能。
2、為什么驚群效應(yīng)會(huì)降低系列性能?
- 多線程/多進(jìn)程的喚醒,會(huì)進(jìn)行上下文切換。頻繁的上下文切換帶來的一個(gè)問題是數(shù)據(jù)將頻繁的在寄存器與運(yùn)行隊(duì)列中流轉(zhuǎn)。極端情況下,時(shí)間更多的小號(hào)在進(jìn)程/線程的調(diào)度上,而不是執(zhí)行
- 為了確保只有一個(gè)線程得到資源,用戶必須對(duì)資源操作進(jìn)行加鎖保護(hù),進(jìn)一步加大了系統(tǒng)開銷。
3、常見的驚群問題
在 Linux 下,我們常見的驚群效應(yīng)發(fā)生于我們使用 accept 以及我們 select 、poll 或 epoll 等系統(tǒng)提供的 API 來處理我們的網(wǎng)絡(luò)鏈接。
(1) accept 驚群:
首先我們用一個(gè)流程圖來復(fù)習(xí)下我們傳統(tǒng)的 accept 使用方式
服務(wù)器
客戶端
#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); }為什么這里沒有出現(xiàn)我們想要的現(xiàn)象(一個(gè)進(jìn)程 accept 成功,三個(gè)進(jìn)程 accept 失敗)?原因在于在 Linux 2.6 之后,Accept 的驚群問題從內(nèi)核上被處理了
(2) select/poll/epoll 驚群:
我們以 epoll 為例,我們來看看傳統(tǒng)的工作模式
服務(wù)器:
客戶端同上
怎么使用?
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: 該套接字是否是監(jiān)聽套接字(listen)
SO_SNDBUF和SO_RCVBUF
理論
含義
- SO_SNDBUF:TCP發(fā)送緩沖區(qū)的容量上限;
- SO_RCVBUF:TCP接受緩沖區(qū)的容量上限;
注意:緩沖區(qū)的上限不能無限大,如果超過內(nèi)核設(shè)置的上限值,則以內(nèi)核設(shè)置值為準(zhǔn)
$ sysctl -a net.ipv4.tcp_rmem = 8192 87380 16777216 net.ipv4.tcp_wmem = 8192 65536 16777216 net.ipv4.tcp_mem = 8388608 12582912 16777216與實(shí)際使用內(nèi)存的關(guān)系?
- SO_SNDBUF和SO_RCVBUF只是規(guī)定了讀寫緩沖區(qū)大小的上限,在實(shí)際使用未達(dá)到上限前,SO_SNDBUF和SO_RCVBUF是不起作用的。
- 一個(gè)TCP連接占用的內(nèi)存相當(dāng)于讀寫緩沖區(qū)實(shí)際占用內(nèi)存大小之和。
- 當(dāng)我們用setsockopt來設(shè)置TCP的接收緩沖區(qū)和發(fā)送緩沖區(qū)的大小時(shí),系統(tǒng)都會(huì)將其值加倍,并且不得小于某個(gè)值。這是為了確保一個(gè)TCP連接擁有足夠的空閑緩沖區(qū)來處理擁塞。
- 接收緩沖區(qū)最小值一般是256
- 發(fā)送緩沖區(qū)最小值一般是2048
- 我們也可以強(qiáng)制修改內(nèi)存參數(shù)/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem來強(qiáng)制TCP接收緩沖區(qū)和發(fā)送緩沖區(qū)的大小沒有最小值限制
與滑動(dòng)窗口的關(guān)系?
接收緩存區(qū)和接收滑動(dòng)窗口關(guān)系
接收緩存區(qū)包含了滑動(dòng)窗口,即接收緩存區(qū)大小>= 滑動(dòng)窗口大小。接受緩沖區(qū)的數(shù)據(jù)主要分為兩部分:
- 接受滑動(dòng)窗口內(nèi)的無序的TCP報(bào)文;
- 有序的,應(yīng)用還未讀取的數(shù)據(jù)(占用比例:1/(2^tcp_adv_win_scale),默認(rèn)tcp_adv_win_scale配置為2);
因此,當(dāng)接受緩沖區(qū)上限固定后,如果應(yīng)用程序讀取數(shù)據(jù)的速率過慢,接收滑動(dòng)窗口會(huì)縮小,從而通知連接的對(duì)端降低發(fā)送速度,避免無謂的網(wǎng)絡(luò)傳輸。
發(fā)送緩存區(qū)和發(fā)送滑動(dòng)窗口關(guān)系
發(fā)送緩存區(qū)包含了發(fā)送滑動(dòng)窗口,即發(fā)送緩存區(qū)大小>= 發(fā)送滑動(dòng)窗口大小。發(fā)送緩沖區(qū)的數(shù)據(jù)主要分為兩部分:
- 發(fā)送窗口內(nèi)的數(shù)據(jù):已發(fā)送還未確認(rèn)的數(shù)據(jù);
- 應(yīng)用寫入的數(shù)據(jù);
緩沖區(qū)大小預(yù)估
一般以BDP來設(shè)置最大接收窗口,BDP叫做帶寬時(shí)延積,也就是帶寬與網(wǎng)絡(luò)時(shí)延的乘積。因?yàn)锽DP就表示了網(wǎng)絡(luò)承載能力,最大接收窗口就表示了網(wǎng)絡(luò)承載能力內(nèi)可以不經(jīng)確認(rèn)發(fā)出的報(bào)文。如下圖所示:
根據(jù)接受窗口大小的占比1-1/(2^tcp_adv_win_scale),計(jì)算出緩沖區(qū)大小上限;
舉例:例如若我們的帶寬為2Gbps,時(shí)延為10ms,那么帶寬時(shí)延積BDP則為2G/8 * 0.01=2.5MB,所以這樣的網(wǎng)絡(luò)中可以設(shè)最大接收窗口為2.5MB,當(dāng)tcp_adv_win_scale=2時(shí)最大讀緩存可以設(shè)為4/3*2.5MB=3.3MB。
1. 獲取接收緩沖區(qū)的大小
#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{/*** 設(shè)置 TCP 套接字的寫緩沖區(qū)大小* @param fd {ACL_SOCKET} 套接字* @param size {int} 緩沖區(qū)設(shè)置大小*/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 套接字的寫緩沖區(qū)大小* @param fd {ACL_SOCKET} 套接字* @return {int} 緩沖區(qū)大小*/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;}/*** 設(shè)置 TCP 套接字的讀緩沖區(qū)大小* @param fd {ACL_SOCKET} 套接字* @param size {int} 緩沖區(qū)設(shè)置大小*/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 套接字的讀緩沖區(qū)大小* @param fd {ACL_SOCKET} 套接字* @return {int} 緩沖區(qū)大小*/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發(fā)送緩沖區(qū)大小—待研究
SO_RCVLOWAT、SO_SNDLOWAT
SO_RCVLOWAT、SO_SNDLOWAT分別表示TCP接收緩存和發(fā)送緩沖區(qū)的低水位標(biāo)記。他們一般被IO復(fù)用系統(tǒng)調(diào)用用來判斷socket是否可讀可寫:
- 當(dāng)TCP接收緩沖區(qū)中可讀數(shù)據(jù)的總數(shù)大于其低水位標(biāo)記時(shí),IO復(fù)用系統(tǒng)調(diào)用將通知應(yīng)用程序可以從對(duì)應(yīng)的socket上讀取數(shù)據(jù);
- 當(dāng)TCP發(fā)送緩沖區(qū)中空閑空間的大小大于其低水位標(biāo)記時(shí),IO復(fù)用系統(tǒng)調(diào)用將通知應(yīng)用程序可以往對(duì)應(yīng)的socket上寫數(shù)據(jù)
默認(rèn)情況下,TCP接收/發(fā)送緩沖區(qū)的低水位標(biāo)記均為1
SO_BROADCAST套接字選項(xiàng)
- 本選項(xiàng)開啟或禁止進(jìn)程發(fā)送廣播消息的能力。只有數(shù)據(jù)報(bào)套接字支持廣播,并且還必須是在支持廣播消息的網(wǎng)絡(luò)上(例如以太網(wǎng),令牌環(huán)網(wǎng)等)。
- 我們不可能在點(diǎn)對(duì)點(diǎn)鏈路上進(jìn)行廣播,也不可能在基于連接的傳輸協(xié)議(例如TCP和SCTP)之上進(jìn)行廣播。
SO_DEBUG 套接字選項(xiàng)
- 本選項(xiàng)僅由TCP支持。當(dāng)給一個(gè)TCP套接字開啟本選項(xiàng)時(shí),內(nèi)核將為TCP在該套接字發(fā)送和接受的所有分組保留詳細(xì)跟蹤信息。這些信息保存在內(nèi)核的某個(gè)環(huán)形緩沖區(qū)中,并可使用trpt程序進(jìn)行檢查。
SO_DONTROUTE套接字選項(xiàng)
SO_ERROR套接字選項(xiàng)
當(dāng)一個(gè)套接字上發(fā)生錯(cuò)誤時(shí),源自Berkeley的內(nèi)核中的協(xié)議模塊將該套接字的名為so_error的變量設(shè)置為標(biāo)準(zhǔn)的Unix Exxx值中的一個(gè)。我們稱它為該套接字的待處理錯(cuò)誤。
內(nèi)核能以下面兩種方式之一立即通知進(jìn)程這個(gè)錯(cuò)誤:
- 如果進(jìn)程阻塞在對(duì)該套接字的select調(diào)用上,那么無論是檢查可讀條件還是可寫條件,select均返回并設(shè)置其中一個(gè)或者所有兩個(gè)條件
- 如果進(jìn)程使用信號(hào)驅(qū)動(dòng)式IO模型,那么給進(jìn)程或者進(jìn)程組一個(gè)SIGIO信號(hào)。
進(jìn)程然后可以通過訪問SO_ERROR套接字選項(xiàng)獲取so_error的值。由getsockopt返回的整數(shù)值就是該套接字的待處理錯(cuò)誤。so_error隨后由內(nèi)核復(fù)位為0
-
當(dāng)進(jìn)程調(diào)用read:
- 沒有數(shù)據(jù)返回時(shí),那么read返回-1而且errno被設(shè)置為so_error的值(非0值), so_error設(shè)復(fù)位為0
- 如果由數(shù)據(jù)等待被讀取,那么read返回讀取到的數(shù)據(jù)。
-
如果在進(jìn)程調(diào)用write時(shí)so_error為非0值,那么wriet返回-1而且被errno被設(shè)為so_error的值(非0值), so_error設(shè)復(fù)位為0
SO_KEEPALIVE套接字選項(xiàng)
SO_KEEPALIVE 保持連接檢測對(duì)方主機(jī)是否崩潰,避免(服務(wù)器)永遠(yuǎn)阻塞于TCP連接的輸入。
設(shè)置該選項(xiàng)后,如果2小時(shí)內(nèi)在此套接口的任一方向都沒有數(shù)據(jù)交換,TCP就自動(dòng)給對(duì)方 發(fā)一個(gè)保持存活探測分節(jié)(keepalive probe)。這是一個(gè)對(duì)方必須響應(yīng)的TCP分節(jié).它會(huì)導(dǎo)致以下三種情況:
1、對(duì)方接收一切正常:以期望的ACK響應(yīng),應(yīng)用程序無動(dòng)作(因?yàn)橐磺姓?#xff09;,又經(jīng)過沒有動(dòng)靜的2小時(shí)后,TCP將發(fā)出另一個(gè)探測分節(jié)。
2、對(duì)方已崩潰且已重新啟動(dòng):以RST響應(yīng)。套接口的待處理錯(cuò)誤被置為ECONNRESET,套接 口本身則被關(guān)閉。
3、對(duì)方無任何響應(yīng):源自berkeley的TCP發(fā)送另外8個(gè)探測分節(jié),相隔75秒一個(gè),試圖得到一個(gè)響應(yīng)。在發(fā)出第一個(gè)探測分節(jié)11分鐘15秒后若仍無響應(yīng)就放棄。
- 如果該套接字沒有任何響應(yīng),套接口的待處理錯(cuò)誤被置為ETIMEOUT,套接口本身則被關(guān)閉。
- 如果該套接字收到ICMP響應(yīng),ICMP錯(cuò)誤是“host unreachable(主機(jī)不可達(dá))”,這種情況下待處理錯(cuò)誤被置為 EHOSTUNREACH。
SO_LINGER
- 作用: 指定close函數(shù)對(duì) 面對(duì)連接的協(xié)議(TCP、SCTP,但不是UDP)如何操作
- 說明:在默認(rèn)情況下,當(dāng)調(diào)用close關(guān)閉socket的使用,close會(huì)立即返回,但是,如果send buffer中還有數(shù)據(jù),系統(tǒng)會(huì)試著先把send buffer中的數(shù)據(jù)發(fā)送出去,然后close才返回。SO_LINGER選項(xiàng)則是用來修改這種默認(rèn)操作的
SO_LINGER要求在用戶進(jìn)程和內(nèi)核間傳遞如下結(jié)構(gòu):
#include <sys/socket.h> struct linger { int l_onoff //0=off, nonzero=on(開關(guān)) int l_linger //linger time(延遲時(shí)間) }(1)如果l_onoff=0,則關(guān)閉這個(gè)選項(xiàng)。采用默認(rèn)操作
(2)如果l_onoff!=0 l_linger =0:
- close調(diào)用立即返回
- TCP模塊將通過發(fā)送RST(復(fù)位)分組而不是用正常的FIN|ACK|FIN|ACK四個(gè)分組來關(guān)閉該連接。
- 至于發(fā)送緩沖區(qū)中如果有未發(fā)送完的數(shù)據(jù),則丟棄。
- 主動(dòng)關(guān)閉一方的TCP狀態(tài)則跳過TIMEWAIT,直接進(jìn)入CLOSED。
網(wǎng)上很多人想利用這一點(diǎn)來解決服務(wù)器上出現(xiàn)大量的TIMEWAIT狀態(tài)的socket的問題,但是,這并不是一個(gè)好主意,這種關(guān)閉方式的用途并不在這兒,實(shí)際用途在于服務(wù)器在應(yīng)用層的需求。
(3)如果l_onoff!=0 l_linger >0:
此時(shí)close的行為取決于兩個(gè)條件:
- 被關(guān)閉的socket對(duì)應(yīng)的TCP發(fā)送緩沖區(qū)中是否還有殘留的數(shù)據(jù)
- 被關(guān)閉的socket是阻塞的還是非阻塞的。
- 如果是阻塞的,close將等待l_linger時(shí)間,直到TCP模塊發(fā)送完殘留數(shù)據(jù)并得到對(duì)方的確認(rèn)。如果這段時(shí)間內(nèi)TCP模塊沒有確認(rèn),那么close返回將返回-1并設(shè)置errno為EWOULDBLOCK
- 如果是非阻塞的,close立即返回,我們通過返回值和error來判斷殘留數(shù)據(jù)是否已經(jīng)發(fā)送完畢
SO_ERROR
當(dāng)一個(gè)套接字上發(fā)生錯(cuò)誤時(shí),內(nèi)核協(xié)議中的協(xié)議模塊將此套接字的名為so_error的變量設(shè)為標(biāo)準(zhǔn)的Unix Exxx值中的一個(gè),我們稱它為該套接字上的待處理錯(cuò)誤(pending error)
內(nèi)核能夠以下面兩種方式之一立即通知進(jìn)程這個(gè)錯(cuò)誤:
-
如果進(jìn)程阻塞在對(duì)該套接字的select調(diào)用上,那么無論是檢查可讀還是可寫條件,select均返回并設(shè)置其中一個(gè)或者所有兩個(gè)條件
-
如果進(jìn)程使用信號(hào)驅(qū)動(dòng)式IO模型,那就給進(jìn)程或者進(jìn)程組產(chǎn)生一個(gè)SIGIO信號(hào)
進(jìn)程然后可以通過訪問SO_ERROR套接字選項(xiàng)獲取so_error的值。由getsockopt返回的整數(shù)值就是該套接字的待處理錯(cuò)誤。so_error隨后由內(nèi)核復(fù)位為0.
-
當(dāng)進(jìn)程調(diào)用read且沒有數(shù)據(jù)返回時(shí),如果so_error為非0值,那么read返回-1且errno被置為so_error的值。so_error隨后被復(fù)位為0。如果該套接字上有數(shù)據(jù)在排隊(duì)等待讀取,那么read返回那些數(shù)據(jù)而不是返回錯(cuò)誤條件。
-
如果在進(jìn)程調(diào)用write時(shí)so_error為非0值,那么write返回-1且errno被設(shè)為so_error的值。so_error隨后被復(fù)位為0。
這是一個(gè)可以獲取但不能設(shè)置的套接字選項(xiàng)。
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
- 套接字選項(xiàng)SO_RCVTIMEO: 用來設(shè)置socket接收數(shù)據(jù)的超時(shí)時(shí)間;
- 套接字選項(xiàng)SO_SNDTIMEO: 用來設(shè)置socket發(fā)送數(shù)據(jù)的超時(shí)時(shí)間;
問:一般情況下,調(diào)用accept/connect/send/secv,進(jìn)程會(huì)阻塞,但是如果對(duì)端異常,進(jìn)程可能無法正常退出等待。如何讓這些調(diào)用自動(dòng)定時(shí)退出
答:可以使用比如alarm定時(shí)器、IO復(fù)用設(shè)置定時(shí)器,還可以使用socket編程里函數(shù)級(jí)別的socket套接字選項(xiàng)SO_RCVTIMEO和SO_SNDTIMEO,僅針對(duì)于數(shù)據(jù)接收和發(fā)送相關(guān),而無需設(shè)置專門的信號(hào)捕獲函數(shù)。
能夠作用的系統(tǒng)調(diào)用包括:send、sendmsg、recv、recvmsg、accept、connect。
| send | SO_SNDTIMEO | 返回-1,設(shè)置errno為EAGAIN或EWOULDBLOCK |
| sendmsg | SO_SNDTIMEO | 返回-1,設(shè)置errno為EAGAIN或EWOULDBLOCK |
| recv | SO_RCVTIMEO | 返回-1,設(shè)置errno為EAGAIN或EWOULDBLOCK |
| recvmsg | SO_RCVTIMEO | 返回-1,設(shè)置errno為EAGAIN或EWOULDBLOCK |
| accept | SO_RCVTIMEO | 返回-1,設(shè)置errno為EAGAIN或EWOULDBLOCK |
| connect | SO_SNDTIMEO | 返回-1,設(shè)置errno為EINPROGRESS |
注意:
- EAGAIN通常和EWOULDBLOCK是同一個(gè)值;
- SO_RCVTIMEO, SO_SNDTIMEO不要求系統(tǒng)調(diào)用對(duì)應(yīng)fd是非阻塞(nonblocking)的,但是使用了該套接字選項(xiàng)的sock fd,會(huì)成為nonblocking(即使之前是blocking)的
示例1:設(shè)置connect超時(shí)時(shí)間
根據(jù)系統(tǒng)調(diào)用accept的返回值,以及errno判斷超時(shí)時(shí)間是否已到,從而決定是否開始處理超時(shí)定時(shí)任務(wù)。
客戶端程序:超時(shí)連接服務(wù)器
/*** 客戶端程序* 連接服務(wù)器,超時(shí)報(bào)錯(cuò)、返回* 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>/* 超時(shí)連接 */ 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);/* 通過選項(xiàng)SO_RCVTIMEO和SO_SNDTIMEO設(shè)置的超時(shí)時(shí)間的類型時(shí)timeval, 和select系統(tǒng)調(diào)用的超時(shí)參數(shù)類型相同 */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) {/* 超時(shí)對(duì)于errno 為EINPROGRESS. 下面條件如果成立,就可以處理定時(shí)任務(wù)了 */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; }運(yùn)行結(jié)果(隨意輸入一個(gè)服務(wù)器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調(diào)用,10秒后返回-1,并且errno設(shè)置為EINPROGRESS。
示例2:超時(shí)接收(服務(wù)器數(shù)據(jù))
服務(wù)端
監(jiān)聽本地任意IP地址,端口8001
從鍵盤輸入一行數(shù)據(jù),就發(fā)送給用戶;如果沒有數(shù)據(jù),就阻塞。
客戶端
設(shè)置10秒超時(shí),接收服務(wù)器數(shù)據(jù)。
客戶端10秒以內(nèi),接收到服務(wù)器數(shù)據(jù),則直接打印;超過10秒,就報(bào)錯(cuò)退出。
/*** 客戶端程序* 示例:超時(shí)接收服務(wù)器數(shù)據(jù),超時(shí)時(shí)間例程中設(shè)置為10秒* 編譯: $ gcc timeout_recv_client.c -o client* 運(yùn)行方式:* 如本地運(yùn)行(對(duì)應(yīng)服務(wù)器實(shí)際監(jiān)聽的IP地址和端口號(hào)) $ ./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; }客戶端運(yùn)行結(jié)果:
可以看到,超過10秒后,客戶端自動(dòng)退出程序,而不再阻塞在recv。
總結(jié)
以上是生活随笔為你收集整理的Linux C/C++编程:setsockopt、getsockopt的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 苏州市RFID客运车辆资产管理系统:RF
- 下一篇: linux的passive用法,get的