上一講我們學(xué)習(xí)了 TCP/IP 的創(chuàng)建和歷史,以及 Linux 操作系統(tǒng)的建立和發(fā)展,相信你對(duì)網(wǎng)絡(luò)編程這棵大樹已經(jīng)有了一個(gè)宏觀上的認(rèn)識(shí),那么今天我們?cè)偻白邘撞剑嚯x看看這棵大樹的細(xì)枝末節(jié)到底是怎樣的。
從哪里開始呢?從網(wǎng)絡(luò)編程的基本概念開始說起吧。
客戶端 - 服務(wù)器網(wǎng)絡(luò)編程模型
在談?wù)摼W(wǎng)絡(luò)編程時(shí),我們首先需要建立一個(gè)概念,也就是我們今天的主題“客戶端 - 服務(wù)器”。
拿我們常用的網(wǎng)絡(luò)購物來說,我們?cè)谑謾C(jī)上的每次操作,都是作為客戶端向服務(wù)器發(fā)送請(qǐng)求,并收到響應(yīng)的例子。
這個(gè)過程具體闡釋如下:

- 當(dāng)一個(gè)客戶端需要服務(wù)時(shí),比如網(wǎng)絡(luò)購物下單,它會(huì)向服務(wù)器端發(fā)送一個(gè)請(qǐng)求。注意,這個(gè)請(qǐng)求是按照雙方約定的格式來發(fā)送的,以便保證服務(wù)器端是可以理解的;
- 服務(wù)器端收到這個(gè)請(qǐng)求后,會(huì)根據(jù)雙方約定的格式解釋它,并且以合適的方式進(jìn)行操作,比如調(diào)用數(shù)據(jù)庫操作來創(chuàng)建一個(gè)購物單;
- 服務(wù)器端完成處理請(qǐng)求之后,會(huì)給客戶端發(fā)送一個(gè)響應(yīng),比如向客戶端發(fā)送購物單的實(shí)際付款額,然后等待客戶端的下一步操作;
- 客戶端收到響應(yīng)并進(jìn)行處理,比如在手機(jī)終端上顯示該購物單的實(shí)際付款額,并且讓用戶選擇付款方式。
在網(wǎng)絡(luò)編程中,具體到客戶端 - 服務(wù)器模型時(shí),我們經(jīng)常會(huì)考慮是使用 TCP 還是 UDP,其實(shí)它們二者的區(qū)別也很簡單:TCP 中連接是誰發(fā)起的,在 UDP 中報(bào)文是誰發(fā)送的。在 TCP 通信中,建立連接是一個(gè)非常重要的環(huán)節(jié)。區(qū)別出客戶端和服務(wù)器,本質(zhì)上是因?yàn)槎呔幊棠P褪遣煌摹?/p>
服務(wù)器端需要在一開始就監(jiān)聽在一個(gè)眾所周知的端口上,等待客戶端發(fā)送請(qǐng)求,一旦有客戶端連接建立,服務(wù)器端就會(huì)消耗一定的計(jì)算機(jī)資源為它服務(wù),服務(wù)器端是需要同時(shí)為成千上萬的客戶端服務(wù)的。如何保證服務(wù)器端在數(shù)據(jù)量巨大的客戶端訪問時(shí)依然能維持效率和穩(wěn)定,這也是我們講述高性能網(wǎng)絡(luò)編程的目的。
客戶端相對(duì)來說更為簡單,它向服務(wù)器端的監(jiān)聽端口發(fā)起連接請(qǐng)求,連接建立之后,通過連接通路和服務(wù)器端進(jìn)行通信。
還有一點(diǎn)需要強(qiáng)調(diào)的是,無論是客戶端,還是服務(wù)器端,它們運(yùn)行的單位都是進(jìn)程(process),而不是機(jī)器。一個(gè)客戶端,比如我們的手機(jī)終端,同一個(gè)時(shí)刻可以建立多個(gè)到不同服務(wù)器的連接,比如同時(shí)打游戲,上知乎,逛天貓;而服務(wù)器端更是可能在一臺(tái)機(jī)器上部署運(yùn)行了多個(gè)服務(wù),比如同時(shí)開啟了 SSH 服務(wù)和 HTTP 服務(wù)。
IP 和端口
正如寄信需要一個(gè)地址一樣,在網(wǎng)絡(luò)世界里,同樣也需要地址的概念。在 TCP/IP 協(xié)議棧中,IP 用來表示網(wǎng)絡(luò)世界的地址。
前面我們提到了,在一臺(tái)計(jì)算機(jī)上是可以同時(shí)存在多個(gè)連接的,那么如何區(qū)分出不同的連接呢?
這里就必須提到端口這個(gè)概念。我們拿住酒店舉例子,酒店的地址是唯一的,每間房間的號(hào)碼是不同的,類似的,計(jì)算機(jī)的 IP 地址是唯一的,每個(gè)連接的端口號(hào)是不同的。
端口號(hào)是一個(gè) 16 位的整數(shù),最多為 65536。當(dāng)一個(gè)客戶端發(fā)起連接請(qǐng)求時(shí),客戶端的端口是由操作系統(tǒng)內(nèi)核臨時(shí)分配的,稱為臨時(shí)端口;然而,前面也提到過,服務(wù)器端的端口通常是一個(gè)眾所周知的端口。
一個(gè)連接可以通過客戶端 - 服務(wù)器端的 IP 和端口唯一確定,這叫做套接字對(duì),按照下面的四元組表示:
(clientaddr:clientport, serveraddr: serverport)
下圖表示了一個(gè)客戶端 - 服務(wù)器之間的連接:

