練習題:
-2
思考題最後給你留兩道思考題吧,你可以想一想 IPv4、IPv6、本地套接字格式以及通用位址套接字,它們有什麼共性呢?如果你是 BSD 套接字的設計者,你為什麼要這樣設計呢?
-1
為什麼本地套接字格式不需要端口号,而 IPv4 和 IPv6 套接字格式卻需要端口号呢?
1
既然緩沖區如此重要,我們可不可以把緩沖區搞得大大的,這樣不就可以提高應用程式的吞吐量了麼?你可以想一想這個方法可行嗎?
2
另外你可以自己總結一下,一段資料流從應用程式發送端,一直到應用程式接收端,總共經過了多少次拷貝?
05 | 使用套接字進行讀寫:開始交流吧 https://time.geekbang.org/column/article/116043
連接配接建立的根本目的是為了資料的收發。

這張圖表達的其實是網絡程式設計中,用戶端和伺服器工作的核心邏輯。
我們先從右側的伺服器端開始看,因為在用戶端發起連接配接請求之前,伺服器端必須初始化好。右側的圖顯示的是伺服器端初始化的過程,首先初始化 socket,之後伺服器端需要執行 bind 函數,将自己的服務能力綁定在一個衆所周知的位址和端口上,緊接着,伺服器端執行 listen 操作,将原先的 socket 轉化為服務端的 socket,服務端最後阻塞在 accept 上等待用戶端請求的到來。此時,伺服器端已經準備就緒。
用戶端需要先初始化 socket,再執行 connect 向伺服器端的位址和端口發起連接配接請求,這裡的位址和端口必須是用戶端預先知曉的。
這個過程,就是著名的 TCP 三次握手(Three-way Handshake)。下一篇文章,我會詳細講到 TCP 三次握手的原理。一旦三次握手完成,用戶端和伺服器端建立連接配接,就進入了資料傳輸過程。
具體來說,用戶端程序向作業系統核心發起 write 位元組流寫操作,核心協定棧将位元組流通過網絡裝置傳輸到伺服器端,伺服器端從核心得到資訊,将位元組流從核心讀入到程序中,并開始業務邏輯的處理,完成之後,伺服器端再将得到的結果以同樣的方式寫給用戶端。
可以看到,一旦連接配接建立,資料的傳輸就不再是單向的,而是雙向的,這也是 TCP 的一個顯著特性。
當用戶端完成和伺服器端的互動後,比如執行一次 Telnet 操作,或者一次 HTTP 請求,需要和伺服器端斷開連接配接時,就會執行 close 函數,作業系統核心此時會通過原先的連接配接鍊路向伺服器端發送一個 FIN 包,伺服器收到之後執行被動關閉,這時候整個鍊路處于半關閉狀态,此後,伺服器端也會執行 close 函數,整個鍊路才會真正關閉。
半關閉的狀态下,發起 close 請求的一方在沒有收到對方 FIN 包之前都認為連接配接是正常的;而在全關閉的狀态下,雙方都感覺連接配接已經關閉。
請你牢牢記住文章開頭的那幅圖,它是貫穿整個專欄的核心圖之一。講這幅圖的真正用意在于引入 socket 的概念,請注意,以上所有的操作,都是通過 socket 來完成的。
無論是用戶端的 connect,還是服務端的 accept,或者 read/write 操作等,socket 是我們用來建立連接配接,傳輸資料的唯一途徑。
IPv4 位址族的結構
/* IPV4套接字位址,32bit值. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
/* 描述IPV4的套接字位址格式 */
struct sockaddr_in
sa_family_t sin_family; /* 16-bit */
in_port_t sin_port; /* 端口口 16-bit*/
struct in_addr sin_addr; /* Internet address. 32-bit */
/* 這裡僅僅用作占位符,不做實際用處 */
unsigned char sin_zero[8];
思考
unix系統有一種一統天下的簡潔之美:一切皆檔案,socket也是檔案。
1.像sock_addr的結構體裡描述的那樣,幾種套接字都要有位址族和位址兩個字段。這容易了解,你要與外部通信,肯定要至少告訴計算機對方的位址和使用的是哪一種位址。與遠端計算機的通信還需要一個端口号。而本地socket的不同之處在于不需要端口号,那麼就有了問題2;
2.本地socket本質上是在通路本地的檔案系統,是以自然不需要端口。遠端socket是直接将一段位元組流發送到遠端計算機的一個程序,而遠端計算機可能同時有多個程序在監聽,是以用端口号标定要發給哪一個程序。
發送緩沖區
你一定要建立一個概念,當 TCP 三次握手成功,TCP 連接配接成功建立後,作業系統核心會為每一個連接配接建立配套的基礎設施,比如發送緩沖區。
發送緩沖區的大小可以通過套接字選項來改變,當我們的應用程式調用 write 函數時,實際所做的事情是把資料從應用程式中拷貝到作業系統核心的發送緩沖區中,并不一定是把資料通過套接字寫出去。
這裡有幾種情況:
第一種情況很簡單,作業系統核心的發送緩沖區足夠大,可以直接容納這份資料,那麼皆大歡喜,我們的程式從 write 調用中退出,傳回寫入的位元組數就是應用程式的資料大小。
第二種情況是,作業系統核心的發送緩沖區是夠大了,不過還有資料沒有發送完,或者資料發送完了,但是作業系統核心的發送緩沖區不足以容納應用程式資料,在這種情況下,你預料的結果是什麼呢?報錯?還是直接傳回?
作業系統核心并不會傳回,也不會報錯,而是應用程式被阻塞,也就是說應用程式在 write 函數調用處停留,不直接傳回。術語“挂起”也表達了相同的意思,不過“挂起”是從作業系統核心角度來說的。
那麼什麼時候才會傳回呢?
實際上,每個作業系統核心的處理是不同的。大部分 UNIX 系統的做法是一直等到可以把應用程式資料完全放到作業系統核心的發送緩沖區中,再從系統調用中傳回。怎麼了解呢?
别忘了,我們的作業系統核心是很聰明的,當 TCP 連接配接建立之後,它就開始運作起來。你可以把發送緩沖區想象成一條包裹流水線,有個聰明且忙碌的勞工不斷地從流水線上取出包裹(資料),這個勞工會按照 TCP/IP 的語義,将取出的包裹(資料)封裝成 TCP 的 MSS 包,以及 IP 的 MTU 包,最後走資料鍊路層将資料發送出去。這樣我們的發送緩沖區就又空了一部分,于是又可以繼續從應用程式搬一部分資料到發送緩沖區裡,這樣一直進行下去,到某一個時刻,應用程式的資料可以完全放置到發送緩沖區裡。在這個時候,write 阻塞調用傳回。注意傳回的時刻,應用程式資料并沒有全部被發送出去,發送緩沖區裡還有部分資料,這部分資料會在稍後由作業系統核心通過網絡發送出去。
讀取資料
我們可以注意到,套接字描述本身和本地檔案描述符并無差別,在 UNIX 的世界裡萬物都是檔案,這就意味着可以将套接字描述符傳遞給那些原先為處理本地檔案而設計的函數。這些函數包括 read 和 write 交換資料的函數。
read 函數
讓我們先從最簡單的 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 個位元組,就需要編寫下面的函數,不斷地循環讀取。