【TCP/IP網絡編程】:09套接字的多種可選項

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

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

前文關於套接字的描述僅僅是使用其默認套接字特性來進行數據通訊,這對於簡單的使用場景來講彷佛是能夠的,然而實際工做場景中的確須要配置相關套接字選項來知足一些特殊需求。下圖所示是一些經常使用的套接字可選配置選項。編程

 一些經常使用套接字可配置選項服務器

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

getsockopt & setsockoptsocket

針對上文所描述的套接字可選項,可分別經過getsockopt函數和setsockopt函數來進行讀取(Get)和設置(Set)(有些選項可能僅支持一種操做)。tcp

#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的做用(套接字類型只能在建立時決定,以後不能再更改)。ide

 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_RCVBUFpost

前文中咱們提到套接字的輸入輸出緩衝區,而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算法。

相關文章
相關標籤/搜索