保留網(wǎng)段
一個(gè)比較常見的現(xiàn)象是,我們所在的單位或者組織,普遍會(huì)使用諸如 10.0.x.x 或者 192.168.x.x 這樣的 IP 地址,你可能會(huì)納悶,這樣的 IP 到底代表了什么呢?不同的組織使用同樣的 IP 會(huì)不會(huì)導(dǎo)致沖突呢?
背后的原因是這樣的,國際標(biāo)準(zhǔn)組織在 IPv4 地址空間里面,專門劃出了一些網(wǎng)段,這些網(wǎng)段不會(huì)用做公網(wǎng)上的 IP,而是僅僅保留做內(nèi)部使用,我們把這些地址稱作保留網(wǎng)段。
下表是三個(gè)保留網(wǎng)段,其可以容納的計(jì)算機(jī)主機(jī)個(gè)數(shù)分別是 16777216 個(gè)、1048576 個(gè)和 65536 個(gè)。

在詳細(xì)講述這個(gè)表格之前,我們需要首先了解一下子網(wǎng)掩碼的概念。
子網(wǎng)掩碼
在網(wǎng)絡(luò) IP 劃分的時(shí)候,我們需要區(qū)分兩個(gè)概念。第一是網(wǎng)絡(luò)(network)的概念,直觀點(diǎn)說,它表示的是這組 IP 共同的部分,比如在 192.168.1.1~192.168.1.255 這個(gè)區(qū)間里,它們共同的部分是 192.168.1.0。
第二是主機(jī)(host)的概念,它表示的是這組 IP 不同的部分,上面的例子中 1~255 就是不同的那些部分,表示有 255 個(gè)可用的不同 IP。
例如 IPv4 地址,192.0.2.12,我們可以說前面三個(gè) bytes 是子網(wǎng),最后一個(gè) byte 是 host,或者換個(gè)方式,我們能說 host 為 8 位,子網(wǎng)掩碼為 192.0.2.0/24(255.255.255.0)。
有點(diǎn)暈?別著急,接下來要講的是一些基本概念。
很久很久以前,有子網(wǎng)(subnet)的分類,在這里,一個(gè) IPv4 地址的第一個(gè),前兩個(gè)或前三個(gè) 字節(jié)是屬于網(wǎng)絡(luò)的一部分。
如果你很幸運(yùn)地可以擁有一個(gè)字節(jié)的網(wǎng)絡(luò),而另外三個(gè)字節(jié)是 host 地址,那在你的網(wǎng)絡(luò)里,你有價(jià)值三個(gè)字節(jié),也就是 24 個(gè)比特的主機(jī)地址,這是什么概念呢? 2 的 24 次方,大約是一千六百萬個(gè)地址左右。這是一個(gè)“Class A”(A 類)網(wǎng)絡(luò)。

