歡迎來到協定堆疊的頂端
前幾級你一直在爬一座塔:線上的位元、鄰居之間的訊框、路由橫越世界的封包,最後是 TCP 把那張雜亂的網路變成一條乾淨、可靠、依序抵達的位元組串流。底下這一切機制都是為了服務一位顧客,而這位顧客就住在這裡,住在頂端:應用層。這裡住著人們真正在乎的那些程式——網頁瀏覽器、聊天應用、電子郵件用戶端、遊戲。底下的全是水管,這裡才是水龍頭。
這裡有一個讓人解放的想法,整座堆疊就是為了把它交給你而打造的。當你寫一個連網的應用程式時,你不需要去想路由器、鏈路層訊框、壅塞視窗,或光速。你寫程式時可以當作:你只要把一塊資料交給 TCP,就能信任它會完整、依序地出現在另一端的程式裡。應用層正是網路終於再度變得簡單的地方——但前提是,你得先弄懂每個連網程式都必須回答的那兩個問題。
這兩個問題是:誰跟誰說話,以及我這個執行中的程式究竟如何碰到網路?第一個問題關乎架構——對話的形狀。第二個問題關乎一個小小的抽象,整個業界數十年前就悄悄達成共識的那個:通訊端。掌握這兩個想法,這一級接下來的內容——HTTP、cookie、電子郵件、檔案傳輸——就都成了同一主題的變奏。
誰跟誰說話:主與從
網際網路對話最常見的形狀,是 主從式模型,而這兩個名字就把整個故事說完了。伺服器是一個一直在執行的程式,坐落在一個眾所周知、穩定的位址,耐心地等著被聯絡。用戶端則是一個在「有人想要某樣東西」時才啟動的程式,它知道伺服器住在哪,並率先伸手聯絡。伺服器從不無緣無故地呼叫用戶端;它永遠只負責回應。可以想成一間餐廳:廚房一直待在一個固定地址等著,而你這位顧客,才是那個走進去、開口點東西的人。
這種不對稱不是偶然,而是一個讓全球資訊網能運作的設計選擇。因為伺服器有一個固定、找得到的位址,數以百萬計、從未見過它的用戶端,都能用同樣的方式定位它(稍後你會看到那是一個像 example.com 的名字,由網際網路的電話簿解析)。因為用戶端先開口,伺服器就能保持匿名又可被聯絡,事先完全不需要知道它的訪客是誰。這一級的大部分內容——整個全球資訊網、收信、檔案下載——都騎在這一個模式上:用戶端問,伺服器答。我們甚至給這種節奏取了名字:請求—回覆模式。
當人人平等:點對點
主從式不是唯一的形狀。在 點對點(P2P)架構裡,根本沒有永久的中央伺服器。取而代之的是,每個參與者——每個對等端——同時扮演用戶端與伺服器:一邊向其他對等端索取它想要的東西的片段,一邊把自己已經有的片段供應出去。這裡沒有廚房,也沒有顧客;而是一場百樂餐,人人既帶菜來、也拿菜走。像 BitTorrent 這樣的檔案分享系統是經典例子:你下載的一個檔案,是分成許多塊、從幾十個普通對等端那裡抵達的,而你一邊收集這些塊,就一邊立刻開始把它們分發給別人。
人們之所以願意忍受這份額外的複雜,是因為一個美麗的擴展性質。在純粹的主從式裡,想要某個檔案的人越多,負載就越往那唯一的伺服器堆,它可能變成瓶頸。在點對點裡,每一個想要這檔案的新參與者,同時也帶來了新的上傳容量去把它分送出去——需求與供給一起成長。這正是為什麼 P2P 在「把一樣熱門的東西分發給一大群人」時特別出色。但取捨是真實存在的:對等端來去無常,沒有中央目錄時要找到「誰有什麼」更難,而一條普通的家用連線,上傳通常遠比下載慢。
實務上,大多數真實系統都是混合體,各取兩者之長。一通視訊通話可能用一台中央伺服器,僅僅是為了把兩個對等端互相介紹,之後就讓媒體直接在它們之間流動。一個內容傳遞網路——你先前見過的那條「在地倉庫鏈」——在精神上,是用「許多伺服器」去回答那個 P2P 用另一種方式解決的、同一個擴展問題。誠實的結論是:「主從式對上點對點」是一道關於「工作與信任該放在哪」的選擇光譜,而不是非此即彼的硬性二選一。
通訊端:你的程式通往網路的那扇門
現在來看第二個問題:一個執行中的程式究竟如何碰到網路?答案是 通訊端——也許是整個這門學問裡最重要的單一抽象。通訊端是你的應用程式與它底下傳輸層之間的那扇門。你的程式把位元組寫進它的通訊端,TCP 就把它們帶走;從網路抵達的位元組,會出現在通訊端裡供你的程式讀取。關鍵在於,作業系統讓通訊端看起來幾乎和一個普通檔案一模一樣:你開啟它、寫入它、從它讀取、關閉它。底下那一整片龐大的複雜,都被藏在一個「行為像你早已熟悉之物」的把手後面。
但一扇門需要一個位址,正確的流量才找得到它。回想傳輸層那一級:一台電腦同時跑著許多連網程式——瀏覽器、郵件應用、音樂串流——它們全共用這台機器唯一的那個 IP 位址。把它們的對話彼此分開的那個零件,就是 連接埠號:如果 IP 位址是這棟大樓的街道地址,連接埠號就是裡頭的門牌號。一個通訊端被綁定到一組「位址加連接埠」上,正是這一組,讓傳輸層能把每一個抵達的位元組,遞送給恰好正確的那個程式。一台網頁伺服器在 TCP port 443 上聆聽安全的網頁流量;而用戶端那條對外的通訊端,會拿到一個由系統替它挑選的臨時連接埠。
這整套介面——建立一個通訊端、給它一個位址、聆聽、連線、讀取、寫入、關閉——很久以前就被標準化為 Berkeley 通訊端 API,而且幾乎每一種作業系統與程式語言都提供它。它是那份安靜而通用的契約,把「網路」從一座協定大山,變成寥寥幾個熟悉的函式呼叫。學會它一次,你就能用任何語言跟網路說話。
看一條連線活過來
讓我們走一遍「用通訊端建立一條主從連線」的編舞,因為看見這些步驟,會讓這個抽象「啪」一聲落定。伺服器與用戶端跳的是不同卻互補的舞。伺服器的工作是架好一扇門、然後守在它旁邊;用戶端的工作是找到那扇門、然後敲它。注意先前那種請求—回覆的不對稱,是如何直接出現在雙方各自呼叫的那些函式裡。
- 伺服器建立一個通訊端,並把它綁定到一個眾所周知的連接埠(比方說 TCP port 443),等於在這台機器上認領了那個門牌號。
- 伺服器呼叫 listen,接著呼叫 accept——它宣告「我開放連線了」,然後阻塞住,耐心等著有人來敲門。
- 用戶端建立自己的通訊端,並呼叫 connect,指名伺服器的位址與連接埠。在底層,這會觸發你上一級見過的 TCP 三向交握(SYN、SYN-ACK、ACK)。
- 交握一完成,伺服器的 accept 就回傳一個專屬於這個用戶端的全新通訊端,而原本那個聆聽用的通訊端則回頭去等下一位來電者。
- 現在,兩端只要透過各自的通訊端讀寫位元組,彷彿在操作一個檔案。用戶端寫入一個請求;伺服器讀取它、寫入一個回覆;用戶端讀取它。完成後,任一方關閉,這會觸發 TCP 的連線拆除。
SERVER CLIENT
------ ------
s = socket()
bind(s, port 443)
listen(s)
accept(s) <--- blocks, waiting
c = socket()
connect(c, server, 443)
...... TCP handshake: SYN, SYN-ACK, ACK ......
conn = (accept returns)
write(c, "request")
read(conn, ...) --> "request"
write(conn, "reply")
read(c, ...) --> "reply"
close(conn) close(c)TCP 還是 UDP,以及這把你帶到哪裡
每個應用程式在開啟通訊端時都要做的一個選擇,是要騎哪一種傳輸:TCP 還是 UDP。TCP 給你那條你研究過的、可靠又依序的位元組串流——每個位元組都會抵達、依序抵達,否則連線就回報失敗。這正是一個網頁或一封電子郵件想要的。但這份可靠是有代價的:任何資料之前都先要交握,而重傳可能引入延遲。UDP 把這一切全扔掉,只是發射出一個個獨立的訊息,沒有連線、沒有排序,也不保證送達。
你現在握有這一級接下來所建立其上的兩塊基石。第一,一個架構:誰發起、誰等待——大部分的全球資訊網用主從式,當「規模與共享容量」很要緊時用點對點,介於兩者之間的則是混合體。第二,一個機制:通訊端,一扇「位址加連接埠」的門,它讓整張網路感覺起來就像在讀寫一個檔案。前方每一個應用層協定——HTTP、SMTP、IMAP、FTP——歸根究柢,都不過是「用戶端與伺服器該透過那扇門送出哪些位元組、以什麼順序送」的一套約定規則罷了。
緊接著的下一篇,會撬開這些協定中最有名的那個:HTTP,全球資訊網的語言。既然你已經知道,一個瀏覽器不過是一個用戶端,在一個眾所周知的連接埠上對一台伺服器開啟通訊端、讀寫位元組,那麼 HTTP 就會顯露出它並不比這更神祕:它只是一份仔細、人類可讀的約定,講的是「那些位元組該說些什麼」。