Computer Networking學(xué)習(xí)筆記[4]

發(fā)送數(shù)據(jù)

發(fā)送數(shù)據(jù)時(shí)常用的有三個(gè)函數(shù)汗唱,分別是 write傻唾、send 和 sendmsg:

ssize_t write (int socketfd, const void *buffer, size_t size)
ssize_t send (int socketfd, const void *buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)

每個(gè)函數(shù)都是單獨(dú)使用的湃崩,使用的場(chǎng)景略有不同:

第一個(gè)函數(shù)是常見的文件寫函數(shù)未荒,如果把 socketfd 換成文件描述符专挪,就是普通的文件寫入。

如果想指定選項(xiàng),發(fā)送帶外數(shù)據(jù)寨腔,就需要使用第二個(gè)帶 flag 的函數(shù)速侈。所謂帶外數(shù)據(jù),是一種基于 TCP 協(xié)議的緊急數(shù)據(jù)迫卢,用于客戶端 - 服務(wù)器在特定場(chǎng)景下的緊急處理倚搬。

如果想指定多重緩沖區(qū)傳輸數(shù)據(jù),就需要使用第三個(gè)函數(shù)靖避,以結(jié)構(gòu)體 msghdr 的方式發(fā)送數(shù)據(jù)潭枣。

發(fā)送緩沖區(qū)

你一定要建立一個(gè)概念,當(dāng) TCP 三次握手成功幻捏,TCP 連接成功建立后盆犁,操作系統(tǒng)內(nèi)核會(huì)為每一個(gè)連接創(chuàng)建配套的基礎(chǔ)設(shè)施,比如發(fā)送緩沖區(qū)篡九。

發(fā)送緩沖區(qū)的大小可以通過套接字選項(xiàng)來改變谐岁,當(dāng)我們的應(yīng)用程序調(diào)用 write 函數(shù)時(shí),實(shí)際所做的事情是把數(shù)據(jù)從應(yīng)用程序中拷貝到操作系統(tǒng)內(nèi)核的發(fā)送緩沖區(qū)中榛臼,并不一定是把數(shù)據(jù)通過套接字寫出去伊佃。

這里有幾種情況:
第一種情況很簡(jiǎn)單,操作系統(tǒng)內(nèi)核的發(fā)送緩沖區(qū)足夠大沛善,可以直接容納這份數(shù)據(jù)航揉,那么皆大歡喜,我們的程序從 write 調(diào)用中退出金刁,返回寫入的字節(jié)數(shù)就是應(yīng)用程序的數(shù)據(jù)大小帅涂。

第二種情況是,操作系統(tǒng)內(nèi)核的發(fā)送緩沖區(qū)是夠大了尤蛮,不過還有數(shù)據(jù)沒有發(fā)送完媳友,或者數(shù)據(jù)發(fā)送完了,但是操作系統(tǒng)內(nèi)核的發(fā)送緩沖區(qū)不足以容納應(yīng)用程序數(shù)據(jù)产捞,在這種情況下醇锚,你預(yù)料的結(jié)果是什么呢?報(bào)錯(cuò)坯临?還是直接返回焊唬?

操作系統(tǒng)內(nèi)核并不會(huì)返回,也不會(huì)報(bào)錯(cuò)看靠,而是應(yīng)用程序被阻塞求晶,也就是說應(yīng)用程序在 write 函數(shù)調(diào)用處停留,不直接返回衷笋。術(shù)語(yǔ)“掛起”也表達(dá)了相同的意思芳杏,不過“掛起”是從操作系統(tǒng)內(nèi)核角度來說的矩屁。

實(shí)際上,每個(gè)操作系統(tǒng)內(nèi)核的處理是不同的爵赵。大部分 UNIX 系統(tǒng)的做法是一直等到可以把應(yīng)用程序數(shù)據(jù)完全放到操作系統(tǒng)內(nèi)核的發(fā)送緩沖區(qū)中吝秕,再?gòu)南到y(tǒng)調(diào)用中返回。怎么理解呢空幻?別忘了烁峭,我們的操作系統(tǒng)內(nèi)核是很聰明的,當(dāng) TCP 連接建立之后秕铛,它就開始運(yùn)作起來约郁。你可以把發(fā)送緩沖區(qū)想象成一條包裹流水線,有個(gè)聰明且忙碌的工人不斷地從流水線上取出包裹(數(shù)據(jù))但两,這個(gè)工人會(huì)按照 TCP/IP 的語(yǔ)義鬓梅,將取出的包裹(數(shù)據(jù))封裝成 TCP 的 MSS 包,以及 IP 的 MTU 包谨湘,最后走數(shù)據(jù)鏈路層將數(shù)據(jù)發(fā)送出去绽快。這樣我們的發(fā)送緩沖區(qū)就又空了一部分,于是又可以繼續(xù)從應(yīng)用程序搬一部分?jǐn)?shù)據(jù)到發(fā)送緩沖區(qū)里紧阔,這樣一直進(jìn)行下去坊罢,到某一個(gè)時(shí)刻,應(yīng)用程序的數(shù)據(jù)可以完全放置到發(fā)送緩沖區(qū)里擅耽。在這個(gè)時(shí)候活孩,write 阻塞調(diào)用返回。注意返回的時(shí)刻乖仇,應(yīng)用程序數(shù)據(jù)并沒有全部被發(fā)送出去憾儒,發(fā)送緩沖區(qū)里還有部分?jǐn)?shù)據(jù),這部分?jǐn)?shù)據(jù)會(huì)在稍后由操作系統(tǒng)內(nèi)核通過網(wǎng)絡(luò)發(fā)送出去这敬。