我們?cè)賮碇匦驴匆幌逻@張表格,表格第一行就是這樣的一個(gè) A 類網(wǎng)絡(luò),10 是對(duì)應(yīng)的網(wǎng)絡(luò)字節(jié)部分,主機(jī)的字節(jié)是 3,我們將一個(gè)字節(jié)的子網(wǎng)記作 255.0.0.0。
相對(duì)的,“Class B”(B 類)的網(wǎng)絡(luò),網(wǎng)絡(luò)有兩個(gè)字節(jié),而 host 只有兩個(gè)字節(jié),也就是說擁有的主機(jī)個(gè)數(shù)為 65536?!癈lass C”(C 類)的網(wǎng)絡(luò),網(wǎng)絡(luò)有三個(gè) 字節(jié),而 host 只有一個(gè) 字節(jié),也就是說擁有的主機(jī)個(gè)數(shù)為 256。
網(wǎng)絡(luò)地址位數(shù)由子網(wǎng)掩碼(Netmask)決定,你可以將 IP 地址與子網(wǎng)掩碼進(jìn)行“位與”操作,就能得到網(wǎng)絡(luò)的值。子網(wǎng)掩碼一般看起來像是 255.255.255.0(二進(jìn)制為 11111111.11111111.11111111.00000000),比如你的 IP 是 192.0.2.12,使用這個(gè)子網(wǎng)掩碼時(shí),你的網(wǎng)絡(luò)就會(huì)是 192.0.2.12 與 255.255.255.0 所得到的值:192.0.2.0,192.0.2.0 就是這個(gè)網(wǎng)絡(luò)的值。
子網(wǎng)掩碼能接受任意個(gè)位,而不單純是上面討論的 8,16 或 24 個(gè)比特而已。所以你可以有一個(gè)子網(wǎng)掩碼 255.255.255.252(二進(jìn)制位 11111111.11111111.11111111.11111100),這個(gè)子網(wǎng)掩碼能切出一個(gè) 30 個(gè)位的網(wǎng)絡(luò)以及 2 個(gè)位的主機(jī),這個(gè)網(wǎng)絡(luò)最多有四臺(tái) host。為什么是 4 臺(tái) host 呢?因?yàn)樽兓牟糠种挥凶詈髢晌?,所有的可能?2 的 2 次方,即 4 臺(tái) host。
注意,子網(wǎng)掩碼的格式永遠(yuǎn)都是二進(jìn)制格式:前面是一連串的 1,后面跟著一連串的 0。
不過一大串的數(shù)字會(huì)有點(diǎn)不好用,比如像 255.192.0.0 這樣的子網(wǎng)掩碼,人們無法直觀地知道有多少個(gè) 1,多少個(gè) 0,后來人們發(fā)明了新的辦法,你只需要將一個(gè)斜線放在 IP 地址后面,接著用一個(gè)十進(jìn)制的數(shù)字用以表示網(wǎng)絡(luò)的位數(shù),類似這樣:192.0.2.12/30, 這樣就很容易知道有 30 個(gè) 1, 2 個(gè) 0,所以主機(jī)個(gè)數(shù)為 4。
相信這個(gè)時(shí)候再去看保留網(wǎng)段,你應(yīng)該會(huì)理解表格里的內(nèi)容了。這里就不再贅述。
全球域名系統(tǒng)
如果每次要訪問一個(gè)服務(wù),都要記下這個(gè)服務(wù)對(duì)應(yīng)的 IP 地址,無疑是一種枯燥而繁瑣的事情,就像你要背下 200 多個(gè)好友的電話號(hào)碼一般無聊。
此時(shí),你應(yīng)該知道我將要表達(dá)什么。對(duì)的,正如電話簿記錄了好友和電話的對(duì)應(yīng)關(guān)系一樣,域名(DNS)也記錄了網(wǎng)站和 IP 的對(duì)應(yīng)關(guān)系。
全球域名按照從大到小的結(jié)構(gòu),形成了一棵樹狀結(jié)構(gòu)。實(shí)際訪問一個(gè)域名時(shí),是從最底層開始寫起,例如 www.google.com,www.tinghua.edu.cn等。

