1. 程式人生 > >【TCP/IP網路程式設計】:09套接字的多種可選項

【TCP/IP網路程式設計】:09套接字的多種可選項

本篇文章主要介紹了套接字的幾個常用配置選項,包括SO_SNDBUF & SO_RCVBUF、SO_REUSEADDR及TCP_NODELAY等。

套接字可選項和I/O緩衝大小

前文關於套接字的描述僅僅是使用其預設套接字特性來進行資料通訊,這對於簡單的使用場景來說似乎是可以的,然而實際工作場景中的確需要配置相關套接字選項來滿足一些特殊需求。下圖所示是一些常用的套接字可選配置選項。

 一些常用套接字可配置選項

從圖中可以看出,套接字可選項是分層的。IPPROTO_IP層可選項是IP協議相關事項,IPPROTO_TCP層可選項是TCP協議相關事項,SOL_SOCKET層是套接字相關的通用可選項。

getsockopt & setsockopt

針對上文所描述的套接字可選項,可分別通過getsockopt函式和setsockopt函式來進行讀取(Get)和設定(Set)(有些選項可能僅支援一種操作)。

#include <sys/socket.h>

//Get option
int getsockopt(int sock, int level, int optname, void *optval, socklen_t  *optlen);
    -> 成功時返回0,失敗時返回-1

//Set option
int setsockopt(int sock, int level, int optname, void *optval, socklen_t  optlen);
    -> 成功時返回0,失敗時返回-1

下面示例原始碼給出了getsockopt函式的使用方法,同時也展示了只讀套接字選項SO_TYPE的作用(套接字型別只能在建立時決定,之後不能再更改)。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/socket.h>
 5 void error_handling(char *message);
 6 
 7 int main(int argc, char *argv[]) 
 8 {
 9     int tcp_sock, udp_sock;
10     int sock_type;
11     socklen_t optlen;
12     int state;
13     
14     optlen=sizeof(sock_type);
15     tcp_sock=socket(PF_INET, SOCK_STREAM, 0);
16     udp_sock=socket(PF_INET, SOCK_DGRAM, 0);    
17     printf("SOCK_STREAM: %d \n", SOCK_STREAM);
18     printf("SOCK_DGRAM: %d \n", SOCK_DGRAM);
19     
20     state=getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
21     if(state)
22         error_handling("getsockopt() error!");
23     printf("Socket type one: %d \n", sock_type);
24     
25     state=getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
26     if(state)
27         error_handling("getsockopt() error!");
28     printf("Socket type two: %d \n", sock_type);
29     return 0;
30 }
31 
32 void error_handling(char *message)
33 {
34     fputs(message, stderr);
35     fputc('\n', stderr);
36     exit(1);
37 }
sock_type

 執行結果

SO_SNDBUF & SO_RCVBUF

前文中我們提到套接字的輸入輸出緩衝區,而SO_SNDBUF 和SO_RCVBUF便是與套接字緩衝區大小相關的兩個可選項。通過這兩個選項我們可以獲取當前套接字的輸入輸出緩衝區大小,抑或設定相應緩衝區的大小。如下是這兩個選項使用的相關示例程式碼。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/socket.h>
 5 void error_handling(char *message);
 6 
 7 int main(int argc, char *argv[])
 8 {
 9     int sock;  
10     int snd_buf, rcv_buf, state;
11     socklen_t len;
12     
13     sock=socket(PF_INET, SOCK_STREAM, 0);    
14     len=sizeof(snd_buf);
15     state=getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
16     if(state)
17         error_handling("getsockopt() error");
18     
19     len=sizeof(rcv_buf);
20     state=getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
21     if(state)
22         error_handling("getsockopt() error");
23     
24     printf("Input buffer size: %d \n", rcv_buf);
25     printf("Outupt buffer size: %d \n", snd_buf);
26     return 0;
27 }
28 
29 void error_handling(char *message)
30 {
31     fputs(message, stderr);
32     fputc('\n', stderr);
33     exit(1);
34 }
get_buf
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/socket.h>
 5 void error_handling(char *message);
 6 
 7 int main(int argc, char *argv[])
 8 {
 9     int sock;
10     int snd_buf=1024*3, rcv_buf=1024*3;
11     int state;
12     socklen_t len;
13     
14     sock=socket(PF_INET, SOCK_STREAM, 0);
15     state=setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
16     if(state)
17         error_handling("setsockopt() error!");
18     
19     state=setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
20     if(state)
21         error_handling("setsockopt() error!");
22     
23     len=sizeof(snd_buf);
24     state=getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
25     if(state)
26         error_handling("getsockopt() error!");
27     
28     len=sizeof(rcv_buf);
29     state=getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
30     if(state)
31         error_handling("getsockopt() error!");
32     
33     printf("Input buffer size: %d \n", rcv_buf);
34     printf("Output buffer size: %d \n", snd_buf);
35     return 0;
36 }
37 
38 void error_handling(char *message)
39 {
40     fputs(message, stderr);
41     fputc('\n', stderr);
42     exit(1);
43 }
44 /*
45 root@com:/home/swyoon/tcpip# gcc get_buf.c -o getbuf
46 root@com:/home/swyoon/tcpip# gcc set_buf.c -o setbuf
47 root@com:/home/swyoon/tcpip# ./setbuf
48 Input buffer size: 2000 
49 Output buffer size: 2048 
50 */
set_buf