image.png

讀取數(shù)據(jù)

ssize_t read (int socketfd, void *buffer, size_t size)

read 函數(shù)要求操作系統(tǒng)內(nèi)核從套接字描述字 socketfd讀取最多多少個(gè)字節(jié)(size)航夺,并將結(jié)果存儲(chǔ)到 buffer 中蕉朵。返回值告訴我們實(shí)際讀取的字節(jié)數(shù)目崔涂,也有一些特殊情況,如果返回值為 0始衅,表示 EOF(end-of-file)冷蚂,這在網(wǎng)絡(luò)中表示對(duì)端發(fā)送了 FIN 包,要處理斷連的情況汛闸;如果返回值為 -1蝙茶,表示出錯(cuò)。當(dāng)然诸老,如果是非阻塞 I/O隆夯,情況會(huì)略有不同,在后面的提高篇中我們會(huì)重點(diǎn)講述非阻塞 I/O 的特點(diǎn)。

緩沖區(qū)實(shí)驗(yàn)

我們用一個(gè)客戶端 - 服務(wù)器的例子來解釋一下讀取緩沖區(qū)和發(fā)送緩沖區(qū)的概念蹄衷。在這個(gè)例子中客戶端不斷地發(fā)送數(shù)據(jù)忧额,服務(wù)器端每讀取一段數(shù)據(jù)之后進(jìn)行休眠,以模擬實(shí)際業(yè)務(wù)處理所需要的時(shí)間愧口。

服務(wù)器端讀取數(shù)據(jù)程序


#include "lib/common.h"

void read_data(int sockfd) {
    ssize_t n;
    char buf[1024];

    int time = 0;
    for (;;) {
        fprintf(stdout, "block in read\n");
        if ((n = readn(sockfd, buf, 1024)) == 0)
            return;

        time++;
        fprintf(stdout, "1K read for %d \n", time);
        usleep(1000);
    }
}


/* 從socketfd描述字中讀取"size"個(gè)字節(jié). */
size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = buffer;
    int length = size;

    while (length > 0) {
        int result = read(fd, buffer_pointer, length);

        if (result < 0) {
            if (errno == EINTR)
                continue;     /* 考慮非阻塞的情況睦番,這里需要再次調(diào)用read */
            else
                return (-1);
        } else if (result == 0)
            break;                /* EOF(End of File)表示套接字關(guān)閉 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length);        /* 返回的是實(shí)際讀取的字節(jié)數(shù)*/
}


int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(12345);

    /* bind到本地地址,端口為12345 */
    bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    /* listen的backlog為1024 */
    listen(listenfd, 1024);

    /* 循環(huán)處理用戶請(qǐng)求 */
    for (;;) {
        clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
        read_data(connfd);   /* 讀取數(shù)據(jù) */
        close(connfd);          /* 關(guān)閉連接套接字耍属,注意不是監(jiān)聽套接字*/
    }
}

對(duì)服務(wù)器端程序解釋如下:21-35 行先后創(chuàng)建了 socket 套接字托嚣,bind 到對(duì)應(yīng)地址和端口,并開始調(diào)用 listen 接口監(jiān)聽厚骗;38-42 行循環(huán)等待連接示启,通過 accept 獲取實(shí)際的連接,并開始讀取數(shù)據(jù)溯捆;8-15 行實(shí)際每次讀取 1K 數(shù)據(jù)丑搔,之后休眠 1 秒,用來模擬服務(wù)器端處理時(shí)延提揍。

客戶端發(fā)送數(shù)據(jù)程序


#include "lib/common.h"

#define MESSAGE_SIZE 102400

