套接字(Socket)編程(三) 套接字可選項(xiàng)

套接字具有多種特性,這些特性可通過可選項(xiàng)更改芳室,本篇文章將介紹更改套接字可選項(xiàng)的方法,并以此為基礎(chǔ)進(jìn)一步觀察套接字內(nèi)部

前面介紹了套接字通信的基本函數(shù)套接字(Socket)編程(一) 函數(shù)概念篇和套接字通信的基本原理接字(Socket)編程(二) 內(nèi)部通信原理坝冕,有需要了解的可以看我前面的兩篇文章梨树。

我們之前寫程序都是在創(chuàng)建好套接字之后(未經(jīng)特殊操作)直接使用的,此時(shí)通過默認(rèn)的套接字特性進(jìn)行同性。之前的示例都較為簡(jiǎn)單纽帖,無需特別操作套接字特性宠漩,但有時(shí)的確需要更改。

一抛计、可設(shè)置套接字的多種可選項(xiàng)

套接字可選項(xiàng)是分層的哄孤,不同的協(xié)議成可設(shè)置的套接字可選項(xiàng)是不一樣的

協(xié)議層 功能
SOL_SOCKET 套接字相關(guān)通用可選項(xiàng)的設(shè)置
IPPROTO_IP 在IP層設(shè)置套接字的相關(guān)屬性
IPPROTO_TCP 在TCP層設(shè)置套接字相關(guān)屬性

下面我們列出套接字三個(gè)協(xié)議層部分可選項(xiàng),并對(duì)其中常用的做下介紹和使用示例

SOL_SOCKET 選項(xiàng)名 說明 數(shù)據(jù)類型
SO_DEBUG 打開或關(guān)閉調(diào)試信息 int
SO_BROADCAST 允許或禁止發(fā)送廣播數(shù)據(jù) int
SO_DONTROUTE 打開或關(guān)閉路由查找功能 int
SO_ERROR 獲得套接字錯(cuò)誤 int
SO_KEEPALIVE 開啟套接字贝到兀活機(jī)制 int
SO_REUSEADDR 是否啟用地址再分配,主要原理是操作關(guān)閉套接字的Time-wait時(shí)間等待的開啟和關(guān)閉 int
SO_LINGER 是否開啟延時(shí)關(guān)閉凝危,開啟的情況下調(diào)用 close() 函數(shù)會(huì)被阻塞波俄,同時(shí)可以設(shè)置延遲關(guān)閉的超時(shí)時(shí)間,如果到超時(shí)未發(fā)送完數(shù)據(jù)則直接復(fù)位套接口的虛電路(屬于異常關(guān)閉)蛾默,如果超時(shí)時(shí)間內(nèi)發(fā)送完數(shù)據(jù)則正常關(guān)閉套接字回調(diào) close()函數(shù) struct linger
SO_TYPE 獲得套接字類型(這個(gè)只能獲取懦铺,不能設(shè)置) int
SO_RCVBUF 接收緩沖區(qū)大小 int
SO_SNDBUF 發(fā)送緩沖區(qū)大小 int
SO_RCVLOWAT 接收緩沖區(qū)下限 int
SO_SNDLOWAT 發(fā)送緩沖區(qū)下限 int
SO_RCVTIMEO 接收超時(shí) struct timeval
SO_SNDTIMEO 發(fā)送超時(shí) struct timeval
IPPROTO_IP 選項(xiàng)名 說明 數(shù)據(jù)類型
IP_MULTICAST_TTL 生存時(shí)間(Time To Live),組播傳送距離 int
IP_ADD_MEMBERSHIP 加入組播 int
IP_DROP_MEMBERSHIP 離開組播 int
IP_MULTICAST_IF 取默認(rèn)接口或默認(rèn)設(shè)置 int
IP_MULTICAST_LOOP 禁止組播數(shù)據(jù)回送 int
IP_HDRINCL 在數(shù)據(jù)包中包含IP首部 int
IP_OPTINOS IP首部選項(xiàng) int
IPPROTO_TCP 選項(xiàng)名 說明 數(shù)據(jù)類型
TCP_KEEPIDLE TCP_KEEPALIVE TCP敝ЪΓ活機(jī)制開啟下冬念,設(shè)置保活包空閑發(fā)送時(shí)間間隔 int
TCP_KEEPINTVL TCP蹦琳酰活機(jī)制開啟下急前,設(shè)置保活包無響應(yīng)情況下重發(fā)時(shí)間間隔 int
TCP_KEEPCNT TCP逼俟梗活機(jī)制開啟下裆针,設(shè)置保活包無響應(yīng)情況下重復(fù)發(fā)送次數(shù) int
TCP_MAXSEG TCP最大數(shù)據(jù)段的大小 int
TCP_NODELAY 不使用Nagle算法 int

二寺晌、getsockopt & setsockopt

我們幾乎可以對(duì)照上面所有可選項(xiàng)進(jìn)行讀取和設(shè)置(當(dāng)然有些套接字只能進(jìn)行其中的一中操作)世吨,可選項(xiàng)的讀取和設(shè)置通過下面兩個(gè)函數(shù)完成