執行結果

從執行結果可以看出,對於緩衝大小的設定並非完全生效。實際上這些設定只是傳遞了我們的要求,而最終的生效值作業系統會根據當前環境做出設定,不過配置值的大小趨勢和我們期望的一致。

SO_REUSEADDR

發生地址繫結錯誤(Binding Error)

回顧之前的文章“【TCP/IP網路程式設計】:04基於TCP的伺服器端/客戶端”,我們介紹了回聲伺服器端/客戶端的實現。其中伺服器端程式碼稍作改變如下。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define TRUE 1
 9 #define FALSE 0
10 void error_handling(char *message);
11 
12 int main(int argc, char *argv[])
13 {
14     int serv_sock, clnt_sock;
15     char message[30];
16     int option, str_len;
17     socklen_t optlen, clnt_adr_sz;
18     struct sockaddr_in serv_adr, clnt_adr;
19     
20     if(argc!=2) {
21         printf("Usage : %s <port>\n", argv[0]);
22         exit(1);
23     }
24     
25     serv_sock=socket(PF_INET, SOCK_STREAM, 0);
26     if(serv_sock==-1)
27         error_handling("socket() error");
28     /*
29     optlen=sizeof(option);
30     option=TRUE;    
31     setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
32     */
33 
34     memset(&serv_adr, 0, sizeof(serv_adr));
35     serv_adr.sin_family=AF_INET;
36     serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
37     serv_adr.sin_port=htons(atoi(argv[1]));
38 
39     if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)))
40         error_handling("bind() error ");
41     
42     if(listen(serv_sock, 5)==-1)
43         error_handling("listen error");
44     clnt_adr_sz=sizeof(clnt_adr);    
45     clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
46 
47     while((str_len=read(clnt_sock,message, sizeof(message)))!= 0)
48     {
49         write(clnt_sock, message, str_len);
50         write(1, message, str_len);
51     }
52     close(clnt_sock);
53     return 0;
54 }
55 
56 void error_handling(char *message)
57 {
58     fputs(message, stderr);
59     fputc('\n', stderr);
60     exit(1);
61 }
reuseaddr_server

客戶端通過輸入“Q”訊息,或是通過CTRL+C終止程式,兩種方式客戶端都會執行close函式向伺服器端傳遞EOF訊息結束標誌。伺服器端收到EOF訊息,也可以正常退出程式。現在考慮另一種情況,如果伺服器端和客戶端在已建立連線的狀態下,向伺服器端執行CTRL+C終止程式,會發生什麼?

這種情況,伺服器端會主動向客戶端傳送FIN訊息斷開連線並退出程式。此時,如果再次以相同埠號啟動伺服器端則會發生錯誤(bind()報錯:“Address already in use”),通常需要等待1~4分鐘才能再次執行伺服器端。客戶端主動傳送FIN訊息斷開連線,不影響客戶端或伺服器端的再次執行;而伺服器端主動傳送FIN訊息斷開連線,則會影響伺服器端的再次執行,為什麼會出現這種現象呢?

TIME_WAIT狀態

TIME_WAIT狀態下的套接字

上圖展示的就是前文有提到過的四次握手斷開連線的過程。從圖中可以看出,主動斷開連線的主機(先發送FIN訊息)會經過TIME_WAIT的狀態,持續時間為2MSL(Maximum Segment Lifetime,最長分節生命期,30s或2min)。而處於TIME_WAIT狀態時,相應的埠號是正在使用狀態,因此,若伺服器端先斷開連線則無法立即重新執行。與伺服器端不同,客戶端由於每次執行都會動態分配埠號,因此不受TIME_WAIT狀態的影響。

原來是TIME_WAIT的作怪,導致主動斷開連線的伺服器端不能立即以相同的埠號重新執行。既然對伺服器端有這種影響,那為什麼要有TIME_WAIT狀態呢?(以下描述主要摘錄自UNP,以客戶端先發送FIN訊息斷開連線為例)

TIME_WAIT狀態的存在有兩個理由:

  • 可靠地實現TCP全雙工連線的終止
  • 允許老的重複分節在網路中消逝