數(shù)據(jù)報(bào)和字節(jié)流
盡管名稱是 TCP/IP 協(xié)議棧,但是從上一講關(guān)于 OSI 和 TCP/IP 協(xié)議棧的對(duì)比中,我們看到傳輸層其實(shí)是有兩種協(xié)議的,一種是大家廣為熟悉的 TCP, 而另一種就是 UDP。
TCP,又被叫做字節(jié)流套接字(Stream Socket),注意我們這里先引入套接字 socket,套接字 socket 在后面幾講中將被反復(fù)提起,因?yàn)樗鼘?shí)際上是網(wǎng)絡(luò)編程的核心概念。當(dāng)然,UDP 也有一個(gè)類似的叫法, 數(shù)據(jù)報(bào)套接字(Datagram Socket),一般分別以“SOCK_STREAM”與“SOCK_DGRAM”分別來表示 TCP 和 UDP 套接字。
Datagram Sockets 有時(shí)稱為“無連接的 sockets”(connectionless sockets)。
Stream sockets 是可靠的,雙向連接的通訊串流。比如以“1-2-3”的順序?qū)⒆止?jié)流輸出到套接字上,它們?cè)诹硪欢艘欢〞?huì)以“1-2-3”的順序抵達(dá),而且不會(huì)出錯(cuò)。
這種高質(zhì)量的通信是如何辦到的呢?這就是由 TCP(Transmission Control Protocol)協(xié)議完成的,TCP 通過諸如連接管理,擁塞控制,數(shù)據(jù)流與窗口管理,超時(shí)和重傳等一系列精巧而詳細(xì)的設(shè)計(jì),提供了高質(zhì)量的端到端的通信方式。
這部分內(nèi)容不是我們這里講解的重點(diǎn),有感興趣的同學(xué)可以去讀《TCP/IP 詳解卷一:協(xié)議》 。
我們平時(shí)使用瀏覽器訪問網(wǎng)頁,或者在手機(jī)端用天貓 App 購物時(shí),使用的都是字節(jié)流套接字。
等等,如果是這樣,世界都用 TCP 好了,哪里有 UDP 什么事呢?
事實(shí)上,UDP 在很多場景也得到了極大的應(yīng)用,比如多人聯(lián)網(wǎng)游戲、視頻會(huì)議,甚至聊天室。如果你聽說過 NTP,你一定很驚訝 NTP 也是用 UDP 實(shí)現(xiàn)的。
使用 UDP 的原因,第一是速度,第二還是速度。
想象一下,一個(gè)有上萬人的聯(lián)網(wǎng)游戲,如果要給每個(gè)玩家同步游戲中其他玩家的位置信息,而且丟失一兩個(gè)也不會(huì)造成多大的問題,那么 UDP 是一個(gè)比較經(jīng)濟(jì)合算的選擇。
還有一種叫做廣播或多播的技術(shù),就是向網(wǎng)絡(luò)中的多個(gè)節(jié)點(diǎn)同時(shí)發(fā)送信息,這個(gè)時(shí)候,選擇 UDP 更是非常合適的。
UDP 也可以做到更高的可靠性,只不過這種可靠性,需要應(yīng)用程序進(jìn)行設(shè)計(jì)處理,比如對(duì)報(bào)文進(jìn)行編號(hào),設(shè)計(jì) Request-Ack 機(jī)制,再加上重傳等,在一定程度上可以達(dá)到更為高可靠的 UDP 程序。當(dāng)然,這種可靠性和 TCP 相比還是有一定的距離,不過也可以彌補(bǔ)實(shí)戰(zhàn)中 UDP 的一些不足。
在后面的章節(jié)中,我們將會(huì)分別介紹 TCP 和 UDP 的網(wǎng)絡(luò)編程技術(shù)。
總結(jié)
這一講我們主要介紹了客戶端 - 服務(wù)器網(wǎng)絡(luò)編程模型,初步介紹了 IP 地址、端口、子網(wǎng)掩碼和域名等基礎(chǔ)概念,以下知識(shí)點(diǎn)你需要重點(diǎn)關(guān)注一下:
- 網(wǎng)絡(luò)編程需要牢牢樹立起“客戶端”和“服務(wù)器”模型,兩者編程的方法和框架是明顯不同的。
- TCP 連接是客戶端 - 服務(wù)器的 IP 和端口四元組唯一確定的,IP 是一臺(tái)機(jī)器在網(wǎng)絡(luò)世界的唯一標(biāo)識(shí)。
- 有兩種截然不同的傳輸層協(xié)議,面向連接的“數(shù)據(jù)流”協(xié)議 TCP,以及無連接的“數(shù)據(jù)報(bào)”協(xié)議 UDP。
從下一講開始,我們將開始使用套接字編寫我們的第一個(gè)客戶端 - 服務(wù)器程序。
思考題
問:我們看到保留地址中第二行 172.16.0.0/12 描述為 16 個(gè)連續(xù)的 B 段,第三行 192.168.0.0/16 描述為 256 個(gè)連續(xù)的 C 段地址,怎么理解這種描述呢?
答:172.16.0.0172.31.255.255,因?yàn)閎類網(wǎng)絡(luò)的host只占最后兩個(gè)字節(jié),172.16172.31就代表了16個(gè)連續(xù)的b類網(wǎng)絡(luò)可用
192.168.0.0~192.168.255.255,因?yàn)閏類網(wǎng)絡(luò)的host只占最后一個(gè)字節(jié),所以從192.168.0到192.168.255,就有256個(gè)連續(xù)的c類網(wǎng)絡(luò)可用
問:章節(jié)里提到了服務(wù)端必須偵聽在一個(gè)眾所周知的端口上,這個(gè)端口怎么選擇,又是如何讓客戶端知道的呢?
答:服務(wù)器可以監(jiān)聽的端口有從0到65535,理論上這臺(tái)服務(wù)器的這個(gè)端口只要沒被占用,你都可以給服務(wù)器綁定。
如果是一些默認(rèn)的服務(wù),服務(wù)器綁的也是默認(rèn)的端口,那么客戶端是可以知道的。比如:80是給http服務(wù),443是給https服務(wù),21是給ftp服務(wù)等。否則的話,就需要服務(wù)器開發(fā)者告訴客戶端應(yīng)該連接哪個(gè)端口。