#include <sys/socket.h>

int getsockopt(int sock,          //用于查看選項(xiàng)套接字描述符
               int level,         //要查看可選項(xiàng)的協(xié)議層
               int optname,       //要查看可選項(xiàng)名字
               void *optval,      //保存查看結(jié)果的緩沖地址值
               socklen_t *optlen  //指向第四個(gè)參數(shù)optval傳遞的緩沖大小的指針
               )
//成功時(shí)返回0,失敗返回-1
#include <sys/socket.h>

int setsockopt(int sock,          //用于更改可選項(xiàng)套接字描述符
               int level,         //要更改可選項(xiàng)的協(xié)議層
               int optname,       //要更改可選項(xiàng)名字
               void *optval,      //要更改可選項(xiàng)的值
               socklen_t optlen   //第四個(gè)參數(shù)optval傳遞的可選項(xiàng)信息的字節(jié)數(shù)
               )
//成功時(shí)返回0呻征,失敗返回-1

三耘婚、應(yīng)用舉例

下面來介紹有關(guān)可選項(xiàng)的設(shè)置讀取和示例,我將從我認(rèn)為重要的往下列舉

1. SO_KEEPALIVE

SO_KEEPALIVE 是否開啟TCP的甭礁常活機(jī)制
平時(shí)開發(fā)中沐祷,常見的對(duì)于面向連接的TCP連接,斷開分兩種情況
①奏甫、連接正常關(guān)閉戈轿,調(diào)用 close() 會(huì)經(jīng)歷斷開的四次握手,send()阵子、recv()立馬返回錯(cuò)誤;
②思杯、連接的對(duì)端異常關(guān)閉,比如網(wǎng)絡(luò)斷掉或突然斷電,這種斷開對(duì)方是檢測(cè)不到連接出現(xiàn)異常的

解決這種異常斷開的檢測(cè)機(jī)制一般有兩種:
①色乾、自己在應(yīng)用層定時(shí)發(fā)送心跳包來判斷連接是否正常誊册,此方法比較通用,靈活可控暖璧,但改變了現(xiàn)有的協(xié)議;
②案怯、使用TCP的keepalive機(jī)制,TCP協(xié)議自帶的迸彀欤活功能嘲碱,使用起來簡(jiǎn)單,減少了應(yīng)用層代碼的復(fù)雜度局蚀, 推測(cè)也會(huì)更節(jié)省流量麦锯,因?yàn)橐话銇碚f應(yīng)用層的數(shù)據(jù)傳輸?shù)絽f(xié)議層時(shí)都會(huì)被加上額外的包頭包尾,由TCP協(xié)議提供的檢活琅绅,其發(fā)的探測(cè)包扶欣,理論上實(shí)現(xiàn)的會(huì)更精妙(用更少的字節(jié)完成更多的目標(biāo)),耗費(fèi)更少的流量千扶;

keepalive原理:TCP內(nèi)嵌有心跳包,以服務(wù)端為例料祠,當(dāng)server檢測(cè)到超過一定時(shí)間(tcp_keepalive_time 7200s 即2小時(shí))沒有數(shù)據(jù)傳輸,那么會(huì)向client端發(fā)送一個(gè)keepalive packet澎羞,此時(shí)client端有三種反應(yīng):

  • client端連接正常髓绽,返回一個(gè)ACK,server端收到ACK后重置計(jì)時(shí)器煤痕,在2小時(shí)后在發(fā)送探測(cè)(如果2小時(shí)內(nèi)連接上有數(shù)據(jù)傳輸,那么在該時(shí)間的基礎(chǔ)上向后推延2小時(shí)發(fā)送探測(cè)包)
  • 客戶端異常關(guān)閉或網(wǎng)絡(luò)斷開梧宫,client無響應(yīng),server收不到ACK摆碉,在一定時(shí)間(tcp_keepalive_intvl 75 即75秒)后重發(fā)keepalive packet塘匣, 并且重發(fā)一定次數(shù)(tcp_keepalive_probes 9 即9次),仍然得不到相應(yīng)則返回超時(shí)
  • 客戶端曾經(jīng)崩潰巷帝,但已經(jīng)重啟忌卤,server收到的探測(cè)響應(yīng)是一個(gè)復(fù)位,server端終止連接

我們可以根據(jù)自己的需求來修改這三個(gè)參數(shù)的系統(tǒng)默認(rèn)值楞泼,下面我們來通過一個(gè)簡(jiǎn)單的回響服務(wù)器代碼來實(shí)現(xiàn)背刍玻活機(jī)制的邏輯實(shí)現(xiàn)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>

