C/S網(wǎng)絡(luò)通信基本理解

一般認(rèn)為Web服務(wù)器程序是一個長時間運(yùn)行的程序(即所謂的守護(hù)進(jìn)程,daemon )报辱,它只響應(yīng)來自網(wǎng)絡(luò)的請求時才發(fā)送網(wǎng)絡(luò)消息召噩。協(xié)議的另一端是Web客戶程序父虑,如某種瀏覽器,與服務(wù)器進(jìn)行通信總是由客戶進(jìn)程發(fā)起授药。在設(shè)計網(wǎng)絡(luò)應(yīng)用時士嚎,確定總是由客戶發(fā)起請求往往能夠簡化協(xié)議和程序本身。當(dāng)然一些較為復(fù)雜的的網(wǎng)絡(luò)應(yīng)用還需要異步回調(diào)通信悔叽,也就是由服務(wù)器向客戶端發(fā)起請求信息莱衩。

客戶與服務(wù)器之間是通過某個網(wǎng)絡(luò)協(xié)議通信的,但實(shí)際上娇澎,這樣的通信通常涉及多個網(wǎng)絡(luò)協(xié)議層笨蚁。這里聚焦的是:TCP/IP協(xié)議族,也稱為網(wǎng)絡(luò)協(xié)議族趟庄。舉例來說括细,Web客戶與服務(wù)器之間使用TCP通信,TCP又轉(zhuǎn)而使用IP通信戚啥,IP再通過某種形式的數(shù)據(jù)鏈路層通信奋单。

Paste_Image.png

在圖中,客戶與服務(wù)器之間的信息流在其中一端是向下通過協(xié)議棧的猫十,跨越網(wǎng)絡(luò)后览濒,在另一端則是向上通過協(xié)議棧的呆盖。另外注意,TCP/IP協(xié)議是內(nèi)核中協(xié)議棧的一部分贷笛。(回憶:在LINUX的進(jìn)程中应又,LINUX系統(tǒng)是在內(nèi)核態(tài)的,內(nèi)核態(tài)被所有的進(jìn)程所共享乏苦,而TCP/IP協(xié)議屬于LINUX系統(tǒng)的網(wǎng)絡(luò)協(xié)議株扛,也就是內(nèi)核中的協(xié)議棧,具體的linux內(nèi)核相關(guān)的學(xué)習(xí)在linux 系統(tǒng)文集中)

同一網(wǎng)絡(luò)應(yīng)用的客戶和服務(wù)器當(dāng)處于不同局域網(wǎng)時邑贴,不同的局域網(wǎng)使用路由器連接到廣域網(wǎng):

Paste_Image.png

根據(jù)原書的介紹席里,代碼里面所使用的大多數(shù)系統(tǒng)函數(shù),都定義了各自的包裹函數(shù)拢驾。而且可以使用這些包裹函數(shù)來檢查錯誤奖磁,輸出適當(dāng)?shù)南ⅲ约霸诔鲥e時終止程序的運(yùn)行繁疤。


下面講代碼了咖为,這個是客戶端顯示服務(wù)器的當(dāng)前時間和日期的簡單代碼(base1)

#include "unp.h" //該頭文件包含大部分網(wǎng)絡(luò)程序都需要的許多系統(tǒng)頭文件,并定義了所用到的各種常量值(如MAXLINE).
int main(int argc, char **argv) // main 函數(shù)的定義稠腊,形參是命令行參數(shù)
{
    int  sockfd, n;
    char    recvline[MAXLINE + 1];
    struct  sockaddr_in servaddr;

    if (argc != 2) err_quit("usage: a.out <IPaddress>");
    //  調(diào)用 socket 函數(shù)創(chuàng)建一個 ipv4 字節(jié)流套接字躁染,返回一個小整數(shù)描述符褪迟,以后的所有函數(shù)調(diào)用(如隨后的connect 和 read)就用該描述符來標(biāo)識這個套接字歼冰。
    //  如果socket函數(shù)調(diào)用失敗,我們就調(diào)用自己的err_sys 函數(shù)放棄程序運(yùn)行曹动。
    //  自定義的 **err_sys 函數(shù)** 輸出我們作為參數(shù)提供的出錯信息以及所發(fā)生的系統(tǒng)錯誤的描述叹放。后面會具體描述這些函數(shù)
    if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error"); 
    // 使用bzero把整個結(jié)構(gòu)清零后饰恕,置地址族為AF_INET,端口號為13.
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port   = htons(13);    /* daytime server */
        
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr)<= 0) err_quit("inet_pton error for %s", argv[1]);
    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr))< 0) err_sys("connect error");
        
    // 兩個左括號間加一個空格井仰,提示比較運(yùn)算符的左側(cè)同時也是一個賦值運(yùn)算
    while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
        if (fputs(recvline, stdout) == EOF) err_sys("fputs error");
    }
    if (n < 0) err_sys("read error");
    exit(0);
}