第一個理由可以假設上述四次握手過程最終的ACK丟失了來解釋。主機B將重新發送它的最終那個FIN,因此主機A必須維護狀態資訊,以允許它重新發送最終那個ACK。如果主機A不維護狀態資訊,它將以一個RST(另外一種型別的TCP分節)訊息來響應,該分節將被主機B解釋為一個錯誤訊息。如果TCP打算執行所有必要的工作以徹底終止某個連線上兩個方向的資料流(即全雙工關閉),那麼它必須正確處理連線終止序列4個分節中任何一個分節丟失的情況。本例也說明了為什麼執行主動關閉的那一端需要處於TIME_WAIT狀態,因為它可能不得不重傳最終那個ACK。

為理解存在TIME_WAIT狀態的第二個理由,我們假設在12.106.32.254的1500埠和206.168.112.219的21埠之間有一個TCP連線。我們關閉這個連線,過一段時間後在相同的IP地址和埠之間建立另一個連線。後一個連線稱為前一個連線的化身(incarnation),因為它們的IP地址和埠號都相同。TCP必須防止來自某個連線的老的重複分組在該連線已終止後再現,從而被誤解成屬於同一連線的某個新的化身。為做到這一點,TCP將不給處於TIME_WAIT狀態的連線發起新的化身。既然TIME_WAIT狀態的持續時間是MSL的2倍,這就足以讓某個方向上的分組最多存活MSL秒即被丟棄,另一個方向上的應答最多存活MSL秒也被丟棄。通過實施這個規則,我們就能保證每成功建立一個TCP連線時,來自該連線先前化身的老的重複分組都已在網路中消逝了(單向傳輸一個分節的最長生命週期是MSL,TIME-WAIT狀態的2MSL是考慮了一次雙向資訊互動的最長時間。比如最後的ACK丟失後,來自對端重發的FIN訊息也會在2MSL內消逝)。

地址再分配

從上文的描述來看,TIME_WAIT狀態在可靠通訊過程中似乎起到了重要的作用,但它也有其自身的缺點。比如下圖的情況,收到FIN訊息的主機A傳送ACK訊息至主機B並啟動Time-wait定時器,如果網路狀態不好致使ACK訊息不斷丟失,則TIME-WAIT狀態可能一直持續下去。

 重啟Time-wait定時器

另一種情況,考慮正在工作中的伺服器突然故障停機而需要快速重啟,這時由於TIME_WAIT狀態則必須等幾分鐘,也會帶來嚴重的影響(時間就是money)。

針對以上TIME_WAIT狀態所帶來的影響,可以通過配置可選項SO_REUSEADDR來解決。預設情況下,SO_REUSEADDR選項處於關閉狀態(值為0,假),即無法分配處於TIME_WAIT狀態下套接字埠。因此,我們需要將該選項置為1(真)即可。

int opt_val = 1;    
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));

SO_REUSEADDR可選項有效解決了以上問題,UNP中也有這麼一句描述“所有TCP伺服器都應該指定本套接字選項,以允許伺服器在這種情形下被重新啟動”。同時我們也應該意識到SO_REUSEADDR其實無視了TIME_WAIT狀態的一些作用,此時如果收到一些不期望的資料(舊連線的分片)可能會導致服務程式混亂,不過這種可能性極低。

TCP_NODELAY

Nagle演算法

Nagle演算法的出現是為了防止因資料包過多而導致的網路過載,它應用於TCP層,其作用如下圖所示。

 Nagle演算法

不難看出,只有收到ACK訊息,Nagle演算法才會傳送下一資料。TCP套接字預設使用Nagle演算法,因此可以最大限度地進行緩衝,直到收到ACK。上圖的演示中,使用Nagle演算法傳送一個字串訊息需要傳遞4個數據包,而不使用Nagle演算法則需要傳遞10個數據包,對網路流量(Traffic,網路負載或混雜程度)產生了較大的影響。當然,上圖的演示只是一種極端的情況(特定場景下,字串中的字元需要間隔一定的時間來傳輸至緩衝區),實際程式中將字串傳輸至緩衝區並非逐個字元進行的。

根據資料傳輸的特性,網路流量未受太大影響時,不使用Nagle演算法反而更快。典型的場景就是“傳輸大檔案資料”,此時即使不使用Nagle演算法,也會在填滿緩衝區時傳輸資料。這種情況並沒有增加資料包的數量,反而由於無需等待ACK而可以連續傳輸,大大提高了傳輸速度。禁止Nagle演算法的方法如下。

int opt_val = 1;    
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt_val, sizeof(opt_val));

是否使用Nagle演算法,需要根據使用與否對網路流量影響的差別大小確定。通常情況,不使用Nagle演算法確實可以獲得更快的傳輸速度。但為了保證網路流量,在未準確判斷資料特性時不應該禁止Nagle算