//這個(gè)頭文件里面包含設(shè)置TCP協(xié)議層保活三個(gè)參數(shù)的可選項(xiàng)
#include <netinet/tcp.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int serv_sockfd, clnt_sockfd;
        struct sockaddr_in serv_addr,clnt_addr;
        
        serv_addr.sin_len = sizeof(struct sockaddr_in);
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(2000);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        
        serv_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (bind(serv_sockfd, (struct sockaddr*)&serv_addr, serv_addr.sin_len) < 0) return 0;
        if (listen(serv_sockfd, 1) < 0) return 0;
        
        socklen_t len_t = sizeof(struct sockaddr_in);
        clnt_addr.sin_len = len_t;
        clnt_sockfd = accept(serv_sockfd, (struct sockaddr*)&clnt_addr, &len_t);
        if (clnt_sockfd < 0) return 0;
        close(serv_sockfd);
        printf("有客戶端連接 IP:%s Port:%d\n",inet_ntoa(clnt_addr.sin_addr),ntohs(clnt_addr.sin_port));
        
        //1. 開啟keepalive機(jī)制
        int keepAlive = 1; // 開啟keepalive屬性. 缺省值: 0(關(guān)閉)
        if (setsockopt(clnt_sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(keepAlive)) == -1)
            printf("開啟keepalive機(jī)制失敗 errorCode:%d descreption:%s\n",errno,strerror(errno));
        
        //2. 設(shè)置倍槔活包檢測(cè)發(fā)送時(shí)間間隔(TCP_KEEPIDLE 好像被替換為 TCP_KEEPALIVE)
        int keepIdle = 10; // 如果在60秒內(nèi)沒有任何數(shù)據(jù)交互,則進(jìn)行探測(cè). 缺省值:7200(s)
        if (setsockopt(clnt_sockfd, IPPROTO_TCP, TCP_KEEPALIVE, &keepIdle, sizeof(keepIdle)) == -1)
            printf("設(shè)置惫鞒В活包檢測(cè)發(fā)送時(shí)間間隔 errorCode:%d descreption:%s\n",errno,strerror(errno));
        
        //3. 設(shè)置保活包發(fā)送無響應(yīng)重發(fā)時(shí)間間隔
        int keepInterval = 5; // 探測(cè)時(shí)發(fā)探測(cè)包的時(shí)間間隔為5秒. 缺省值:75(s)
        if (setsockopt(clnt_sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(keepInterval)) == -1)
            printf("設(shè)置背剑活包發(fā)送無響應(yīng)重發(fā)時(shí)間間隔 errorCode:%d descreption:%s\n",errno,strerror(errno));
        //4. 設(shè)置蔽活包發(fā)送無響應(yīng)重發(fā)次數(shù)
        int keepCount = 3; // 探測(cè)重試的次數(shù). 全部超時(shí)則認(rèn)定連接失效..缺省值:9(次)
        if (setsockopt(clnt_sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(keepCount)) == -1)
            printf("設(shè)置保活包發(fā)送無響應(yīng)重發(fā)次數(shù) errorCode:%d descreption:%s\n",errno,strerror(errno));
        
        char recv_buffer[512];
        while (1) {
            memset(recv_buffer, 0, 512);
            ssize_t recv_len = recv(clnt_sockfd, recv_buffer, sizeof(recv_buffer), 0);
            if (recv_len == 0) {
                printf("收到數(shù)據(jù)包長(zhǎng)度為0,可能是EOF包 errorCode:%d descreption:%s\n",errno,strerror(errno));
                break;
            }else if (recv_len < 0) {
                printf("讀取數(shù)據(jù)出錯(cuò) errorCode:%d descreption:%s\n",errno,strerror(errno));
                break;
            }else {
                printf("recv: %s\n",recv_buffer);
                ssize_t sendLen = send(clnt_sockfd, recv_buffer, recv_len, 0);
                if (sendLen < 0) {
                    printf("發(fā)送失敗 errorCode:%d description:%s\n",errno,strerror(errno));
                }else {
                    printf("send: %s\n",recv_buffer);
                }
            }
        }
        printf("服務(wù)器關(guān)閉\n");
        close(clnt_sockfd);
    }
    return 0;
}

對(duì)于運(yùn)行結(jié)果的總結(jié):
上面示例設(shè)置
TCP_KEEPALIVE :設(shè)置闭牌活包空閑發(fā)送時(shí)間間隔為 10s
TCP_KEEPINTVL:設(shè)置本活包無響應(yīng)情況下重發(fā)時(shí)間間隔為 5s
TCP_KEEPCNT:設(shè)置保活包無響應(yīng)情況下重復(fù)發(fā)送次數(shù)為 3次
計(jì)算出來航攒,如果對(duì)方TCP連接異常磺陡,服務(wù)器這邊可以在 10+5*3 = 25 大概25s左右的時(shí)間內(nèi)檢測(cè)出異常,并在讀取數(shù)據(jù)時(shí)拋出errno為60原因?yàn)镺peration timed out的錯(cuò)誤

2. SO_REUSEADDR