細(xì)節(jié)
socket模塊定義了一些常量參數(shù)埋嵌,用來指定 socket的的地址族、socket的類型俱恶、以及支持的TCP/IP協(xié)議雹嗦。
socket.socket([family[, type[, proto]]]):根據(jù)指定的 地址族套接字類型協(xié)議編號(默認(rèn)為0)來創(chuàng)建 套接字對象合是。AF_INET 對應(yīng)的 IPV4, AF_INET6 對應(yīng)的 IPV6了罪。具體參數(shù)見下表:

Paste_Image.png

1、我們把服務(wù)器的IP地址和端口號填入一個網(wǎng)際套接字地址結(jié)構(gòu)(一個名為 servaddr 的 sockaddr_in 結(jié)構(gòu)變量)端仰。這里捶惜,端口號為13,是時間獲取服務(wù)器的眾所周知端口荔烧,支持該服務(wù)器的任何 TCP/IP 主機(jī)都使用這個端口號吱七。

2汽久、而網(wǎng)際套接字地址結(jié)構(gòu)中 IP地址 和 端口號 這兩個成員必須使用特定格式,為此我們調(diào)用庫 htons(“主機(jī)到網(wǎng)絡(luò)短整數(shù)”)去轉(zhuǎn)換二進(jìn)制端口號踊餐,又調(diào)用庫函數(shù) inet_pton(“呈現(xiàn)形式到整數(shù)”)去把 ASCII命令行參數(shù)(例如運(yùn)行本例子所用的206.168.112.96)轉(zhuǎn)換為合適的格式景醇。

3、bzero 不是一個ANSI C函數(shù)吝岭,幾乎所有支持套接字API的廠商都提供bzero三痰,如果沒有,那么可以使用unp.h頭文件中提供的該函數(shù)的宏定義窜管。inet_pton 函數(shù)是一個支持 IPV6 的新函數(shù)散劫,以前的代碼使用 inet_addr 函數(shù)來把ASCII點(diǎn)分十進(jìn)制數(shù)串變換為正確的格式,不過它有不少局限幕帆,而這些局限在inet_pton中都得以糾正获搏。

4、connect 函數(shù)應(yīng)用于一個TCP套接字時失乾,將與由它的第二個參數(shù)指向的套接字地址結(jié)構(gòu)指定的服務(wù)器建立一個TCP連接常熙。該套接字地址結(jié)構(gòu)的長度也必須作為該函數(shù)的第三個參數(shù)指定,對于網(wǎng)際套接字地址結(jié)構(gòu)碱茁,我們總是使用C語言的sizeof操作符由編譯器來計算這個長度裸卫。

5、在頭文件unp.h中纽竣,我們使用#define 把SA定義為struct sockaddr墓贿,通用套接字地址結(jié)構(gòu)。每當(dāng)一個套接字函數(shù)需要一個指向某個套接字地址結(jié)構(gòu)的指針時蜓氨,這個指針必須強(qiáng)制類型轉(zhuǎn)換成一個指向通用套接字地址結(jié)構(gòu)的指針募壕。

6、fputs()函數(shù)用于將指定的字符串寫入到文件流中语盈,其原型為:
int fputs(char * string, FILE * stream);【參數(shù)】string為將要寫入的字符串,stream為文件流指針缰泡〉痘模【返回值】成功返回非負(fù)數(shù),失敗返回EOF棘钞。fputs()從string的開頭往文件寫入字符串缠借,直到遇見結(jié)束符 '\0','\0' 不會被寫入到文件中宜猜。注意:fputs()可以指定輸出的文件流泼返,不會輸出多余的字符;puts()只能向 stdout 輸出字符串姨拥,而且會在最后自動增加換行符绅喉。

協(xié)議無關(guān)性

上面的程序是與 IPv4 協(xié)議相關(guān)的:我們分配并初始化一個 sockaddr_in 類型的結(jié)構(gòu)渠鸽,把該結(jié)構(gòu)的協(xié)議族成員設(shè)置為AF_INET, 并指定 socket 函數(shù)的第一個參數(shù)為 AF_INET.

為了讓圖1-5的程序能夠在IPv6上運(yùn)行,我們必須修改這段代碼柴罐。下面是一個能在IPV6上運(yùn)行的版本(base2):

#include    "unp.h"
int main(int argc, char **argv)
{
    int sockfd, n;
    struct sockaddr_in6 servaddr;
    char    recvline[MAXLINE + 1];

    if (argc != 2) err_quit("usage: a.out <IPaddress>");
    if ( (sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) err_sys("socket error");
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin6_family = AF_INET6;
    servaddr.sin6_port   = htons(13);   /* daytime server */
    
    if (inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0) err_quit("inet_pton error for %s", argv[1]);
    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) err_sys("connect error");
    
    while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
        if (fputs(recvline, stdout) == EOF) err_sys("fputs error");
    }
    if (n < 0) err_sys("read error");
    exit(0);
}

不足之處:

  • 這種寫法是與ipv6協(xié)議相關(guān)的寫法徽缚,更好的做法是編寫協(xié)議無關(guān)的程序。
  • 這里的不足之處革屠,用戶必須以點(diǎn)分十進(jìn)制數(shù)格式給出服務(wù)器的IP地址(如適合于IPV4版本的206.168.112.219)凿试。而我們一般都習(xí)慣于用名字代替數(shù)字。
