連線建立的根本目的是為了資料的收發。拿我們常用的網購場景舉例子,我們在瀏覽商品或者購買貨品的時候,並不會察覺到網路連線的存在,但是我們可以真切感覺到資料在客戶端和伺服器端有效的傳送, 比如瀏覽商品時商品資訊的不斷重新整理,購買貨品時顯示購買成功的訊息等。
首先來看一下傳送資料
傳送資料時常用的有三個函式,分別是 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)
每個函式都是單獨使用的,使用的場景略有不同:
第乙個函式是常見的檔案寫函式,如果把 socketfd 換成檔案描述符,就是普通的檔案寫入。
如果想指定選項,傳送帶外資料,就需要使用第二個帶 flag 的函式。所謂帶外資料,是一種基於 tcp 協議的緊急資料,用於客戶端 - 伺服器在特定場景下的緊急處理。
如果想指定多重緩衝區傳輸資料,就需要使用第三個函式,以結構體 msghdr 的方式傳送資料。
看到這裡可能會問,既然套接字描述符是一種特殊的描述符,那麼在套接字描述符上呼叫 write 函式,應該和在普通檔案描述符上呼叫 write 函式的行為是一致的,都是通過描述符控制代碼寫入指定的資料。
對於普通檔案描述符而言,乙個檔案描述符代表了開啟的乙個檔案控制代碼,通過呼叫 write 函式,作業系統核心幫我們不斷地往檔案系統中寫入位元組流。注意,寫入的位元組流大小通常和輸入引數 size 的值是相同的,否則表示出錯。
對於套接字描述符而言,它代表了乙個雙向連線,在套接字描述符上呼叫 write 寫入的位元組數有可能比請求的數量少,這在普通檔案描述符情況下是不正常的。
產生這個現象的原因在於作業系統核心為讀取和傳送資料做了很多我們表面上看不到的工作。接下來我拿 write 函式舉例,重點闡述傳送緩衝區的概念。
你一定要建立乙個概念,當 tcp 三次握手成功,tcp 連線成功建立後,作業系統核心會為每乙個連線建立配套的基礎設施,比如傳送緩衝區。
傳送緩衝區的大小可以通過套接字選項來改變,當我們的應用程式呼叫 write 函式時,實際所做的事情是把資料從應用程式中拷貝到作業系統核心的傳送緩衝區中,並不一定是把資料通過套接字寫出去。
這裡有幾種情況:
第一種情況很簡單,作業系統核心的傳送緩衝區足夠大,可以直接容納這份資料,那麼皆大歡喜,我們的程式從 write 呼叫中退出,返回寫入的位元組數就是應用程式的資料大小。
第二種情況是,作業系統核心的傳送緩衝區是夠大了,不過還有資料沒有傳送完,或者資料傳送完了,但是作業系統核心的傳送緩衝區不足以容納應用程式資料,在這種情況下,你預料的結果是什麼呢?報錯?還是直接返回?
作業系統核心並不會返回,也不會報錯,而是應用程式被阻塞,也就是說應用程式在 write 函式呼叫處停留,不直接返回。術語「掛起」也表達了相同的意思,不過「掛起」是從作業系統核心角度來說的。
那麼什麼時候才會返回呢?
別忘了,我們的作業系統核心是很聰明的,當 tcp 連線建立之後,它就開始運作起來。你可以把傳送緩衝區想象成一條包裹流水線,有個聰明且忙碌的工人不斷地從流水線上取出包裹(資料),這個工人會按照 tcp/ip 的語義,將取出的包裹(資料)封裝成 tcp 的 mss 包,以及 ip 的 mtu 包,最後走資料鏈路層將資料傳送出去。這樣我們的傳送緩衝區就又空了一部分,於是又可以繼續從應用程式搬一部分資料到傳送緩衝區裡,這樣一直進行下去,到某乙個時刻,應用程式的資料可以完全放置到傳送緩衝區裡。在這個時候,write 阻塞呼叫返回。注意返回的時刻,應用程式資料並沒有全部被傳送出去,傳送緩衝區裡還有部分資料,這部分資料會在稍後由作業系統核心通過網路傳送出去。
注意到,套接字描述本身和本地檔案描述符並無區別,在 unix 的世界裡萬物都是檔案,這就意味著可以將套接字描述符傳遞給那些原先為處理本地檔案而設計的函式。這些函式包括 read 和 write 交換資料的函式。
先從最簡單的 read 函式開始看起,這個函式的原型如下:
ssize_t read (int socketfd, void *buffer, size_t size)
read 函式要求作業系統核心從套接字描述字 socketfd讀取最多多少個位元組(size),並將結果儲存到 buffer 中。返回值告訴我們實際讀取的位元組數目,也有一些特殊情況,如果返回值為 0,表示 eof(end-of-file),這在網路中表示對端傳送了 fin 包,要處理斷連的情況;如果返回值為 -1,表示出錯。當然,如果是非阻塞 i/o,情況會略有不同,在後面的提高篇中會重點講述非阻塞 i/o 的特點。
注意這裡是最多讀取 size 個位元組。如果我們想讓應用程式每次都讀到 size 個位元組,就需要編寫下面的函式,不斷地迴圈讀取。
/* 從socketfd描述字中讀取"size"個位元組. */
size_t readn(int fd, void *buffer, size_t size) else if (result == 0)
break; /* eof(end of file)表示套接字關閉 */
length -= result;
buffer_pointer += result;
}return (size - length); /* 返回的是實際讀取的位元組數*/
}
對這個程式稍微解釋下:
用乙個客戶端 - 伺服器的例子來解釋一下讀取緩衝區和傳送緩衝區的概念。在這個例子中客戶端不斷地傳送資料,伺服器端每讀取一段資料之後進行休眠,以模擬實際業務處理所需要的時間。
#include "lib/common.h"
void read_data(int sockfd)
}int main(int argc, char **ar**)
}
對伺服器端程式解釋如下:
#include "lib/common.h"
#define message_size 102400
void send_data(int sockfd)
query[message_size] = '0';
const char *cp;
cp = query;
size_t remaining = strlen(query);
while (remaining)
remaining -= n_written;
cp += n_written;
}return;
}int main(int argc, char **ar**)
send_data(sockfd);
exit(0);
}
對客戶端程式解釋如下:
客戶端程式傳送了乙個很大的位元組流,程式執行起來之後,我們會看到服務端不斷地在螢幕上列印出讀取位元組流的過程:
而客戶端直到最後所有的位元組流傳送完畢才列印出下面的一句話,說明在此之前 send 函式一直都是阻塞的,也就是說阻塞式套接字最終傳送返回的實際寫入位元組數和請求位元組數是相等的。
如果我們把服務端的休眠時間稍微調大,把客戶端傳送的位元組數從 10240000 調整為 1024000,再次執行剛才的例子,我們會發現,客戶端很快列印出一句話:
但與此同時,服務端讀取程式還在螢幕上不斷列印讀取資料的進度,顯示出服務端讀取程式還在辛苦地從緩衝區中讀取資料。
通過這個例子我想再次強調一下:傳送成功僅僅表示的是資料被拷貝到了傳送緩衝區中,並不意味著連線對端已經收到所有的資料。至於什麼時候傳送到對端的接收緩衝區,或者更進一步說,什麼時候被對方應用程式緩衝所接收,對我們而言完全都是透明的。
既然緩衝區如此重要,我們可不可以把緩衝區搞得大大的,這樣不就可以提高應用程式的吞吐量了麼?
一段資料流從應用程式傳送端,一直到應用程式接收端,總共經過了多少次拷貝?
Socket套接字是什麼
應用層通過傳輸層進行資料通訊時,tcp和udp會遇到同時為多個應用程式程序提供併發服務的問題。多個tcp連線或多個應用程式程序可能需要 通過同乙個tcp協議埠傳輸資料。為了區別不同的應用程式程序和連線,許多計算機作業系統為應用程式與tcp ip協議互動提供了稱為套接字 socket 的介面,區分不同...
socket是什麼?套接字是什麼?
網路程式設計就是編寫程式使兩台聯網的計算機相互交換資料。這就是全部內容了嗎?是的!網路程式設計要比想象中的簡單許多。那麼,這兩台計算機之間用什麼傳輸資料呢?首先需要物理連線。如今大部分計算機都已經連線到網際網路,因此不用擔心這一點。在此基礎上,只需要考慮如何編寫資料傳輸程式。但實際上這點也不用愁,因...
什麼是套接字
常用的tcp ip協議的3種套接字型別如下所示。流套接字 sock stream 流套接字用於提供面向連線 可靠的資料傳輸服務。該服務將保證資料能夠實現無差錯 無重 送,並按順序接收。流套接字之所以能夠實現可靠的資料服務,原因在於其使用了傳輸控制協議,即tcp the transmission co...