SO_REUSEADDR 是否啟用地址再分配漠畜,設(shè)置這個(gè)可選項(xiàng)將影響套接字關(guān)閉的Time-wait狀態(tài)币他,還是比較重要的一個(gè)可選項(xiàng)。
在第二篇文章接字(Socket)編程(二) 內(nèi)部通信原理里面有講到套接字關(guān)閉時(shí)的四次握手原理盆驹,從圖中看出主動(dòng)關(guān)閉的一方在接收到對(duì)方的FIN包后有一個(gè)ACK確認(rèn)圆丹,然后進(jìn)入Time-wait狀態(tài),當(dāng)時(shí)講到這個(gè)Time-wait等待狀態(tài)主要的為了確認(rèn)對(duì)方已經(jīng)收到我們最后一個(gè)ACK確認(rèn)消息躯喇,確認(rèn)收到則關(guān)閉,沒有或者超時(shí)則重發(fā)最后的確認(rèn)包硝枉,上篇文章也詳細(xì)的介紹了為什么有這個(gè)等待狀態(tài)廉丽,這里就不做過多的重說了,需要了解的可以點(diǎn)擊前面連接查看妻味。
PS:這個(gè)Wait-time狀態(tài)只發(fā)生在主動(dòng)斷開連接的一方正压,被斷開一方是沒有這個(gè)狀態(tài)的。
作為服務(wù)器责球,通常都是客戶端主動(dòng)斷開焦履,這個(gè)時(shí)候沒有Wait-time狀態(tài),所以不會(huì)發(fā)生特別的事情雏逾,重啟也不成問題嘉裤,但有的時(shí)候由于自己異常需要重啟,自己作為斷開的一方栖博,系統(tǒng)對(duì)于這個(gè)Socket斷開有Wait-time狀態(tài)屑宠,重新運(yùn)行起來綁定相同的地址和端口就會(huì)出現(xiàn)問題,系統(tǒng)會(huì)返回“bind error”的錯(cuò)誤仇让,因?yàn)檫@個(gè)端口正在被使用典奉,這個(gè)時(shí)候需要Time-wait時(shí)間用完,大概2分鐘丧叽,再運(yùn)行服務(wù)器就可以綁定該端口了卫玖!
但是一般服務(wù)器是不能等這個(gè)時(shí)間的,有用戶在那重啟一次可能已經(jīng)造成損失踊淳,再停個(gè)兩分鐘損失更大假瞬,這個(gè)時(shí)候就需要設(shè)置套接字可選項(xiàng)SO_REUSEADDR值為1,去掉最后的Wait-time時(shí)間。

#include <sys/socket.h>

int enableReuseAddr(int sock)
{
    int optval = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
        return 0;
    }
    return 1;
}
3. SO_LINGER

SO_LINGER 此選項(xiàng)從字面意思上來看就是延遲關(guān)閉笨触,我們可以這么理解懦傍,在調(diào)用關(guān)閉套接字的情況下,設(shè)置了此參數(shù)就可以讓系統(tǒng)盡量把緩沖區(qū)的數(shù)據(jù)發(fā)送出去(因?yàn)橛谐瑫r(shí)時(shí)間)芦劣,但是對(duì)于此參數(shù)的設(shè)置分為四種情況粗俱,下面我們來列舉下
①. 設(shè)置結(jié)構(gòu)體里面參數(shù) l_onoff=0 時(shí),為內(nèi)核缺失情況(系統(tǒng)默認(rèn)情況)虚吟,關(guān)閉套接字時(shí)系統(tǒng)正常調(diào)用 close() 函數(shù)
②. 設(shè)置結(jié)構(gòu)體里面參數(shù) l_onoff=1 非0時(shí)寸认,但是參數(shù) l_linger=0超時(shí)時(shí)間為0時(shí),在關(guān)閉套接字時(shí)會(huì)向?qū)Ψ桨l(fā)送RST信號(hào)給對(duì)方(非正常的四次握手關(guān)閉)串慰,對(duì)方會(huì)讀取到Socket重置的錯(cuò)誤(errno = 45 \* Connection reset by peer *\)
③. 設(shè)置結(jié)構(gòu)體里面參數(shù) l_onoff=1 非0時(shí)偏塞,但是參數(shù) l_linger設(shè)置的超時(shí)時(shí)間太小,系統(tǒng)在規(guī)定的超時(shí)時(shí)間內(nèi)沒有將全部的數(shù)據(jù)發(fā)送出去邦鲫,這個(gè)時(shí)候情況和上面第二種情況 l_linger=0 時(shí)是一樣的灸叼,直接重置Socket向?qū)Ψ桨l(fā)送RST信號(hào)(非正常的四次握手關(guān)閉)
④. 設(shè)置結(jié)構(gòu)體里面參數(shù) l_onoff=1 非0時(shí),參數(shù) l_linger設(shè)置的超時(shí)時(shí)間足夠庆捺,在發(fā)送完全部數(shù)據(jù)后系統(tǒng)正常調(diào)用 close() 函數(shù)正常關(guān)閉

#include <sys/socket.h>

