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

一般認為Web服務器程序是一個長時間運行的程序(即所謂的守護進程,daemon ),它只響應來自網(wǎng)絡的請求時才發(fā)送網(wǎng)絡消息。協(xié)議的另一端是Web客戶程序,如某種瀏覽器,與服務器進行通信總是由客戶進程發(fā)起。在設計網(wǎng)絡應用時,確定總是由客戶發(fā)起請求往往能夠簡化協(xié)議和程序本身。當然一些較為復雜的的網(wǎng)絡應用還需要異步回調通信,也就是由服務器向客戶端發(fā)起請求信息。

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

Paste_Image.png

在圖中,客戶與服務器之間的信息流在其中一端是向下通過協(xié)議棧的,跨越網(wǎng)絡后,在另一端則是向上通過協(xié)議棧的。另外注意,TCP/IP協(xié)議是內核中協(xié)議棧的一部分。(回憶:在LINUX的進程中,LINUX系統(tǒng)是在內核態(tài)的,內核態(tài)被所有的進程所共享,而TCP/IP協(xié)議屬于LINUX系統(tǒng)的網(wǎng)絡協(xié)議,也就是內核中的協(xié)議棧,具體的linux內核相關的學習在linux 系統(tǒng)文集中)

同一網(wǎng)絡應用的客戶和服務器當處于不同局域網(wǎng)時,不同的局域網(wǎng)使用路由器連接到廣域網(wǎng):

Paste_Image.png

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


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

#include "unp.h" //該頭文件包含大部分網(wǎng)絡程序都需要的許多系統(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>");
    //  調用 socket 函數(shù)創(chuàng)建一個 ipv4 字節(jié)流套接字,返回一個小整數(shù)描述符,以后的所有函數(shù)調用(如隨后的connect 和 read)就用該描述符來標識這個套接字。
    //  如果socket函數(shù)調用失敗,我們就調用自己的err_sys 函數(shù)放棄程序運行。
    //  自定義的 **err_sys 函數(shù)** 輸出我們作為參數(shù)提供的出錯信息以及所發(fā)生的系統(tǒng)錯誤的描述。后面會具體描述這些函數(shù)
    if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error"); 
    // 使用bzero把整個結構清零后,置地址族為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");
        
    // 兩個左括號間加一個空格,提示比較運算符的左側同時也是一個賦值運算
    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);
}

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

Paste_Image.png

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

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

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

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

5、在頭文件unp.h中,我們使用#define 把SA定義為struct sockaddr,通用套接字地址結構。每當一個套接字函數(shù)需要一個指向某個套接字地址結構的指針時,這個指針必須強制類型轉換成一個指向通用套接字地址結構的指針。

6、fputs()函數(shù)用于將指定的字符串寫入到文件流中,其原型為:
int fputs(char * string, FILE * stream);【參數(shù)】string為將要寫入的字符串,stream為文件流指針?!痉祷刂怠砍晒Ψ祷胤秦摂?shù),失敗返回EOF。fputs()從string的開頭往文件寫入字符串,直到遇見結束符 '\0','\0' 不會被寫入到文件中。注意:fputs()可以指定輸出的文件流,不會輸出多余的字符;puts()只能向 stdout 輸出字符串,而且會在最后自動增加換行符。

協(xié)議無關性

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

為了讓圖1-5的程序能夠在IPv6上運行,我們必須修改這段代碼。下面是一個能在IPV6上運行的版本(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é)議相關的寫法,更好的做法是編寫協(xié)議無關的程序。
  • 這里的不足之處,用戶必須以點分十進制數(shù)格式給出服務器的IP地址(如適合于IPV4版本的206.168.112.219)。而我們一般都習慣于用名字代替數(shù)字。
錯誤處理:包裹函數(shù)

任何現(xiàn)實世界的程序都必須檢查每個函數(shù)調用是否返回錯誤。在上面的程序中,我們檢查socket、inet_pton、connect、read 和 fputs函數(shù)是否返回錯誤,當發(fā)生錯誤時,就調用我們自己的err_quit 或 err_sys 函數(shù)輸出一個出錯信息并終止程序的運行。但是個別情況下,當這些函數(shù)返回錯誤時,我們想做的事并非簡單地終止程序的運行。

于是,我們通過定義包裹函數(shù)來縮短程序。每個包裹函數(shù)完成實際的函數(shù)調用,檢查返回值,并在發(fā)生錯誤時終止進程。我們約定包裹函數(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ù)遇到錯誤時并不設置標準Unix的errno變量,而是把errno的值作為函數(shù)返回值返回調用者。這意味著每次調用以pthread_開頭的某個函數(shù)時,我們必須分配一個變量來存放函數(shù)返回值,以便在調用err_sys前把errno變量設置成該值。