錯誤處理:包裹函數(shù)

任何現(xiàn)實(shí)世界的程序都必須檢查每個函數(shù)調(diào)用是否返回錯誤似芝。在上面的程序中那婉,我們檢查socket、inet_pton党瓮、connect详炬、read 和 fputs函數(shù)是否返回錯誤,當(dāng)發(fā)生錯誤時麻诀,就調(diào)用我們自己的err_quit 或 err_sys 函數(shù)輸出一個出錯信息并終止程序的運(yùn)行痕寓。但是個別情況下,當(dāng)這些函數(shù)返回錯誤時蝇闭,我們想做的事并非簡單地終止程序的運(yùn)行呻率。

于是,我們通過定義包裹函數(shù)來縮短程序呻引。每個包裹函數(shù)完成實(shí)際的函數(shù)調(diào)用礼仗,檢查返回值,并在發(fā)生錯誤時終止進(jìn)程逻悠。我們約定包裹函數(shù)名是實(shí)際函數(shù)名的首字母大寫形式元践。例如在語句:
sockfd = Soccket(AF_INET,SOCKET_STREAM,0); 中,函數(shù)Socket 是函數(shù) socket的包裹函數(shù)童谒,如圖:

/* include Socket */
int Socket(int family, int type, int protocol)
{
    int n;
    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

線程函數(shù)遇到錯誤時并不設(shè)置標(biāo)準(zhǔn)Unix的errno變量单旁,而是把errno的值作為函數(shù)返回值返回調(diào)用者。這意味著每次調(diào)用以pthread_開頭的某個函數(shù)時饥伊,我們必須分配一個變量來存放函數(shù)返回值象浑,以便在調(diào)用err_sys前把errno變量設(shè)置成該值。

/* include Pthread_mutex_lock */
void Pthread_mutex_lock(pthread_mutex_t *mptr)
{
    int n;
    if ( (n = pthread_mutex_lock(mptr)) == 0) return;
    errno = n;
    err_sys("pthread_mutex_lock error");
}
/* end Pthread_mutex_lock */

除非必須檢查某個確定的錯誤是否發(fā)生琅豆,并以不同于終止進(jìn)程的其他某種方式處理它愉豺,否則就使用這些包裹函數(shù)。

下面是匹配的時間獲取服務(wù)器程序(base3)

#include    "unp.h"
#include    <time.h>

int main(int argc, char **argv)
{
    int  listenfd, connfd;
    struct sockaddr_in  servaddr;
    char  buff[MAXLINE];
    time_t  ticks;
        
    //  調(diào)用 socket 函數(shù)調(diào)用一個 ipv4 字節(jié)流套接字茫因,返回一個小整數(shù)描述符
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    //  填寫網(wǎng)際套接字地址結(jié)構(gòu)并調(diào)用bind函數(shù)
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET
    //   我們指定IP地址為INADDR_ANY蚪拦,這樣要是服務(wù)器主機(jī)有多個網(wǎng)絡(luò)接口,服務(wù)器進(jìn)程就可以在任意  
    //   網(wǎng)絡(luò)接口上接受客戶連接。
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(13);  /* daytime server */
        
    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    //   調(diào)用 listen 函數(shù)把該套接字轉(zhuǎn)換成一個監(jiān)聽套接字驰贷,這樣來自客戶的外來鏈接就可在該套接字上由內(nèi)核接受盛嘿。
    //   常值 LISTENNQ 在我們的 unp.h 頭文件中定義。它指定系統(tǒng)內(nèi)核允許在這個監(jiān)聽描述符上排隊的最大客戶饱苟。
    Listen(listenfd, LISTENQ);
    for ( ; ; ) {
        connfd = Accept(listenfd, (SA *) NULL, NULL);  
                ticks = time(NULL);
                // snprintf 函數(shù)在這個字符串末尾添加一個回車符和一個換行符孩擂,隨后write函數(shù)把結(jié)果字符串寫給用戶。
                snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
                Write(connfd, buff, strlen(buff));
        Close(connfd);
    }
}

細(xì)節(jié):
1箱熬、通常情況下类垦,服務(wù)器進(jìn)程在accept調(diào)用中被投入睡眠,等待某個客戶連接的到達(dá)并被內(nèi)核接受城须。TCP連接使用所謂的三次握手來建立連接蚤认。握手完畢時accept返回,其返回值是一個稱為已連接描述符的新描述符(本例中為connfd)糕伐。該描述符用于與新近連接的那個客戶通信砰琢。accept 為每個連接到本服務(wù)器的客戶返回一個新描述符。
2良瞧、調(diào)用sprintf無法檢查目的緩沖區(qū)是否溢出陪汽,相反,snprintf要求其第二個參數(shù)指定目的緩沖區(qū)的大小褥蚯,因此可確保該緩沖區(qū)不溢出挚冤。
3、值得注意的是:許多網(wǎng)絡(luò)入侵是由黑客通過發(fā)送數(shù)據(jù)赞庶,導(dǎo)致服務(wù)器對sprintf的調(diào)用使其緩沖區(qū)溢出而發(fā)生的训挡,必須小心使用的函數(shù)還有g(shù)ets、strcat和strcpy歧强,通常應(yīng)分別改為調(diào)用fgets澜薄、strncat和strncpy。更好的替代函數(shù)是后面才引入的strlcat 和 strlcpy摊册,它們確保結(jié)果是正確終止的字符串肤京。
4、本服務(wù)器一次只能處理一個客戶茅特。如果多個客戶連接差不多同時到達(dá)蟆沫,系統(tǒng)內(nèi)核在某個最大數(shù)目的限制下把它們排入隊列,然后每次返回一個給accept函數(shù)温治。本服務(wù)器只需調(diào)用time 和 ctime 這兩個庫函數(shù),運(yùn)行速度很快戒悠。同時能處理多個用戶的并發(fā)服務(wù)器有多種編程寫法熬荆,最簡單的技術(shù)是Unix的fork函數(shù)(多進(jìn)程編程),或在服務(wù)器啟動時預(yù)先fork一定數(shù)量的子進(jìn)程(進(jìn)程池)绸狐。

之后卤恳,全部用來描述網(wǎng)絡(luò)編程中使用的各種技術(shù)的兩個客戶/服務(wù)器程序示例如下:

  • 時間獲取客戶/服務(wù)器程序(base1, base2, base3)累盗;
  • 回射客戶/服務(wù)器程序(base4)

所有程序的擴(kuò)展,以及所有程序的完善突琳,都是與這三個程序息息相關(guān)若债。
描述一個網(wǎng)絡(luò)中各個協(xié)議層的常用方法是使用國際標(biāo)準(zhǔn)化組織的**計算機(jī)通信開放系統(tǒng)互聯(lián)(OSI)模型。這是一個七層模型拆融,如下圖所示蠢琳,圖中同時給出了它與網(wǎng)際協(xié)議族的近似映射。

協(xié)議族.png

這里镜豹,OSI 模型的底下兩層是隨系統(tǒng)提供的 設(shè)備驅(qū)動 和 網(wǎng)絡(luò)硬件傲须。通常情況下,除需知道數(shù)據(jù)鏈路的某些特性外趟脂,我們不必要知道這兩層的具體情況泰讽。

圖中,TCP和UDP之間留有空隙昔期,表明:網(wǎng)絡(luò)應(yīng)用繞過傳輸層直接使用 IPv4 或 IPv6 是可能的已卸。這就是所謂的原始套接字。

OSI模型的頂上三層被合并成一層硼一,稱為應(yīng)用層累澡。這就是:Web客戶(瀏覽器)、Telent客戶欠动、Web服務(wù)器永乌、FTP服務(wù)器和其他我們在使用的網(wǎng)絡(luò)應(yīng)用所在的層。而進(jìn)行網(wǎng)絡(luò)編程的套接字就是從應(yīng)用層進(jìn)入傳輸層的接口具伍。重點(diǎn)是翅雏,如何使用套接字編寫使用TCP或UDP的網(wǎng)絡(luò)應(yīng)用程序。 后面還會有如何通過原始套接字徹底繞過IP層直接讀取數(shù)據(jù)鏈路層的幀人芽。

問:之所以套接字提供的是從OSI模型的頂上三層進(jìn)入傳輸層的接口望几?
這樣設(shè)計有兩個理由,理由之一是:頂上三層處理具體網(wǎng)絡(luò)應(yīng)用(如FTP萤厅、Telnet或HTTP)的所有細(xì)節(jié)橄抹,卻對通信細(xì)節(jié)了解很少;底下四層對具體忘了應(yīng)用了解不多惕味,卻處理所有的通信細(xì)節(jié):** 發(fā)送數(shù)據(jù)楼誓,等待確認(rèn),給無序到達(dá)數(shù)據(jù)排序名挥,計算并驗證校驗和疟羹,等等。理由之二:頂上三層通常構(gòu)成所謂的用戶進(jìn)程,底下四層卻通常作為操作系統(tǒng)內(nèi)核的一部分提供榄融。Unix與其他現(xiàn)代操作系統(tǒng)都提供分隔用戶進(jìn)程與內(nèi)核的機(jī)制参淫。由此可見,OSI的第四層和第五層的接口是構(gòu)建API的自然位置愧杯。

網(wǎng)絡(luò)拓?fù)涞陌l(fā)現(xiàn)
大多數(shù)Unix系統(tǒng)都提供了可用于發(fā)現(xiàn)某些網(wǎng)絡(luò)細(xì)節(jié)的兩個基本命令:netstat 和 ifconfig涎才。而且,有些廠商把這些命令存放在諸如/sbin 或 /usr/sbin 這樣的管理目錄中力九,而不是通常的/usr/bin目錄耍铜,而這些管理目錄可能不在通常的shell搜索路徑中(由PATH環(huán)境變量指定)