int enableLinger(int sock)
{
    struct linger so_linger;
    so_linger.l_onoff = 1;  //開啟(非0) 關(guān)閉(0)
    so_linger.l_linger = 5; //滯留時(shí)間古今,設(shè)置為大于0時(shí)才起作用
    if (setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)) == -1) {
        return 0;
    }
    return 1;
}

套接字關(guān)閉(主要針對(duì)TCP套接字?jǐn)嚅_)
可能通過上面的介紹,我們理解起來還是感覺哪不對(duì)勁滔以,到底Socket關(guān)閉都經(jīng)歷了什么捉腥?套接字的幾種關(guān)閉方式有什么區(qū)別?設(shè)置超時(shí)和不超時(shí)有什么區(qū)別你画?貌似直接調(diào)用 close() 函數(shù)抵碟,系統(tǒng)也是將數(shù)據(jù)全部發(fā)送給對(duì)方后才關(guān)閉....帶著這些疑問,我們來斷下面的分析

①. cloes函數(shù)關(guān)閉:我們最常用的關(guān)閉套接字方式坏匪,說到這種關(guān)閉我們?cè)俅尾坏貌惶嵯露鄠€(gè)進(jìn)程使用同一個(gè)套接字(我看過一本有關(guān)網(wǎng)絡(luò)編程的書拟逮,里面提到fork出子進(jìn)程來使用同一個(gè)套接字,章標(biāo)題叫編寫多進(jìn)程服務(wù)器端剥槐,書名叫《TCP/IP網(wǎng)絡(luò)編程》是韓國人寫的)唱歧,當(dāng)多個(gè)進(jìn)程使用同一個(gè)Socket時(shí),其中某一個(gè)進(jìn)程調(diào)用 close() 函數(shù)只是使套接字的引用計(jì)數(shù) -1 粒竖,此時(shí)該進(jìn)程無法使用該Socket進(jìn)行讀寫操作颅崩,其他使用該Socket的進(jìn)程可以進(jìn)行正常通信,當(dāng)引用計(jì)數(shù)減到0時(shí)蕊苗,系統(tǒng)才會(huì)開始釋放該套接字沿后,下面我們來說下該函數(shù)關(guān)閉的過程。
系統(tǒng)默認(rèn)(就是在沒有設(shè)置SO_LINGER可選項(xiàng)的情況下)在調(diào)用 close() 時(shí)朽砰,只是將EOF字段(即FIN報(bào)文段)寫進(jìn)套接字緩沖區(qū)尖滚,寫進(jìn)緩沖區(qū)就返回喉刘,這就是說調(diào)用 close() 函數(shù)返回,并不代表已經(jīng)成功將FIN報(bào)文段發(fā)送給對(duì)方(拿A漆弄、B兩個(gè)主機(jī)來舉例)睦裳。

  • A主動(dòng)斷開,調(diào)用 close() 函數(shù)將EOF字段(FIN報(bào)文段)寫進(jìn)輸出緩沖撼唾,系統(tǒng)等待前面的數(shù)據(jù)傳送完再將后來加進(jìn)緩沖區(qū)的EOF字段(FIN報(bào)文段)發(fā)送給對(duì)方
  • B主機(jī)系統(tǒng)在收到A的FIN報(bào)文段會(huì)立即向?qū)Ψ交厮鸵粋€(gè)ACK確認(rèn)包(表示我收到了你的FIN報(bào)文段)
  • B主機(jī)的應(yīng)用層這時(shí)調(diào)用recv()函數(shù)會(huì)收到EOF字段(EOF字符長(zhǎng)度為0廉邑,也就是 recv()函數(shù)返回接收到數(shù)據(jù)長(zhǎng)度為0),就知道A要斷開連接了倒谷,自己處理完數(shù)據(jù)也調(diào)用 close()向?qū)Ψ桨l(fā)送FIN報(bào)文段
  • A主機(jī)系統(tǒng)在收到對(duì)方的FIN報(bào)文段后蛛蒙,會(huì)向?qū)Ψ桨l(fā)送ACK確認(rèn),確認(rèn)發(fā)送成功之后系統(tǒng)釋放掉該套接字渤愁,B收到ACK后也會(huì)釋放掉要關(guān)閉的套接字牵祟,關(guān)閉到此結(jié)束

上面就是調(diào)用 cloes() 函數(shù)經(jīng)歷的四次握手?jǐn)嚅_,看文字難理解可以結(jié)合我前面的一篇文章里面的套接字?jǐn)嚅_四次握手圖對(duì)比著理解抖格。