void send_data(int sockfd) {
    char *query;
    query = malloc(MESSAGE_SIZE + 1);
    for (int i = 0; i < MESSAGE_SIZE; i++) {
        query[i] = 'a';
    }
    query[MESSAGE_SIZE] = '\0';

    const char *cp;
    cp = query;
    size_t remaining = strlen(query);
    while (remaining) {
        int n_written = send(sockfd, cp, remaining, 0);
        fprintf(stdout, "send into buffer %ld \n", n_written);
        if (n_written <= 0) {
            error(1, errno, "send failed");
            return;
        }
        remaining -= n_written;
        cp += n_written;
    }

    return;
}

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        error(1, 0, "usage: tcpclient <IPaddress>");

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(12345);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    int connect_rt = connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }
    send_data(sockfd);
    exit(0);
}

對(duì)客戶端程序解釋如下:31-37 行先后創(chuàng)建了 socket 套接字啤月,調(diào)用 connect 向?qū)?yīng)服務(wù)器端發(fā)起連接請(qǐng)求43 行在連接建立成功后,調(diào)用 send_data 發(fā)送數(shù)據(jù)6-11 行初始化了一個(gè)長(zhǎng)度為 MESSAGE_SIZE 的字符串流16-25 行調(diào)用 send 函數(shù)將 MESSAGE_SIZE 長(zhǎng)度的字符串流發(fā)送出去

實(shí)驗(yàn)一: 觀察客戶端數(shù)據(jù)發(fā)送行為

客戶端程序發(fā)送了一個(gè)很大的字節(jié)流劳跃,程序運(yùn)行起來之后谎仲,我們會(huì)看到服務(wù)端不斷地在屏幕上打印出讀取字節(jié)流的過程:

1K read for 989 
block in read
1K read for 990 
block in read
1K read for 991 
block in read
1K read for 992 
block in read
1K read for 993 
block in read
1K read for 994 
block in read
1K read for 995 
block in read
1K read for 996 
block in read
1K read for 997 
block in read
1K read for 998 
block in read
1K read for 999 
block in read
1K read for 1000

而客戶端直到最后所有的字節(jié)流發(fā)送完畢才打印出下面的一句話,說明在此之前 send 函數(shù)一直都是阻塞的刨仑,也就是說阻塞式套接字最終發(fā)送返回的實(shí)際寫入字節(jié)數(shù)和請(qǐng)求字節(jié)數(shù)是相等的郑诺。而關(guān)于非阻塞套接字的操作,我會(huì)在后面的文章中講解杉武。

實(shí)驗(yàn)二: 服務(wù)端處理變慢

如果我們把服務(wù)端的休眠時(shí)間稍微調(diào)大辙诞,把客戶端發(fā)送的字節(jié)數(shù)從 10240000 調(diào)整為 1024000,再次運(yùn)行剛才的例子轻抱,我們會(huì)發(fā)現(xiàn)飞涂,客戶端很快打印出一句話:

send into buffer 1024000 

但與此同時(shí),服務(wù)端讀取程序還在屏幕上不斷打印讀取數(shù)據(jù)的進(jìn)度祈搜,顯示出服務(wù)端讀取程序還在辛苦地從緩沖區(qū)中讀取數(shù)據(jù)较店。通過這個(gè)例子我想再次強(qiáng)調(diào)一下:

發(fā)送成功僅僅表示的是數(shù)據(jù)被拷貝到了發(fā)送緩沖區(qū)中,并不意味著連接對(duì)端已經(jīng)收到所有的數(shù)據(jù)容燕。至于什么時(shí)候發(fā)送到對(duì)端的接收緩沖區(qū)梁呈,或者更進(jìn)一步說,什么時(shí)候被對(duì)方應(yīng)用程序緩沖所接收蘸秘,對(duì)我們而言完全都是透明的官卡。

總結(jié)

這一講重點(diǎn)講述了通過 send 和 read 來收發(fā)數(shù)據(jù)包蝗茁,你需要牢記以下兩點(diǎn):

  • 對(duì)于 send 來說,返回成功僅僅表示數(shù)據(jù)寫到發(fā)送緩沖區(qū)成功寻咒,并不表示對(duì)端已經(jīng)成功收到评甜。
  • 對(duì)于 read 來說,需要循環(huán)讀取數(shù)據(jù)仔涩,并且需要考慮 EOF 等異常條件忍坷。

思考題

最后你不妨思考一下,既然緩沖區(qū)如此重要熔脂,我們可不可以把緩沖區(qū)搞得大大的佩研,這樣不就可以提高應(yīng)用程序的吞吐量了么?你可以想一想這個(gè)方法可行嗎霞揉?另外你可以自己總結(jié)一下旬薯,一段數(shù)據(jù)流從應(yīng)用程序發(fā)送端,一直到應(yīng)用程序接收端适秩,總共經(jīng)過了多少次拷貝绊序?