/* 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ā)生,并以不同于終止進程的其他某種方式處理它,否則就使用這些包裹函數(shù)。

下面是匹配的時間獲取服務器程序(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;
        
    //  調用 socket 函數(shù)調用一個 ipv4 字節(jié)流套接字,返回一個小整數(shù)描述符
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    //  填寫網(wǎng)際套接字地址結構并調用bind函數(shù)
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET
    //   我們指定IP地址為INADDR_ANY,這樣要是服務器主機有多個網(wǎng)絡接口,服務器進程就可以在任意  
    //   網(wǎng)絡接口上接受客戶連接。
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(13);  /* daytime server */
        
    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    //   調用 listen 函數(shù)把該套接字轉換成一個監(jiān)聽套接字,這樣來自客戶的外來鏈接就可在該套接字上由內核接受。
    //   常值 LISTENNQ 在我們的 unp.h 頭文件中定義。它指定系統(tǒng)內核允許在這個監(jiān)聽描述符上排隊的最大客戶。
    Listen(listenfd, LISTENQ);
    for ( ; ; ) {
        connfd = Accept(listenfd, (SA *) NULL, NULL);  
                ticks = time(NULL);
                // snprintf 函數(shù)在這個字符串末尾添加一個回車符和一個換行符,隨后write函數(shù)把結果字符串寫給用戶。
                snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
                Write(connfd, buff, strlen(buff));
        Close(connfd);
    }
}

細節(jié):
1、通常情況下,服務器進程在accept調用中被投入睡眠,等待某個客戶連接的到達并被內核接受。TCP連接使用所謂的三次握手來建立連接。握手完畢時accept返回,其返回值是一個稱為已連接描述符的新描述符(本例中為connfd)。該描述符用于與新近連接的那個客戶通信。accept 為每個連接到本服務器的客戶返回一個新描述符。
2、調用sprintf無法檢查目的緩沖區(qū)是否溢出,相反,snprintf要求其第二個參數(shù)指定目的緩沖區(qū)的大小,因此可確保該緩沖區(qū)不溢出。
3、值得注意的是:許多網(wǎng)絡入侵是由黑客通過發(fā)送數(shù)據(jù),導致服務器對sprintf的調用使其緩沖區(qū)溢出而發(fā)生的,必須小心使用的函數(shù)還有gets、strcat和strcpy,通常應分別改為調用fgets、strncat和strncpy。更好的替代函數(shù)是后面才引入的strlcat 和 strlcpy,它們確保結果是正確終止的字符串。
4、本服務器一次只能處理一個客戶。如果多個客戶連接差不多同時到達,系統(tǒng)內核在某個最大數(shù)目的限制下把它們排入隊列,然后每次返回一個給accept函數(shù)。本服務器只需調用time 和 ctime 這兩個庫函數(shù),運行速度很快。同時能處理多個用戶的并發(fā)服務器有多種編程寫法,最簡單的技術是Unix的fork函數(shù)(多進程編程),或在服務器啟動時預先fork一定數(shù)量的子進程(進程池)。

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

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

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

協(xié)議族.png

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

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

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

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

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

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


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


Paste_Image.png

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

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

Paste_Image.png

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

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

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

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

最后一個版本,是聯(lián)網(wǎng)API標準,定義了兩個API,并稱它們?yōu)樵敱M網(wǎng)絡接口(DNI)

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

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


ANSI C創(chuàng)造了 size_t 數(shù)據(jù)類型,它用于作為malloc的唯一參數(shù)(待分配的字節(jié)數(shù)),或者作為read 和 write的第三個參數(shù)(待讀或寫的字節(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ī)定,存放套接字地址結構大小的函數(shù)參數(shù)具有size_t數(shù)據(jù)類型(如bind和connect的第三個參數(shù))。如果不修改這些規(guī)定,當Unix系統(tǒng)從ILP32模型轉變?yōu)長P64模型時,size_t和long都將從32位值變?yōu)?4位值。這兩個例子實際上并不需要使用64位的數(shù)據(jù)類型:套接字地址結構的長度最多也就幾百個字節(jié)。處理這些情況的辦法是使用專門設計的數(shù)據(jù)類型。套接字API對套接字地址結構使用 socklen_t 數(shù)據(jù)類型。不把這些值由32位改為64位的理由是易于為那些已在32位系統(tǒng)中編譯的應用程序提供在新的64位系統(tǒng)張的二進制代碼兼容性。


(注明1:守護進程不僅僅是一個長時間運行的程序,而是一個隨著計算機啟動,而自動運行的后臺程序,而且能在后臺運行且不跟任何終端關聯(lián)的進程——運行,一般用shell去寫一個自定義的守護進程)

(注明2:異步回調:回調就是該函數(shù)寫在高層,低層通過一個函數(shù)指針保存這個函數(shù),在某個事件的觸發(fā)下,低層通過該函數(shù)指針調用高層那個函數(shù)。異步區(qū)別于同步,在同步模式下,一段代碼調用另一段代碼時,只能采用同步調用,必須等待這段代碼執(zhí)行完返回結果后,調用方才能繼續(xù)往下執(zhí)行,有了多線程的支持,可以采用異步調用,調用方和被調方可以屬于兩個不同的線程,調用方啟動被調方線程后,不等對方返回結果就繼續(xù)執(zhí)行后續(xù)代碼。被調方執(zhí)行完畢后,通過某種手段通知調用方:結果已經(jīng)出來,請酌情處理。,這里的某種手段主要是指的線程間的通信:管道,socket)

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

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