②. 套接字設(shè)置了SO_LINGER可選項(xiàng)下诺苹,調(diào)用close函數(shù)關(guān)閉:與第①種情況不同的是,在調(diào)用 close() 函數(shù)并不是立即返回雹拄,超時(shí)時(shí)間足夠的情況下筝尾,會(huì)等待里面所有的數(shù)據(jù)發(fā)送完以及最后調(diào)用 close() 函數(shù)加進(jìn)輸出緩沖的FIN報(bào)文段也發(fā)送給對(duì)方之后,才會(huì)返回办桨,在此之前阻塞了調(diào)用 close() 函數(shù)的線程。
這個(gè)時(shí)候就會(huì)有疑問站辉,這個(gè)可選項(xiàng)會(huì)在什么時(shí)候用到呢呢撞?設(shè)和不設(shè)置SO_LINGER可選項(xiàng)系統(tǒng)都會(huì)將數(shù)據(jù)都發(fā)送給對(duì)方后再釋放套接字,只不過一個(gè)是寫進(jìn)輸出緩沖就返回饰剥,一個(gè)是等發(fā)送成功才返回殊霞。我在開發(fā)中就遇到了一個(gè)必須設(shè)置該可選項(xiàng)的情況,因?yàn)槲沂亲鲋悄芗揖娱_發(fā)方面汰蓉,其中有一種配置單片機(jī)聯(lián)網(wǎng)方式就是連接上單片機(jī)的LAN網(wǎng)絡(luò)绷蹲,這個(gè)時(shí)候手機(jī)跟單片機(jī)在同一個(gè)局域網(wǎng)下,就可以跟單片機(jī)建立TCP連接并將路由器的WiFi名字和密碼發(fā)送給對(duì)方顾孽,單片機(jī)收到后解析完會(huì)給手機(jī)端發(fā)送一個(gè)數(shù)據(jù)包祝钢,表示自己已經(jīng)收到聯(lián)網(wǎng)需要的信息然后重啟自己連上該路由器,但有時(shí)網(wǎng)絡(luò)不好這個(gè)信息并不一定發(fā)給手機(jī)端(單片機(jī)那邊只是調(diào)用 send()函數(shù)返回后再close() 關(guān)閉套接字)若厚,這時(shí)只能說明數(shù)據(jù)已經(jīng)寫進(jìn)單片機(jī)的輸出緩沖拦英,并不代表已經(jīng)發(fā)送給手機(jī)端,然后緊接著重啟测秸,這時(shí)所有的數(shù)據(jù)都丟失疤估,導(dǎo)致手機(jī)端應(yīng)用層并不知道設(shè)備有沒有收到發(fā)送的指令灾常,也不知道設(shè)備有沒有解析出報(bào)文連接上路由器,此時(shí)如果用SO_LINGER可選項(xiàng)對(duì)套接字進(jìn)行設(shè)置就很容易解決這個(gè)問題铃拇,調(diào)用 close() 函數(shù)直到發(fā)送FIN報(bào)文段成功才會(huì)返回钞瀑。

③. shutdown半關(guān)閉套接字:
從字面意思上我們很容易理解,半關(guān)閉就是要么關(guān)閉套接字的輸入流要么關(guān)閉套接字的輸出流慷荔,要么兩個(gè)都關(guān)閉雕什。

int shutdown(int sock,  //需要斷開套接字的文件描述符
             int howto  //傳遞斷開方式信息
             );
//成功時(shí)返回0.失敗返回-1
howto的可選值分別有如下三種
#define SHUT_RD     0   斷開輸入流
#define SHUT_WR     1   斷開輸出流
#define SHUT_RDWR   2   同時(shí)斷開I/O流

①. 填寫 SHUT_RD 斷開輸入流,這時(shí)輸出流還可以接著發(fā)送數(shù)據(jù)拧廊,這時(shí)調(diào)用 recv() 一直返回讀取數(shù)據(jù)長(zhǎng)度為0监徘,第一次是原因是“No such file or directory”,后面都是“Operation timed out”
②. 填寫 SHUT_WR 斷開輸出流吧碾,這時(shí)輸入流還可以接著讀取數(shù)據(jù)凰盔,套接字對(duì)方調(diào)用recv() 一直返回讀取數(shù)據(jù)長(zhǎng)度為0,第一次可以理解為收到對(duì)方的EOF報(bào)文倦春,輸出原因是“No such file or directory”户敬,后面都是“Operation timed out”,這時(shí)自己調(diào)用 send()函數(shù)會(huì)拋出SIGPIPE信號(hào)量睁本,內(nèi)核缺失情況下項(xiàng)目停止運(yùn)行
③. 填寫 SHUT_RDWR 同事斷開I/O流尿庐,原理上就是先調(diào) SHUT_RD 然后再調(diào)一次 SHUT_WR

shutdown() 函數(shù)與 close()函數(shù)最大的區(qū)別就是不管大少個(gè)進(jìn)程用同一個(gè)套接字,前者只要其中一個(gè)進(jìn)程調(diào)用了半關(guān)閉呢堰,其他的進(jìn)程使用該套接字也將進(jìn)入半關(guān)閉狀態(tài)

4. SO_RCVTIMEO & SO_SNDTIMEO

SO_RCVTIMEO 設(shè)置套接字讀取數(shù)據(jù)的超時(shí)時(shí)間抄瑟,SO_SNDTIMEO 設(shè)置套接字發(fā)送數(shù)據(jù)的超時(shí)時(shí)間。

#include <sys/socket.h>