(1)netstat -i 提供網(wǎng)絡(luò)接口的信息。我們還指定 -n標(biāo)志以輸出數(shù)值地址畏邢,而不是試圖把它們反向解析成名字业扒。下面的例子給出了接口及其名字好統(tǒng)計信息:


其中環(huán)回(loopback)接口稱為lo,以太網(wǎng)接口稱為eth0舒萎。下面的例子給出了支持IPV6的一個主機(jī)的類似信息:


Paste_Image.png

(2)netstat -r展示路由表程储,也是另一種確定接口的方法。我們通常指定-n標(biāo)志以輸出數(shù)值地址臂寝。它還給出默認(rèn)路由器的IP地址章鲤。

(3)有了各個網(wǎng)絡(luò)接口的名字,執(zhí)行ifconfig就可獲得每個接口的詳細(xì)信息咆贬。

Paste_Image.png

該命令給出了指定接口的IP地址败徊、子網(wǎng)掩碼和廣播地址。其中的MULTICAST標(biāo)志通常指明該接口所在主機(jī)支持多播掏缎。有些ifconfig的實(shí)現(xiàn)還提供-a標(biāo)志皱蹦,用于輸出所有已配置接口的信息。

(4)找出本地網(wǎng)絡(luò)中眾多主機(jī)的IP地址的方法之一是眷蜈,針對從上一步找到的本地接口的廣播地址執(zhí)行ping命令沪哺。