(注明5:對于內核而言,所有打開的文件都通過文件描述符引用。文件描述符是一個非負整數(shù)。當打開一個現(xiàn)有文件或創(chuàng)建一個新文件時,內核向進程返回一個文件描述符。當讀或寫一個文件時,使用open或create返回的文件描述符表示該文件,將其作為參數(shù)傳給read或write函數(shù)。用size_t作為參數(shù)的幾個API函數(shù)如下:

  • malloc 向系統(tǒng)申請分配指定size個字節(jié)的內存空間。返回類型是 void* 類型。void* 表示未確定類型的指針。C,C++規(guī)定,void* 類型可以通過類型轉換強制轉換為任何其它類型的指針。
#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ù)
// 有幾種情況可使實際讀到的字節(jié)數(shù)少于要求讀的字節(jié)數(shù):
// 1)讀普通文件時,在讀到要求字節(jié)數(shù)之前就已經(jīng)達到了文件末端。例如,若在到達文件末端之前還有30個字節(jié),而要求讀100個字節(jié),則read返回30,下一次再調用read時,它將返回0(文件末端)。
// 2)當從終端設備讀時,通常一次最多讀一行。
// 3)當從網(wǎng)絡讀時,網(wǎng)絡中的緩存機構可能造成返回值小于所要求讀的字結束。
// 4)當從管道或FIFO讀時,如若管道包含的字節(jié)少于所需的數(shù)量,那么read將只返回實際可用的字節(jié)數(shù)。
// 5)當從某些面向記錄的設備(例如磁帶)讀時,一次最多返回一個記錄。
// 6)當某一個信號造成中斷,而已經(jīng)讀取了部分數(shù)據(jù)。

case:

  // 設置讀取的長度:
  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;
  //隨機選擇一個子線程,通過管道向其傳遞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ù)被復制到了kernel中的TCP發(fā)送緩沖區(qū)。至于數(shù)據(jù)什么時候被發(fā)往網(wǎng)絡,什么時候被對方主機接收,什么時候被對方進程讀取,系統(tǒng)調用層面不會給予任何保證和通知。之所以會阻塞,是當kernel的該socket的發(fā)送緩沖區(qū)已滿時。對于每個socket,擁有自己的send buffer和receive buffer。從Linux 2.6開始,兩個緩沖區(qū)大小都由系統(tǒng)自動調節(jié),但一般都在default和max之間浮動。

# 獲取socket的發(fā)送/接受緩沖區(qū)的大?。海ê竺娴闹凳窃贚inux 2.6.38 x86_64上測試的結果)
sysctl net.core.wmem_default       #126976
sysctl net.core.wmem_max        #131071

已經(jīng)發(fā)送到網(wǎng)絡的數(shù)據(jù)依然需要暫存在send buffer中,只有收到對方的ack后,kernel才從buffer中清除這一部分數(shù)據(jù),為后續(xù)發(fā)送數(shù)據(jù)騰出空間。接收端將收到的數(shù)據(jù)暫存在receive buffer中,自動進行確認。但如果socket所在的進程不及時將數(shù)據(jù)從receive buffer中取出,最終導致receive buffer填滿,由于TCP的滑動窗口和擁塞控制,接收端會阻止發(fā)送端向其發(fā)送數(shù)據(jù)。這些控制皆發(fā)生在TCP/IP棧中,對應用程序是透明的,應用程序繼續(xù)發(fā)送數(shù)據(jù),最終導致send buffer填滿,write調用阻塞。一般來說,由于接收端進程從socket讀數(shù)據(jù)的速度跟不上發(fā)送端進程向socket寫數(shù)據(jù)的速度,最終導致發(fā)送端write調用阻塞。而read調用的行為相對容易理解,從socket的receive buffer中拷貝數(shù)據(jù)到應用程序的buffer中。read調用阻塞,通常是發(fā)送端的數(shù)據(jù)沒有到達。)

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

// 設置一個文件描述符為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);
}

幾個重要的結論:

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

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


(注明7:read/write對連接異常的反饋行為
對應用程序來說,與另一進程的TCP通信其實是完全異步的過程:

  1. 我并不知道對面什么時候、能否收到的數(shù)據(jù)
  2. 我不知道什么時候能夠收到對面的數(shù)據(jù)
  3. 我不知道什么時候通信結束(主動退出或是異常退出、機器故障、網(wǎng)絡故障等等)
    對于1和2,采用write() -> read() -> write() -> read() ->...的序列,通過blocking read或者nonblock read+輪詢的方式,應用程序基于可以保證正確的處理流程。
    對于3,kernel將這些事件的“通知”通過read/write的結果返回給應用層。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

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

友情鏈接更多精彩內容