int setRecvTimeout(int sock)
{
    //設(shè)置套接字讀取數(shù)據(jù)超時(shí)時(shí)間為5s
    struct timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;
    if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == -1) {
        return 0;
    }
    return 1;
}

int setSendTimeout(int sock)
{
    //設(shè)置套接字發(fā)送數(shù)據(jù)的超時(shí)時(shí)間為5s
    struct timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;
    if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == -1) {
        return 0;
    }
    return 1;
}

這個(gè)超時(shí)在平時(shí)的開發(fā)中用來設(shè)置TCP套接字倒是不多枉疼,一般UDP套接字用的比較多皮假,我前面的一篇iOS開發(fā)-TFTP客戶端和服務(wù)器的實(shí)現(xiàn)文章里面在用UDP套接字發(fā)送數(shù)據(jù)給對(duì)方,對(duì)方收到之后會(huì)用UDP回發(fā)一個(gè)ACK包骂维,這個(gè)時(shí)候我們才知道對(duì)方收到了我的上一個(gè)數(shù)據(jù)包惹资,接著發(fā)送下一個(gè)數(shù)據(jù)包,這個(gè)里面就用到了套接字的超時(shí)選項(xiàng)設(shè)置航闺,在規(guī)定的時(shí)間內(nèi)沒有收到對(duì)方的ACK確認(rèn)包褪测,則重發(fā)!UDP套接字發(fā)送數(shù)據(jù)不用建立連接潦刃,所以數(shù)據(jù)發(fā)送出去并不知道對(duì)方是否收到侮措,這樣一應(yīng)一答的通信設(shè)計(jì)用超時(shí)可選項(xiàng)進(jìn)行超時(shí)設(shè)置是再合適不過!

4. SO_RCVBUF & SO_SNDBUF

SO_RCVBUF 設(shè)置或獲取套接字輸入緩沖區(qū)的大小福铅,這個(gè)緩沖區(qū)是我們?cè)趧?chuàng)建套接字時(shí)系統(tǒng)為我們創(chuàng)建好的萝毛,SO_SNDBUF 設(shè)置或獲取套接字輸出緩沖區(qū)的大小

#include <sys/socket.h>

int getRecvBufferSize(int sock)
{
    int recvSize;
    socklen_t len;
    if (getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvSize, &len) == -1) {
        return 0;
    }else {
        printf("套接字的輸入緩沖大小是: %d\n",recvSize);
    }
    return recvSize;
}

int setSendBufferSize(int sock)
{
    int sendSize;
    socklen_t len;
    if (getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendSize, &len) == -1) {
        return 0;
    }else {
        printf("套接字的輸出緩沖大小是: %d\n",sendSize);
    }
    return sendSize;
}
我自己寫了一個(gè)客戶端的TCP套接字,打印了一下看滑黔,這個(gè)應(yīng)該以系統(tǒng)為準(zhǔn)笆包,系統(tǒng)不一樣分配的大小也不一樣环揽!

套接字的輸入緩沖大小是: 408300
套接字的輸出緩沖大小是: 146988
5. SO_RCVBUF & SO_SNDBUF

SO_RCVLOWAT 可選項(xiàng)用來設(shè)置套接字接收緩沖區(qū)下限,對(duì)于TCP套接字而言庵佣,一般被I/O復(fù)用系統(tǒng)用來判斷套接字是否可讀歉胶,當(dāng)套接字緩沖區(qū)中可讀數(shù)據(jù)總數(shù)大于我們?cè)O(shè)置的接收緩沖區(qū)下限時(shí),I/O復(fù)用系統(tǒng)調(diào)用將通知應(yīng)用程序可以從對(duì)應(yīng)的socket上讀取數(shù)據(jù)(觸發(fā) select() 函數(shù) 或 Linux獨(dú)有的 epoll() 函數(shù))巴粪,調(diào)用 recv() 函數(shù)才會(huì)返回通今,得到輸入緩沖區(qū)中的數(shù)據(jù)。
SO_SNDLOWAT 可選項(xiàng)用來設(shè)置套接字發(fā)送緩沖區(qū)的下限肛根,當(dāng)TCP套接字發(fā)送緩沖區(qū)中空閑空間(可以寫入數(shù)據(jù)的空間)大于設(shè)置的發(fā)送緩沖區(qū)下限時(shí)辫塌,I/O復(fù)用系統(tǒng)調(diào)用將通知應(yīng)用程序可以往對(duì)應(yīng)的socket上寫入數(shù)據(jù),調(diào)用 send() 函數(shù)才有返會(huì)(才能將數(shù)據(jù)寫進(jìn)輸出緩沖中)派哲。
默認(rèn)情況下臼氨,TCP套接字接收緩沖區(qū)的下限和TCP套接字發(fā)送緩沖區(qū)的下限標(biāo)記均為1字節(jié)

#include <sys/socket.h>

int setRcvBufferLowerLimit(int sock)
{
    //設(shè)置套接字接收下限為10個(gè)字節(jié)
    int opval = 10;
    if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &opval, sizeof(opval)) == -1) {
        return 0;
    }
    return 1;
}