image.png

讓我們先看發(fā)送端,當(dāng)應(yīng)用程序?qū)?shù)據(jù)發(fā)送到發(fā)送緩沖區(qū)時(shí)秽荞,調(diào)用的是 send 或 write 方法骤公,如果緩存中沒有空間,系統(tǒng)調(diào)用就會(huì)失敗或者阻塞扬跋。我們說阶捆,這個(gè)動(dòng)作事實(shí)上是一次“顯式拷貝”。而在這之后钦听,數(shù)據(jù)將會(huì)按照 TCP/IP 的分層再次進(jìn)行拷貝洒试,這層的拷貝對(duì)我們來說就不是顯式的了。接下來輪到 TCP 協(xié)議棧工作朴上,創(chuàng)建 Packet 報(bào)文垒棋,并把報(bào)文發(fā)送到傳輸隊(duì)列中(qdisc),傳輸隊(duì)列是一個(gè)典型的 FIFO 隊(duì)列痪宰,隊(duì)列的最大值可以通過 ifconfig 命令輸出的 txqueuelen 來查看叼架。通常情況下,這個(gè)值有幾千報(bào)文大小酵镜。TX ring 在網(wǎng)絡(luò)驅(qū)動(dòng)和網(wǎng)卡之間碉碉,也是一個(gè)傳輸請(qǐng)求的隊(duì)列柴钻。網(wǎng)卡作為物理設(shè)備工作在物理層淮韭,主要工作是把要發(fā)送的報(bào)文保存到內(nèi)部的緩存中,并發(fā)送出去贴届。接下來再看接收端靠粪,報(bào)文首先到達(dá)網(wǎng)卡蜡吧,由網(wǎng)卡保存在自己的接收緩存中,接下來報(bào)文被發(fā)送至網(wǎng)絡(luò)驅(qū)動(dòng)和網(wǎng)卡之間的 RX ring占键,網(wǎng)絡(luò)驅(qū)動(dòng)從 RX ring 獲取報(bào)文 昔善,然后把報(bào)文發(fā)送到上層。這里值得注意的是畔乙,網(wǎng)絡(luò)驅(qū)動(dòng)和上層之間沒有緩存君仆,因?yàn)榫W(wǎng)絡(luò)驅(qū)動(dòng)使用 Napi 進(jìn)行數(shù)據(jù)傳輸。因此牲距,可以認(rèn)為上層直接從 RX ring 中讀取報(bào)文返咱。最后,報(bào)文的數(shù)據(jù)保存在套接字接收緩存中牍鞠,應(yīng)用程序從套接字接收緩存中讀取數(shù)據(jù)咖摹。這就是數(shù)據(jù)流從應(yīng)用程序發(fā)送端,一直到應(yīng)用程序接收端的整個(gè)過程难述,你看懂了嗎萤晴?上面的任何一個(gè)環(huán)節(jié)稍有積壓,都會(huì)對(duì)程序性能產(chǎn)生影響胁后。但好消息是店读,內(nèi)核和網(wǎng)絡(luò)設(shè)備供應(yīng)商已經(jīng)幫我們把一切都打點(diǎn)好了,我們看到和用到的攀芯,其實(shí)只是冰山上的一角而已两入。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市敲才,隨后出現(xiàn)的幾起案子裹纳,更是在濱河造成了極大的恐慌,老刑警劉巖紧武,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剃氧,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡阻星,警方通過查閱死者的電腦和手機(jī)朋鞍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妥箕,“玉大人滥酥,你說我怎么就攤上這事∑璐保” “怎么了坎吻?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)宇葱。 經(jīng)常有香客問我瘦真,道長(zhǎng)刊头,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任诸尽,我火速辦了婚禮原杂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘您机。我一直安慰自己穿肄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布际看。 她就那樣靜靜地躺著被碗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仿村。 梳的紋絲不亂的頭發(fā)上锐朴,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音蔼囊,去河邊找鬼焚志。 笑死,一個(gè)胖子當(dāng)著我的面吹牛畏鼓,可吹牛的內(nèi)容都是我干的酱酬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼云矫,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼膳沽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起让禀,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挑社,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后巡揍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痛阻,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年腮敌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阱当。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡糜工,死狀恐怖弊添,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捌木,我是刑警寧澤油坝,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響免钻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜崔拥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一极舔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧链瓦,春花似錦拆魏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至贴膘,卻和暖如春卖子,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刑峡。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工洋闽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人突梦。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓诫舅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親宫患。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刊懈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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