POSIX 的背景
POSIX(可移植操作系統(tǒng)接口)是由IEEE開發(fā)的一系列標(biāo)準(zhǔn)。第一個標(biāo)準(zhǔn)詳述了進(jìn)入類Unix內(nèi)核的C語言接口酌儒,涵蓋了下述領(lǐng)域:進(jìn)程原語(fork辜妓、exec、信號和定時器)忌怎、進(jìn)程環(huán)境(用戶ID和進(jìn)程組)籍滴、文件與目錄(所有I/O函數(shù))、終端I/O榴啸、系統(tǒng)數(shù)據(jù)庫(口令文件和用戶組文件)以及tar和cpio歸檔格式孽惰。POSIX.1增添了3章關(guān)于線程的內(nèi)容,并另有關(guān)于線程同步(互斥鎖和條件變量)鸥印、線程調(diào)度和同步調(diào)度的各節(jié)勋功。其中腥例,聲明 ISO/IEC 9945 由下面3個部分構(gòu)成:

  • Part 1: System API(C language)—— 第一部分:系統(tǒng)API(C語言)。
  • Part 2: Shell and utilities—— 第二部分:Shell 和實(shí)用程序酝润。
  • Part 3: System administration—— 第三部分:系統(tǒng)管理(正在開發(fā)中) 。

最后一個版本璃弄,是聯(lián)網(wǎng)API標(biāo)準(zhǔn)要销,定義了兩個API,并稱它們?yōu)樵敱M網(wǎng)絡(luò)接口(DNI)

  • DNI/Socket夏块,基于 4.4BSD 的套接字API
  • DNI/XTI疏咐,基于 X/Open 的 XPG4規(guī)范

** 64位體系結(jié)構(gòu)**
選用64位軟件的體系結(jié)構(gòu),原因之一是在每個進(jìn)程內(nèi)部可以由此使用更長的編址長度(即64位指針)脐供,從而可以尋址很大的內(nèi)存空間(超過2^32字節(jié))』肴現(xiàn)有32位Unix系統(tǒng)上共同的編程模型稱為ILP32模型,表示整數(shù)(I)政己、長整數(shù)(L)和指針(P)都占用32位酌壕。64位Unix系統(tǒng)上變得最為流行的模型稱為LP64模型,表示只有長整數(shù)(L)和指針(P)占用64位歇由。下面對這兩種模型進(jìn)行了比較卵牍。


ANSI C創(chuàng)造了 size_t 數(shù)據(jù)類型,它用于作為malloc的唯一參數(shù)(待分配的字節(jié)數(shù))沦泌,或者作為read 和 write的第三個參數(shù)(待讀或?qū)懙淖止?jié)數(shù))糊昙。在32位系統(tǒng)中 size_t 是一個 32位值,但是在64位系統(tǒng)中它必須是一個64位值谢谦,以便發(fā)揮更大尋址模型的優(yōu)勢释牺。也就意味著64位系統(tǒng)中也許含有一個把size_t定義為unsigned long 的typedef指令。聯(lián)網(wǎng)API存在如下問題:POSIX.1g的某些草案規(guī)定回挽,存放套接字地址結(jié)構(gòu)大小的函數(shù)參數(shù)具有size_t數(shù)據(jù)類型(如bind和connect的第三個參數(shù))没咙。如果不修改這些規(guī)定,當(dāng)Unix系統(tǒng)從ILP32模型轉(zhuǎn)變?yōu)長P64模型時厅各,size_t和long都將從32位值變?yōu)?4位值镜撩。這兩個例子實(shí)際上并不需要使用64位的數(shù)據(jù)類型:套接字地址結(jié)構(gòu)的長度最多也就幾百個字節(jié)。處理這些情況的辦法是使用專門設(shè)計的數(shù)據(jù)類型队塘。套接字API對套接字地址結(jié)構(gòu)使用 socklen_t 數(shù)據(jù)類型袁梗。不把這些值由32位改為64位的理由是易于為那些已在32位系統(tǒng)中編譯的應(yīng)用程序提供在新的64位系統(tǒng)張的二進(jìn)制代碼兼容性。