int setSendBufferLowerLimit(int sock)
{
    //設(shè)置套接字發(fā)送緩沖區(qū)的下限為10個(gè)字節(jié)
    int opval = 10;
    if (setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &opval, sizeof(opval)) == -1) {
        return 0;
    }
    return 1;
}
6. SO_TYPE

SO_TYPE 獲取套接字類型,只能用來獲取而不能用來設(shè)置

#include <sys/socket.h>

int getSocketType(int sock)
{
    int opval;
    socklen_t opvalLen;
    if (getsockopt(sock, SOL_SOCKET, SO_TYPE, &opval, &opvalLen) == -1) {
        return 0;
    }else {
        if (opval == SOCK_DGRAM) {
            printf("UDP套接字\n");
        }else if (opval == SOCK_STREAM) {
            printf("TCP套接字\n");
        }else {
            printf("套接字類型為: %d\n",opval);
        }
    }
    return opval;
}
7. SO_BROADCAST

SO_BROADCAST 允許或禁止發(fā)送廣播數(shù)據(jù)芭届,以及后面有關(guān)多播的參數(shù)設(shè)置(IPPROTO_IP協(xié)議層的IP_MULTICAST_TTL設(shè)置多播傳輸距離储矩,IP_ADD_MEMBERSHIP加入多播組,IP_DROP_MEMBERSHIP離開多播組...)褂乍,都會(huì)在我的下篇文章里面做詳細(xì)的介紹持隧,這里就不做過多累述了。

8. TCP_NODELAY

TCP_NODELAY 是否禁用Nagle算法逃片。
為了防止網(wǎng)絡(luò)數(shù)據(jù)包過多而發(fā)生網(wǎng)絡(luò)過載屡拨,Nagle算法在1984年誕生了,他應(yīng)用于TCP層褥实,非常簡(jiǎn)單洁仗,是否使用差異看下圖

Nagle算法.png

TCP套接字默認(rèn)使用Nagle算法進(jìn)行數(shù)據(jù)交換,因此最大限度的進(jìn)行緩沖性锭,只有收到上個(gè)包的確認(rèn)ACK后才會(huì)傳輸下個(gè)數(shù)據(jù)包。
在不使用Nagle算法的情況下發(fā)送數(shù)據(jù)叫胖,不需要等待收到上個(gè)數(shù)據(jù)包的ACK確認(rèn)草冈,接著就開始發(fā)送下面的數(shù)據(jù),這種情況下會(huì)對(duì)網(wǎng)絡(luò)流量產(chǎn)生極大的負(fù)面影響瓮增,即使只傳輸1個(gè)字節(jié)的數(shù)據(jù)怎棱,其頭信息都有可能是幾十個(gè)字節(jié)。
因此為了提高網(wǎng)絡(luò)傳輸效率绷跑,必須使用Nagle算法拳恋。

但是對(duì)于大文件的網(wǎng)絡(luò)傳輸,有時(shí)不使用Nagle算法能提高傳輸速度

int enableNagel(int sock)
{
    //禁用Nagle算法
    int opval = 1;
    if (setsockopt(sock, SOL_SOCKET, TCP_NODELAY, &opval, sizeof(opval)) == -1) {
        return 0;
    }
    return 1;
}

四砸捏、結(jié)語

對(duì)于套接字可選項(xiàng)的總結(jié)就到這谬运,開發(fā)中常用的都已經(jīng)列舉出來了隙赁,有遺漏的日后會(huì)補(bǔ)上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末梆暖,一起剝皮案震驚了整個(gè)濱河市伞访,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌轰驳,老刑警劉巖厚掷,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異级解,居然都是意外死亡冒黑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門勤哗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抡爹,“玉大人,你說我怎么就攤上這事俺陋』硌樱” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵腊状,是天一觀的道長(zhǎng)诱咏。 經(jīng)常有香客問我,道長(zhǎng)缴挖,這世上最難降的妖魔是什么袋狞? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮映屋,結(jié)果婚禮上苟鸯,老公的妹妹穿的比我還像新娘。我一直安慰自己棚点,他們只是感情好早处,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瘫析,像睡著了一般砌梆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贬循,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天咸包,我揣著相機(jī)與錄音,去河邊找鬼杖虾。 笑死烂瘫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的奇适。 我是一名探鬼主播坟比,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼芦鳍,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了温算?” 一聲冷哼從身側(cè)響起怜校,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎注竿,沒想到半個(gè)月后茄茁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巩割,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年裙顽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宣谈。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愈犹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闻丑,到底是詐尸還是另有隱情漩怎,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布嗦嗡,位于F島的核電站勋锤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏侥祭。R本人自食惡果不足惜叁执,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望矮冬。 院中可真熱鬧谈宛,春花似錦、人聲如沸胎署。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琼牧。三九已至径筏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間障陶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工聊训, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抱究,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓带斑,卻偏偏與公主長(zhǎng)得像鼓寺,于是被迫代替她去往敵國和親勋拟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345