發(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ā)送出去这敬。
讀取數(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)過了多少次拷貝绊序?
讓我們先看發(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í)只是冰山上的一角而已两入。