(注明1:守護(hù)進(jìn)程不僅僅是一個長時間運(yùn)行的程序憔古,而是一個隨著計算機(jī)啟動遮怜,而自動運(yùn)行的后臺程序,而且能在后臺運(yùn)行且不跟任何終端關(guān)聯(lián)的進(jìn)程——運(yùn)行鸿市,一般用shell去寫一個自定義的守護(hù)進(jìn)程)

(注明2:異步回調(diào):回調(diào)就是該函數(shù)寫在高層锯梁,低層通過一個函數(shù)指針保存這個函數(shù)即碗,在某個事件的觸發(fā)下,低層通過該函數(shù)指針調(diào)用高層那個函數(shù)陌凳。異步區(qū)別于同步剥懒,在同步模式下,一段代碼調(diào)用另一段代碼時合敦,只能采用同步調(diào)用初橘,必須等待這段代碼執(zhí)行完返回結(jié)果后,調(diào)用方才能繼續(xù)往下執(zhí)行充岛,有了多線程的支持保檐,可以采用異步調(diào)用,調(diào)用方和被調(diào)方可以屬于兩個不同的線程崔梗,調(diào)用方啟動被調(diào)方線程后夜只,不等對方返回結(jié)果就繼續(xù)執(zhí)行后續(xù)代碼。被調(diào)方執(zhí)行完畢后蒜魄,通過某種手段通知調(diào)用方:結(jié)果已經(jīng)出來扔亥,請酌情處理。权悟,這里的某種手段主要是指的線程間的通信:管道砸王,socket)

(注明3:C語言中用#define偽命令定義的對象稱為常數(shù),用const限定詞定義并初始化的對象稱為常量)峦阁。常數(shù)的值在編譯時確定谦铃,常量的值則在運(yùn)行時初始化后確定(不過此后只能作為右值使用)。本書絕大多數(shù)恒定值是用#define 定義的常數(shù)榔昔。)

(注明4:Unix errno 值驹闰,只要一個Unix函數(shù)(例如某個套接字函數(shù))中有錯誤發(fā)生,全局變量errno就被置為一個指明該錯誤類型的正值撒会,函數(shù)本身則通常返回-1嘹朗。err_sys 查看errno變量的值并輸出相應(yīng)的出錯消息,例如當(dāng)errno值等于ETIMEOUT時诵肛,將輸出“Connection time out”(連接超時)屹培。errno的值只在函數(shù)發(fā)生錯誤時設(shè)置。如果函數(shù)不返回錯誤怔檩,errno的值就沒有定義褪秀。errno 的左右正數(shù)錯誤值都是常值,具有以“E”開頭的全大寫字母名字薛训,并通常在<sys/errno.n> 頭文件中定義媒吗。值0不表示任何錯誤。)

(注明5:對于內(nèi)核而言乙埃,所有打開的文件都通過文件描述符引用闸英。文件描述符是一個非負(fù)整數(shù)锯岖。當(dāng)打開一個現(xiàn)有文件或創(chuàng)建一個新文件時,內(nèi)核向進(jìn)程返回一個文件描述符甫何。當(dāng)讀或?qū)懸粋€文件時出吹,使用open或create返回的文件描述符表示該文件,將其作為參數(shù)傳給read或write函數(shù)辙喂。用size_t作為參數(shù)的幾個API函數(shù)如下:

  • malloc 向系統(tǒng)申請分配指定size個字節(jié)的內(nèi)存空間趋箩。返回類型是 void* 類型。void* 表示未確定類型的指針加派。C,C++規(guī)定,void* 類型可以通過類型轉(zhuǎn)換強(qiáng)制轉(zhuǎn)換為任何其它類型的指針跳芳。
#include <stdlib.h>
#include <malloc.h>
extern void* malloc(unsigned int num_bytes); // 函數(shù)聲明:void *malloc(size_t size);
  • read函數(shù)定義如下:
#include <unistd>
ssize_t read(int filedes, void *buf, size_t nbytes);
// 返回:若成功則返回讀到的字節(jié)數(shù)芍锦,若已到文件末尾則返回0,若出錯則返回-1
// filedes:文件描述符
// buf:讀取數(shù)據(jù)緩存區(qū)
// nbytes:要讀取的字節(jié)數(shù)
// 有幾種情況可使實(shí)際讀到的字節(jié)數(shù)少于要求讀的字節(jié)數(shù):
// 1)讀普通文件時飞盆,在讀到要求字節(jié)數(shù)之前就已經(jīng)達(dá)到了文件末端娄琉。例如,若在到達(dá)文件末端之前還有30個字節(jié)吓歇,而要求讀100個字節(jié)孽水,則read返回30,下一次再調(diào)用read時城看,它將返回0(文件末端)女气。
// 2)當(dāng)從終端設(shè)備讀時,通常一次最多讀一行测柠。
// 3)當(dāng)從網(wǎng)絡(luò)讀時炼鞠,網(wǎng)絡(luò)中的緩存機(jī)構(gòu)可能造成返回值小于所要求讀的字結(jié)束。
// 4)當(dāng)從管道或FIFO讀時轰胁,如若管道包含的字節(jié)少于所需的數(shù)量谒主,那么read將只返回實(shí)際可用的字節(jié)數(shù)。
// 5)當(dāng)從某些面向記錄的設(shè)備(例如磁帶)讀時赃阀,一次最多返回一個記錄霎肯。
// 6)當(dāng)某一個信號造成中斷,而已經(jīng)讀取了部分?jǐn)?shù)據(jù)榛斯。

case:

  // 設(shè)置讀取的長度:
  char msg[1024];
  // 讀取用戶輸入:
  int ret = read(fd, msg, sizeof(msg));
  if( ret < 0 )
  {
    perror("read fail ");
    exit(1);
  }
  • write函數(shù)定義如下:
#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
// 返回:若成功則返回寫入的字節(jié)數(shù)观游,若出錯則返回-1
// filedes:文件描述符
// buf:待寫入數(shù)據(jù)緩存區(qū)
// nbytes:要寫入的字節(jié)數(shù)

case:

void TcpEventServer::ListenerEventCb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data)
{
  TcpEventServer *server = (TcpEventServer*)user_data;
  //隨機(jī)選擇一個子線程,通過管道向其傳遞socket描述符
  int num = rand() % server->m_ThreadCount;
  int sendfd = server->m_Threads[num].notifySendFd;
  write(sendfd, &fd, sizeof(evutil_socket_t));
}

(注明5:read/write的語義:為什么會阻塞肖抱?http://www.cnblogs.com/xiehongfeng100/p/4619451.html
首先备典,write成功返回,只是buf中的數(shù)據(jù)被復(fù)制到了kernel中的TCP發(fā)送緩沖區(qū)意述。至于數(shù)據(jù)什么時候被發(fā)往網(wǎng)絡(luò)提佣,什么時候被對方主機(jī)接收吮蛹,什么時候被對方進(jìn)程讀取,系統(tǒng)調(diào)用層面不會給予任何保證和通知拌屏。之所以會阻塞潮针,是當(dāng)kernel的該socket的發(fā)送緩沖區(qū)已滿時。對于每個socket倚喂,擁有自己的send buffer和receive buffer每篷。從Linux 2.6開始,兩個緩沖區(qū)大小都由系統(tǒng)自動調(diào)節(jié)端圈,但一般都在default和max之間浮動焦读。

# 獲取socket的發(fā)送/接受緩沖區(qū)的大小:(后面的值是在Linux 2.6.38 x86_64上測試的結(jié)果)
sysctl net.core.wmem_default       #126976
sysctl net.core.wmem_max        #131071

已經(jīng)發(fā)送到網(wǎng)絡(luò)的數(shù)據(jù)依然需要暫存在send buffer中舱权,只有收到對方的ack后矗晃,kernel才從buffer中清除這一部分?jǐn)?shù)據(jù),為后續(xù)發(fā)送數(shù)據(jù)騰出空間宴倍。接收端將收到的數(shù)據(jù)暫存在receive buffer中张症,自動進(jìn)行確認(rèn)。但如果socket所在的進(jìn)程不及時將數(shù)據(jù)從receive buffer中取出鸵贬,最終導(dǎo)致receive buffer填滿俗他,由于TCP的滑動窗口和擁塞控制,接收端會阻止發(fā)送端向其發(fā)送數(shù)據(jù)阔逼。這些控制皆發(fā)生在TCP/IP棧中兆衅,對應(yīng)用程序是透明的,應(yīng)用程序繼續(xù)發(fā)送數(shù)據(jù)嗜浮,最終導(dǎo)致send buffer填滿涯保,write調(diào)用阻塞。一般來說周伦,由于接收端進(jìn)程從socket讀數(shù)據(jù)的速度跟不上發(fā)送端進(jìn)程向socket寫數(shù)據(jù)的速度夕春,最終導(dǎo)致發(fā)送端write調(diào)用阻塞。而read調(diào)用的行為相對容易理解专挪,從socket的receive buffer中拷貝數(shù)據(jù)到應(yīng)用程序的buffer中及志。read調(diào)用阻塞,通常是發(fā)送端的數(shù)據(jù)沒有到達(dá)寨腔。)

(注明6:blocking(默認(rèn))和nonblock模式下read/write行為的區(qū)別
將socket fd設(shè)置為nonblock(非阻塞)是在服務(wù)器編程中常見的做法速侈,采用blocking IO并為每一個client創(chuàng)建一個線程的模式開銷巨大且可擴(kuò)展性不佳(帶來大量的切換開銷),更為通用的做法是采用線程池+Nonblock I/O+Multiplexing(select/poll迫卢,以及Linux上特有的epoll)倚搬。

// 設(shè)置一個文件描述符為nonblock
int set_nonblocking(int fd)
{
    int flags;
    if ((flags = fcntl(fd, F_GETFL, 0)) == -1)
        flags = 0;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

幾個重要的結(jié)論:

  1. read總是在接收緩沖區(qū)有數(shù)據(jù)時立即返回,而不是等到給定的read buffer填滿時返回乾蛤。
    只有當(dāng)receive buffer為空時每界,blocking模式才會等待捅僵,而nonblock模式下會立即返回-1(errno = EAGAIN或EWOULDBLOCK)注:阻塞模式下,當(dāng)對方socket關(guān)閉時眨层,read會返回0庙楚。

  2. blocking 的 write 只有在緩沖區(qū)足以放下整個 buffer 時才返回(與blocking read并不相同)nonblock write
    則是返回能夠放下的字節(jié)數(shù),之后調(diào)用則返回-1(errno = EAGAIN或EWOULDBLOCK)對于blocking的write有個特例:當(dāng)write正阻塞等待時對面關(guān)閉了socket趴樱,則write則會立即將剩余緩沖區(qū)填滿并返回所寫的字節(jié)數(shù)馒闷,再次調(diào)用則write失敗(connection reset by peer)


(注明7:read/write對連接異常的反饋行為
對應(yīng)用程序來說叁征,與另一進(jìn)程的TCP通信其實(shí)是完全異步的過程:

  1. 我并不知道對面什么時候纳账、能否收到的數(shù)據(jù)
  2. 我不知道什么時候能夠收到對面的數(shù)據(jù)
  3. 我不知道什么時候通信結(jié)束(主動退出或是異常退出、機(jī)器故障捺疼、網(wǎng)絡(luò)故障等等)
    對于1和2塞祈,采用write() -> read() -> write() -> read() ->...的序列,通過blocking read或者nonblock read+輪詢的方式帅涂,應(yīng)用程序基于可以保證正確的處理流程。
    對于3尤蛮,kernel將這些事件的“通知”通過read/write的結(jié)果返回給應(yīng)用層媳友。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市产捞,隨后出現(xiàn)的幾起案子醇锚,更是在濱河造成了極大的恐慌,老刑警劉巖坯临,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焊唬,死亡現(xiàn)場離奇詭異,居然都是意外死亡看靠,警方通過查閱死者的電腦和手機(jī)赶促,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挟炬,“玉大人鸥滨,你說我怎么就攤上這事“妫” “怎么了婿滓?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長粥喜。 經(jīng)常有香客問我凸主,道長,這世上最難降的妖魔是什么额湘? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任卿吐,我火速辦了婚禮旁舰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘但两。我一直安慰自己鬓梅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布谨湘。 她就那樣靜靜地躺著绽快,像睡著了一般。 火紅的嫁衣襯著肌膚如雪紧阔。 梳的紋絲不亂的頭發(fā)上坊罢,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音擅耽,去河邊找鬼活孩。 笑死,一個胖子當(dāng)著我的面吹牛乖仇,可吹牛的內(nèi)容都是我干的憾儒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼乃沙,長吁一口氣:“原來是場噩夢啊……” “哼起趾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起警儒,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤训裆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜀铲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體边琉,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年记劝,在試婚紗的時候發(fā)現(xiàn)自己被綠了变姨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡厌丑,死狀恐怖钳恕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蹄衷,我是刑警寧澤忧额,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站愧口,受9級特大地震影響睦番,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一托嚣、第九天 我趴在偏房一處隱蔽的房頂上張望巩检。 院中可真熱鬧,春花似錦示启、人聲如沸兢哭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽迟螺。三九已至,卻和暖如春舍咖,著一層夾襖步出監(jiān)牢的瞬間矩父,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工排霉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窍株,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓攻柠,卻偏偏與公主長得像球订,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瑰钮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 《UNIX 網(wǎng)絡(luò)編程卷一:套接字聯(lián)網(wǎng)API》筆記 套接字 套接字編程接口冒滩,是在 TCP/IP 協(xié)議族中,應(yīng)用層進(jìn)入...
    超net閱讀 5,809評論 2 13
  • Socket基礎(chǔ)概念 網(wǎng)絡(luò)中進(jìn)程之間如何通信飞涂? 網(wǎng)絡(luò)中進(jìn)程之間如何通信?首要解決的問題是如何唯一標(biāo)識一個進(jìn)程祈搜,否則...
    DiamondsAndRust閱讀 4,751評論 2 54
  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測試 ...
    KeKeMars閱讀 6,336評論 0 6
  • 夜靜寒風(fēng)拂窗臺 夢里無芳芳亦來 縷縷情絲何處去 情到深處愁自埋
    霧夜憂魂閱讀 496評論 2 25
  • 東伯雪鷹看著卷軸上記載的情報较店,天心樓能查出的,枯界之葉擁有者有十五位容燕,這十五位梁呈,按照天心樓記載的,罪孽最大的一位名...
    im喵小姐閱讀 254評論 0 0