通訊的基本功:send、receive 與 socket
send 與 receive 兩個動作,搭配同步/非同步、訊息目的地、可靠性、順序四個面向,構成所有通訊的基礎語彙。
先讀原文開場,旁邊就是白話
這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。
send 與 receive 兩個動作,搭配同步/非同步、訊息目的地、可靠性、順序四個面向,構成所有通訊的基礎語彙。
行程間通訊的特性
send 與 receive 兩個動作,搭配同步/非同步、訊息目的地、可靠性、順序四個面向,構成所有通訊的基礎語彙。
深度探秘
兩個動作撐起整個世界:send 與 receive
行程怎麼對話?
在分散式系統裡,不同電腦上的行程 (process) 不能像同一台電腦上的程式那樣共用記憶體,它們唯一能做的就是傳訊息。而傳訊息說穿了只有兩個動作:
- send:把一串位元組送到某個目的地
- receive:從某個地方把訊息收進來
每個目的地都配著一個佇列 (queue):傳送方把訊息丟進遠端的佇列,接收方從自己本地的佇列把訊息拿出來。
四個關鍵面向
光是 send/receive 還不夠,我們得問四個問題,才能描述一個通訊到底長怎樣:
| 面向 | 在問什麼 |
|---|---|
| 同步 vs 非同步 | 送/收的時候要不要『卡住』等對方? |
| 訊息目的地 | 訊息要送到哪裡?怎麼指定? |
| 可靠性 | 訊息會不會掉?會不會壞掉或重複? |
| 順序 | 收到的順序,會不會跟送出的順序不一樣? |
這四個面向就是接下來整章的共同語彙。
通訊的核心只有 send 與 receive 加一個佇列,再用同步性、目的地、可靠性、順序四個面向去描述它。
生活妙喻
同步是打電話,非同步是寄信
同步通訊:打電話
同步 (synchronous) 通訊就像打電話:你撥了號,得一直握著話筒等對方接起來才能講話;對方也得停下手邊的事來接。雙方在『每一則訊息』都會同步等待對方——send 與 receive 都是阻塞 (blocking) 操作。
撥號的人會卡在那裡,直到對面有人喊『喂』。
非同步通訊:寄信
非同步 (asynchronous) 通訊則像寄信:你把信投進信箱(複製到本地緩衝區)就可以轉身去做別的事,信會在背景被送出,你不用站在信箱旁邊等。這就是非阻塞 (non-blocking) 的 send。
至於收信端,可以選擇『一直守在信箱旁』(阻塞收),也可以『先去忙、信來了再用輪詢或鈴聲通知我』(非阻塞收)。
為什麼現代系統多半用阻塞收?
因為像 Java 這種支援多執行緒的環境,可以派一個執行緒專門守信箱(阻塞收),其他執行緒照常工作——又簡單又不浪費。非阻塞收反而要處理『訊息半路插進控制流』的複雜度,所以反而少用。
同步像打電話(雙方都得停下等待),非同步像寄信(投了就走,背景幫你送)。
實用超能力
可靠性與順序怎麼看
可靠性:兩個性質
判斷一個通訊服務可不可靠,看兩個性質:
- 有效性 (validity):訊息保證會送達嗎?就算掉了『合理數量』的封包,最終還是會到,才算可靠;只要掉一個封包就可能送不到,就是不可靠。
- 完整性 (integrity):到達的訊息有沒有壞掉、有沒有被重複?
順序:收到的順序 = 送出的順序?
有些應用要求訊息按傳送者的順序 (sender order) 抵達。對這些應用來說,亂序到達就等同一種失效 (failure)。
想像聊天室:如果你先送『你好』再送『再見』,對方卻先看到『再見』,那就出事了。
位置透明性的小技巧
如果用戶端直接用固定 IP 指名服務,那服務就被釘死在那台機器上,不能搬家。解法是:
- 用戶端只記服務的名字,再透過名稱伺服器 (name server) 或 binder 在執行時把名字翻譯成位置。
這樣服務就能換機器,用戶端完全無感——這叫位置透明性 (location transparency)。
用戶端: 我要找『天氣服務』
名稱伺服器: 它現在住在 138.37.88.249:6789
用戶端: 好,我往那裡送
可靠性看有效性(不掉)與完整性(不壞不重複);順序看到達順序是否等於送出順序;用名稱伺服器換得位置透明性。
同步像打電話,撥號者得握著話筒卡在原地等對方接;非同步像寄信,投進信箱就轉身離開,郵差在背景幫你送。
每個目的地都有一個信箱,別人把信投進來、你有空再去收。送信的人把信丟進你的信箱,你從自己的信箱拿信。
你只記得一個好記的名字(服務名),背後對應到誰、住哪裡由總機(名稱伺服器)幫你接通,警員換人換位置你都不必改。
本節字彙
Socket 與 port:通訊的端點
socket 是行程通訊的端點,綁定到一個 Internet 位址加一個 port。一個 port 只有一個接收者卻可有多個傳送者。
深度探秘
socket 是行程通訊的『插座』
socket 是什麼?
不管是 UDP 還是 TCP,行程要通訊都得透過一個叫 socket 的抽象——它是通訊的端點 (endpoint)。一則訊息就是從一個行程的 socket,傳到另一個行程的 socket。
要能收訊息,一個 socket 必須綁定 (bind) 到:
- 本機的某個 Internet 位址,加上
- 一個 local port(一個整數,代表電腦內部的訊息目的地)
port 的規矩
每台電腦有 2^16(約 6 萬多)個可用的 port。幾條重要規則:
- 一個 port 只能有一個接收行程(IP multicast 是例外),但
- 任意多個行程都可以送訊息到同一個 port
- 一個行程可以同時用多個 port 來收訊息
- 同一台電腦上,不同行程不能共用同一個 port
每個 socket 都綁定一種協定——不是 UDP 就是 TCP。
伺服器與用戶端的分工
伺服器會把 socket 綁到一個公開的 port,讓用戶端知道往哪送;用戶端則隨便綁一個空閒的本地 port 就好。
socket 是通訊端點,要綁到(Internet 位址, local port)才能收訊息;一個 port 一個接收者,但可有多個傳送者。
生活妙喻
公司大樓的總機與分機
把電腦想成一棟公司大樓
- Internet 位址 = 大樓的地址(找到是哪棟樓)
- port = 大樓裡的分機號碼(找到樓裡的哪個人)
- socket = 桌上那台電話機(真正收發的設備)
要打給某人,你得同時知道『地址 + 分機』。一通電話只會響在『綁定那個分機的那台電話』上。
為什麼一個 port 只有一個接收者?
因為一支分機同時只接給一個人。但反過來,任何人都能打進這支分機(多個傳送者),就像很多客戶都能撥同一支客服分機。
伺服器公開 port = 把客服專線印在名片上
伺服器把它的 port『公告』出去,就像把客服分機印在名片上,客戶照著撥就能找到它。客戶自己用哪支分機打出去?隨便挑一支沒人用的就行。
flowchart LR 用戶端socket -->|訊息| 約定的port 約定的port --> 伺服器socket 其他傳送者 -->|也能送到| 約定的port
Internet 位址是大樓地址、port 是分機、socket 是桌上電話;一支分機一個接聽者,但人人都能打進來。
實用超能力
用 Java 的 InetAddress 處理位址
不必手動處理 IP 的細節
UDP 和 TCP 底下的 IP 封包都送往 Internet 位址。Java 提供 InetAddress 類別來代表位址,而且很貼心——你不必知道實際的 IP 數字,只要給它一個 DNS 主機名稱,它會自動去查。
// 給 DNS 名稱,自動查出對應的 Internet 位址
InetAddress aComputer =
InetAddress.getByName("bruno.dcs.qmul.ac.uk");
// 若名稱查不到,會丟出 UnknownHostException
為什麼這樣設計很聰明?
InetAddress 把『位址到底用幾個位元組表示』這件事包起來:
- IPv4 用 4 個位元組
- IPv6 用 16 個位元組
你的程式碼完全不用改,就能同時相容兩者——這就是封裝 (encapsulation) 的威力:細節藏在類別內部,外面只看到乾淨的介面。
小結一張表
| 角色 | 該做的事 |
|---|---|
| 伺服器 | 把 socket 綁到公開的 server port,等人來連 |
| 用戶端 | 綁一個空閒的本地 port,往伺服器的 port 送 |
用 InetAddress.getByName 給 DNS 名稱就能取得位址,且自動相容 IPv4/IPv6——位址的位元組細節被封裝起來了。
Internet 位址是大樓地址(找對棟),port 是分機號碼(找對人),socket 是真正收發的電話機;要找到某人得同時知道地址加分機。
一支客服分機同一時間只給一位專員接聽,但任何客戶都能撥進這支分機——一個接聽者、無數來電者。
你只記聯絡人名字(DNS 名稱),手機自動查出號碼(IP);號碼是市話還是國際碼(IPv4/IPv6)你完全不必操心。
本節字彙
兩塊積木:UDP 與 TCP
UDP 把一個訊息當一個獨立封包寄出,不確認也不重送。簡單、低負擔,但會掉封包、可能亂序——屬於 omission failures。
UDP 資料報通訊
UDP 把一個訊息當一個獨立封包寄出,不確認也不重送。簡單、低負擔,但會掉封包、可能亂序——屬於 omission failures。
深度探秘
把一則訊息當一個獨立封包寄出去
UDP 在做什麼?
UDP(User Datagram Protocol)提供最簡單的通訊形式:把一則訊息當成一個獨立的封包——稱為資料報 (datagram)——直接寄出去。
關鍵是:沒有確認 (acknowledgement),也沒有重送 (retry)。送出去之後就不管了,到不到是另一回事。
要送或收,行程得先建一個 socket,綁到本機位址與一個 local port。receive 會連同訊息一起回傳傳送者的位址與 port,讓你能回信。
幾個要注意的細節
| 議題 | 說明 |
|---|---|
| 訊息大小 | 收方要先準備一個固定大小的位元組陣列;訊息太大會被截斷 (truncate)。多數環境限制約 8 KB。 |
| 阻塞 | send 通常不阻塞(交給底層就返回),receive 會阻塞直到訊息到達。 |
| Timeout | 可在 socket 上設逾時,避免對方掛掉時 receive 無限等下去。 |
| Receive from any | receive 不指定來源,任何送到這個 socket 的訊息都收,並告訴你寄件人是誰。 |
UDP 把訊息當獨立資料報寄出,不確認不重送;送不阻塞、收會阻塞,訊息太大會被截斷。
生活妙喻
UDP 就像投明信片
明信片的特性
UDP 寄資料報,就像寄一張明信片:
- 你貼上地址投進郵筒就走,不會收到『已送達』回執(沒有確認)
- 萬一郵局弄丟了,你不會知道、也不會自動補寄(沒有重送)
- 好幾張明信片可能不照你寄的順序到(可能亂序)
- 但明信片很輕便、成本低(沒有保證送達的額外負擔)
為什麼有人就愛用明信片?
因為對某些情境,『偶爾掉一張』完全可以接受,而換來的是速度快、開銷小:
- DNS(查網域名稱):查不到就再問一次,沒必要為單次查詢建立可靠連線。
- VoIP(網路語音):晚到的語音封包就算補送回來也沒用了,丟掉繼續講比卡頓好。
重點不是『可靠最好』,而是『合不合用』。對即時、容錯、講求輕量的應用,UDP 的明信片風格反而是優點。
UDP 像投明信片:沒回執、不補寄、可能亂序,但輕便快速——對 DNS、VoIP 這類容忍掉包的應用反而剛好。
實用超能力
UDP 的失效模型與 Java 寫法
UDP 的失效模型
UDP 資料報會遇到兩種狀況:
- 遺漏失效 (omission failures):因為檢查碼 (checksum) 錯誤、或來源/目的地沒有緩衝空間,封包偶爾被丟掉。
- 亂序 (ordering):訊息有時不照傳送者的順序到達。
好消息:靠 checksum,收到的訊息『被弄壞卻沒被發現』的機率極低——所以完整性大致有保障,問題主要在有效性(會掉)。
想要可靠怎麼辦?
用 UDP 的應用得自己加檢查。一個常見手法是確認 (acknowledgement):收到就回一聲,沒回就重送,把不可靠的服務疊成可靠的。
Java 的兩個類別
// 傳送端:建 socket、打包、送出
DatagramSocket aSocket = new DatagramSocket();
byte[] m = message.getBytes();
InetAddress aHost = InetAddress.getByName(hostName);
DatagramPacket request =
new DatagramPacket(m, m.length, aHost, 6789);
aSocket.send(request);
// 接收端:準備緩衝區、阻塞等訊息
byte[] buffer = new byte[1000];
DatagramPacket reply =
new DatagramPacket(buffer, buffer.length);
aSocket.receive(reply); // 會阻塞到訊息到達
DatagramPacket:裝訊息+長度+目的地位址與 portDatagramSocket:負責 send / receive,可用setSoTimeout設逾時
UDP 的失效是遺漏(會掉)與亂序;完整性靠 checksum 大致無虞,要可靠得自己加確認與重送。Java 用 DatagramPacket 與 DatagramSocket。
貼上地址投進郵筒就走,沒有送達回執、弄丟也不補寄、好幾張可能亂序到,但輕便快速、成本低。
明信片本身不保證送達,但你可以約定『收到請回一張』,沒收到回信就重寄,自己堆出可靠性。
收方準備的緩衝區若比訊息小,超出的部分就被剪掉,像信紙塞不進信封就被裁短。
本節字彙
TCP 串流通訊
TCP 提供一條雙向位元組串流,靠確認、序號、重送與流量控制,隱藏訊息大小、遺失、重複與亂序等網路細節。
深度探秘
一條沒有邊界的雙向位元組串流
TCP 給你什麼抽象?
TCP(Transmission Control Protocol)提供的不是一封封訊息,而是一條位元組串流 (stream of bytes):可以往裡寫、也可以從裡讀,而且沒有訊息邊界——你寫進去的『一則訊息』跟對方讀出來的不一定對齊。
TCP 幫你把網路的種種麻煩藏起來:
| 網路問題 | TCP 怎麼處理 |
|---|---|
| 訊息大小 | 由底層決定收集多少資料再打包成 IP 封包送出 |
| 遺失 | 用確認 (acknowledgement) 機制;逾時沒收到確認就重送 |
| 流量控制 (flow control) | 配速:寫太快就阻塞寫方,等讀方消化 |
| 重複與亂序 | 每個 IP 封包帶序號,據此剔除重複、重排亂序 |
先連線,再溝通
兩個行程必須先建立連線才能在串流上通訊。一方扮用戶端(發出 connect 請求),一方扮伺服器(accept 接受)。連好之後,雙方各有一條輸入串流與一條輸出串流,地位平等。
TCP 是沒有訊息邊界的雙向位元組串流,靠確認+重送、序號、流量控制隱藏遺失、重複、亂序與速差;通訊前要先建立連線。
生活妙喻
TCP 像打電話前先接通的專線
從明信片升級到專線電話
如果說 UDP 是『投明信片』,那 TCP 就是先撥號接通、確認雙方都在線上,再開始講話的專線電話:
- 先建立連線:撥號(connect)→ 對方接聽(accept),通話才開始。
- 保證聽清楚:話沒聽到,會請對方再講一次(確認+重送)。
- 配速不淹沒:你講太快、對方記不下來時,會請你慢一點(流量控制)。
- 照順序聽到:你先說的話對方先聽到(序號重排)。
為什麼說『沒有訊息邊界』?
打電話時,你說的話是連續的聲音流,不是一句句獨立的封包。對方聽到的是一段連續語音,得自己判斷句子在哪裡斷。TCP 也一樣:它只保證『位元組照順序、不漏不重複』,但不替你標記哪裡是一則訊息的結尾。
sequenceDiagram participant 用戶端 participant 伺服器 用戶端->>伺服器: connect 請求 伺服器-->>用戶端: accept 接受 用戶端->>伺服器: 寫入位元組串流 伺服器-->>用戶端: 寫回位元組串流
TCP 像先接通的專線電話:先連線、保證聽清楚、會配速、照順序;但話語是連續流,沒有現成的訊息邊界。
實用超能力
TCP 的失效模型、用途與 Java 寫法
TCP 也不是『絕對可靠』
TCP 用 checksum 擋損壞、用序號擋重複、用逾時重送補遺失,所以正常情況下保證送達。但如果:
- 封包遺失超過某個上限,或
- 網路被切斷、嚴重壅塞
TCP 收不到確認,過一陣子就會宣告連線斷掉。所以嚴格說,TCP 並非在所有情況下都可靠。連線斷掉時還有兩個尷尬:
- 行程分不清是網路斷了還是對方行程掛了。
- 也無法確定自己最近送出的訊息對方到底收到沒。
哪些服務跑在 TCP 上?
HTTP(網頁)、FTP(檔案傳輸)、Telnet(遠端終端)、SMTP(寄信)——都需要可靠、有序的位元組串流。
Java 的兩個類別
// 伺服器:開一個 listening socket 等連線
ServerSocket listenSocket = new ServerSocket(7896);
while (true) {
Socket clientSocket = listenSocket.accept(); // 阻塞等連線
// 通常為每個用戶端開一條新執行緒來服務
new Connection(clientSocket);
}
// 用戶端:建 Socket 即同時連到指定主機與 port
Socket s = new Socket(hostName, 7896);
DataInputStream in = new DataInputStream(s.getInputStream());
DataOutputStream out = new DataOutputStream(s.getOutputStream());
out.writeUTF(message); // 寫入輸出串流
String reply = in.readUTF(); // 從輸入串流讀
ServerSocket:伺服器用來監聽連線,accept取得一個新的Socket。Socket:一對行程的連線兩端,提供getInputStream/getOutputStream。- 伺服器常為每個用戶端開一條執行緒,這樣等某個用戶端輸入時不會卡住其他人。
TCP 正常時保證送達,但連線斷掉時分不清是網路還是對方掛了;HTTP/FTP/SMTP 都用它。Java 用 ServerSocket 與 Socket,常為每個用戶端開一條執行緒。
通話前要先撥號(connect)對方接聽(accept)才開始;過程中保證聽清楚、會配速、照順序,比明信片可靠得多。
對方聽到的是一段連續聲音,得自己判斷句子在哪裡斷;TCP 只保證位元組順序與完整,不替你標記訊息結尾。
你講太快、對方記不下來時,會請你放慢;TCP 在讀方來不及消化時會阻塞寫方,避免淹沒對方。
本節字彙
把資料打包送上路:外部資料表示與 marshalling
不同電腦對整數位元組順序(big-endian/little-endian)、浮點數、字元編碼的存法都不同,要溝通就得約定一套外部格式,並把結構攤平成位元組。
為什麼需要外部資料表示
不同電腦對整數位元組順序(big-endian/little-endian)、浮點數、字元編碼的存法都不同,要溝通就得約定一套外部格式,並把結構攤平成位元組。
深度探秘
程式裡是結構,網路上只有位元組
兩個世界的落差
程式執行時,資料以結構化的物件存在記憶體裡(一堆互相連結的物件);但網路上傳的訊息只是一串位元組 (sequence of bytes)。
所以不管用什麼通訊方式,送出前都得把資料結構攤平 (flatten) 成位元組序列,到了對面再重建 (rebuild) 回來。
為什麼不能直接把記憶體內容倒出來送?
因為不同電腦對同一個值的存法不一樣:
| 差異點 | 說明 |
|---|---|
| 整數位元組順序 | big-endian:最高位元組在前;little-endian:最低位元組在前 |
| 浮點數 | 不同架構的表示法也不同 |
| 字元編碼 | 有的用 ASCII(一字元 1 byte),有的用 Unicode(一字元 2 bytes) |
如果甲機把整數直接倒出來,乙機照自己的習慣解讀,數字就錯了。所以需要一套大家都同意的格式——外部資料表示 (external data representation)。
程式裡資料是結構化物件,網路只能傳位元組;不同電腦對整數順序、浮點數、字元編碼的存法不同,所以要約定一套外部資料表示。
生活妙喻
big-endian 與 little-endian:先寫哪一位?
一個簡單到吵不完的問題
要寫數字『一千兩百三十四』(1234),你會從哪一端開始寫?
- big-endian:從最大位寫起——千、百、十、個(1-2-3-4)。像我們平常寫阿拉伯數字。
- little-endian:從最小位寫起——個、十、百、千(4-3-2-1)。
兩種寫法都對,只要讀的人知道作者用哪一種。問題是:如果甲用 big-endian 寫、乙卻當 little-endian 讀,1234 就會被讀成天差地別的數字。
這就是著名的『大小端之爭』——名字其實出自《格列佛遊記》裡為了『水煮蛋該從大端還小端敲開』而開戰的小人國。
字元編碼:同一個字母,不同的代號
字元也一樣:ASCII 用 1 個位元組存一個英文字母,Unicode 為了容納全世界的文字,用 2 個位元組。沒講好就會雞同鴨講。
解法:先講好一套『通用文字』
就像聯合國開會大家先約定用某種官方語言,電腦之間也先約定一套外部格式,發送前翻成它、收到後翻回自己的習慣。
big-endian 從最高位寫起、little-endian 從最低位寫起;字元編碼也各有不同。沒講好就會誤讀,所以要先約定一套通用格式。
實用超能力
marshalling:自動又可靠的打包術
兩條讓電腦互通的路
要讓任意兩台電腦交換二進位資料,有兩種作法:
- 傳前轉成約定的外部格式,收到再轉回本地格式。(若兩台已知是同型,可省略轉換)
- 用傳送者的格式傳,並夾帶『我用哪種格式』的標示,由收方視需要轉換。
注意:位元組本身在傳輸途中永遠不會被改動,改的是『怎麼解讀它們』。
marshalling 與 unmarshalling
- marshalling(封送):把一堆資料項組裝成適合放進訊息的形式——也就是把結構化資料與基本值翻成外部資料表示。
- unmarshalling(解封送):在抵達端把它們拆開、還原成等價的資料項。
為什麼最好交給機器做?
因為 marshalling 得照顧到每個基本元件最細的表示細節,手工做極易出錯;自動產生的程序還能順便處理緊湊度 (compactness)。所以在 RMI/RPC 中,這些工作通常由中介軟體 (middleware) 自動完成,應用程式設計師根本不用碰。
甲機物件 --marshalling--> 約定外部格式的位元組 --網路--> 乙機
乙機 --unmarshalling--> 還原成等價物件
marshalling 是把資料攤平成外部格式、unmarshalling 是還原;位元組不變、變的是解讀方式。手工易錯,通常由中介軟體自動完成。
各國代表母語不同,先約定一種官方語言發言與翻譯;電腦傳資料前也先翻成一套大家同意的外部格式,避免誤讀。
1234 可以從最高位(big)或最低位(little)開始寫,兩種都對,但讀的人必須知道作者用哪種,否則數字就讀錯了。
手工打包易漏易錯,交給專業打包公司(中介軟體)按標準裝箱、到目的地拆箱還原,又快又不出錯。
本節字彙
三種風格:CORBA CDR、Java 序列化、XML
CDR 是緊湊二進位、不帶型別(雙方先知道型別);Java 序列化是二進位但夾帶完整型別;XML 是文字、自我描述、可讀但較肥。
深度探秘
三種打包資料的哲學
三位選手登場
同樣是把資料攤平送出去,業界有三種代表性風格:
1. CORBA CDR(Common Data Representation)
- 緊湊的二進位格式
- 不帶型別資訊:因為假設收發雙方事先就知道資料的順序與型別(透過 IDL 介面定義)
- 像 string 會以『長度(unsigned long)+字元』表示,並對齊到對應位元組邊界
2. Java 物件序列化(serialization)
- 二進位,但夾帶完整型別資訊(類別名稱+版本號)
- 收方就算事先不知型別,也能從序列化內容自行重建物件
- 只給 Java 用
3. XML(Extensible Markup Language)
- 文字格式,用標籤 (tags) 描述結構,自我描述
- 人類可讀、跨平台,但訊息較肥、處理較慢
| 風格 | 編碼 | 帶型別資訊? | 適用範圍 |
|---|---|---|---|
| CORBA CDR | 二進位 | 否(雙方先約定) | 多語言 RMI |
| Java 序列化 | 二進位 | 是(完整) | 僅 Java |
| XML | 文字 | 是(標籤/命名空間) | 跨應用、Web 服務 |
CDR 緊湊二進位但不帶型別(靠 IDL 先約定);Java 序列化二進位且帶完整型別、只給 Java;XML 文字、自我描述、可讀但較肥。
生活妙喻
寄包裹的三種貼標方式
同樣寄包裹,標籤怎麼貼?
想像你要寄一份『個人資料:姓名 Smith、地點 London、年份 1984』給朋友。
CDR:什麼標籤都不貼的緊湊包裹
你和朋友事先講好:『第一格是姓名、第二格是地點、第三格是年份』。所以包裹裡只放值,不寫欄位名——最省空間,但雙方得先有共識。
像兩個有默契的老同事,遞個眼神就懂。
Java 序列化:附上完整出貨單的包裹
包裹裡除了值,還夾一張詳單:『這是 Person 類別、版本 X、欄位有 name(String)、place(String)、year(int)』。朋友就算第一次收到也能照單組裝——但這套詳單格式只有 Java 看得懂。
XML:每樣東西都貼名牌的包裹
<person id="123456789">
<name>Smith</name>
<place>London</place>
<year>1984</year>
</person>
每個值都用 <name>...</name> 這種成對標籤包起來,誰拿到都看得懂、人眼也讀得出來——代價是包裹變大、處理變慢。
自我描述 vs 事先約定
CDR 不自我描述(靠默契),Java 與 XML 都自我描述(自帶說明),只是 Java 把說明塞進序列化內容,XML 可引用外部的命名空間 (namespaces) 來定義標籤意義。
CDR 像有默契不貼標的緊湊包裹、Java 像附完整出貨單但只有 Java 懂、XML 像每樣都貼名牌人人可讀但較大。
實用超能力
怎麼選?以及 reflection 的妙用
兩個關鍵的設計分水嶺
挑序列化方案時,常看兩件事:
要不要事先約定型別?
- CDR:要。靠 CORBA IDL 描述型別,再由介面編譯器自動產生 marshalling/unmarshalling 程式。
- Java:不用事先寫型別描述。靠 reflection(反射) 在執行時自動查出類別名稱、欄位的名稱與型別與值,完全通用地序列化。
- XML:用標籤自我描述,產生方式依情境而定。
要緊湊還是要可讀/可攜?
- 要省頻寬與速度 → 二進位(CDR / Java)
- 要跨平台、人可讀、跨不同應用 → XML(可壓縮以省頻寬)
reflection 為什麼這麼省事?
Java 的 reflection 能在執行時『反問』一個類別:你叫什麼名字?有哪些欄位?型別是什麼?正因如此,不必為每種物件手寫專屬的 marshalling 函式(不像 CORBA 要先用 IDL 描述)。反序列化時,再用類別名稱建類別、依欄位型別建構子,重建出物件。
小提醒:transient
有些東西不該被序列化——例如指向本地檔案或 socket 的參考,換台機器就失效了。Java 可把這類變數宣告為 transient,序列化時跳過它。
public class Session implements Serializable {
private String userName; // 會被序列化
private transient Socket conn; // 跳過,不序列化本地資源
}
更輕量的後起之秀
除了三巨頭,Google 的 protocol buffers 與 JSON 也很流行,朝著比 XML 更輕量的方向走。
CDR 靠 IDL 事先約定並自動產生程式,Java 靠 reflection 通用自動序列化(本地資源用 transient 跳過),XML 自我描述可讀但較肥;另有 protocol buffers、JSON 更輕量。
雙方事先約定每格放什麼,包裹只放值不寫欄位名,最省空間,但得靠事前共識(IDL)。
包裹夾一張寫明類別、版本、欄位型別的詳單,收方照單重建;缺點是這套詳單格式只有 Java 認得。
每個值都用成對標籤包起來,誰收到都看得懂、人眼也讀得出,代價是包裹較大、處理較慢。
本節字彙
遠端物件參考
在分散式物件模型裡,要指名呼叫哪個遠端物件,需要一個跨整個系統都唯一、且時空上不重複的識別碼——遠端物件參考。
深度探秘
怎麼指名『那一個』遠端物件?
問題的起點
在支援分散式物件模型的語言(如 Java、CORBA)裡,用戶端呼叫某個遠端物件的方法時,要送一則呼叫訊息到主管該物件的伺服器行程。問題是:一台伺服器上可能有成千上萬個物件,這則訊息得指名到底要呼叫哪一個。
這個『跨整個分散式系統都有效的識別碼』就是遠端物件參考 (remote object reference)。
它有哪些用途?
- 在呼叫訊息中指定要被呼叫的物件
- 作為方法呼叫的參數傳入、或作為結果回傳
- 每個遠端物件有唯一一個遠端物件參考
- 可以互相比較,看兩個參考是否指向同一個遠端物件
本節關注的是它的外部表示——也就是要怎麼把這個識別碼攤平成可傳輸的形式。
遠端物件參考是跨整個分散式系統都有效的唯一識別碼,用來在呼叫訊息中指名要呼叫哪個遠端物件,也可當參數、結果與比較依據。
生活妙喻
全球唯一、永不重發的身分證
為什麼要『時空唯一』?
遠端物件參考必須在空間上唯一(所有電腦的所有行程之間都不撞號)也在時間上唯一(就算某物件被刪掉,它的參考也絕不重用)。
為什麼刪掉了還不能重用?因為可能還有人手上握著一張過期的參考。如果你把這個號碼發給新物件,那個拿著舊參考的人一呼叫,就會接到錯誤的物件——後果不堪設想。
正確的設計是:拿過期參考去呼叫,應該回報錯誤,而不是悄悄連到別的物件。
像什麼?
就像身分證字號:全國唯一、不會兩人共用;而且就算某人過世,他的號碼也不會再發給新生兒——否則查到舊紀錄就會張冠李戴。遠端物件參考要的,正是這種『絕不回收再用』的唯一性。
遠端物件參考要時空都唯一、刪除後絕不重用;否則握著舊參考的人會接到錯誤物件。它像永不回收再發的身分證字號。
實用超能力
怎麼造出唯一參考,又能不能搬家?
一種保證唯一的造法
一個經典作法是把幾個欄位串接起來:
| Internet 位址 | port 號 | 建立時間 | 物件編號 | 介面資訊 |
32 bits 32 bits 32 bits 32 bits
- Internet 位址 + port:定位到主機與行程
- port + 建立時間:在該電腦上產生唯一的行程識別
- 物件編號:每在該行程建立一個物件就遞增,保證同行程內不撞號
- 介面資訊:例如介面名稱,讓收到參考的行程知道這物件有哪些方法可呼叫
一個重要取捨:能不能搬家?
- 最簡單的實作:遠端物件只活在建立它的行程裡、行程不在了它也消失。此時參考可直接當位址用——訊息就照參考裡的 IP 與 port 送過去。
- 想讓物件能搬到別台機器:那就不能把參考直接當位址用,否則物件一搬,舊位址就失效。要多一層間接 (indirection),讓物件能在生命週期中於不同伺服器被啟動。
- 完全與位置無關:P2P 覆蓋系統(第 10 章)用一種跟位置完全脫鉤的參考,靠分散式路由演算法把訊息導向資源。
flowchart TD 參考[遠端物件參考] --> 直接[直接當位址用] 參考 --> 間接[多一層間接可搬移] 參考 --> 無關[與位置完全無關靠路由] 直接 --> 簡單[實作簡單但物件不能搬家] 間接 --> 彈性[物件可換伺服器] 無關 --> P2P[適用P2P覆蓋系統]
經典造法是串接 IP+port+建立時間+物件編號+介面資訊保證唯一;若參考直接當位址用則物件不能搬家,要可搬移就得多一層間接,P2P 則用與位置無關的參考。
身分證字號全國唯一、且就算持有人過世也不會再發給新生兒;遠端物件參考也要絕不回收重用,否則拿舊參考的人會張冠李戴。
把幾個各自有區別力的欄位(IP、port、時間、物件編號)串起來,整體就幾乎不可能與別人重複。
名片印死地址最直接,但搬家就作廢;只印『請洽總機』雖多一道手續,搬到哪總機都能幫你轉接。
本節字彙
一次說給一群人聽:群播通訊
群播把單一訊息送給一個群組的所有成員,傳送者不需知道成員是誰。IP multicast 用 Class D 位址表示群組,成員資格是動態的。
IP multicast 怎麼運作
群播把單一訊息送給一個群組的所有成員,傳送者不需知道成員是誰。IP multicast 用 Class D 位址表示群組,成員資格是動態的。
深度探秘
一次說給一整群人聽
點對點不夠用的時候
前面講的 send/receive 都是點對點 (point-to-point):一個傳送者對一個接收者。但有時你要把同一則訊息送給一群行程——例如一個服務由分散在不同電腦上的多個行程組成(為了容錯或提升可用性)。
這時群播 (multicast) 才合適:一個操作就把單一訊息送給群組裡的每個成員,而且通常傳送者對成員是誰、群組多大都無感(成員資格對傳送者透明)。
群播能撐起哪些東西?
| 用途 | 怎麼用群播 |
|---|---|
| 容錯(複製服務) | 把請求群播給所有伺服器副本,掛幾個仍能服務 |
| 服務探索 | 用群播找尋可用的探索服務以註冊或查詢介面 |
| 提升效能(複製資料) | 資料一變更就群播新值給管理各副本的行程 |
| 事件通知傳播 | 例如 Facebook 有人改狀態,所有好友收到通知 |
最簡單的群播協定不保證送達、也不保證順序——這點很重要,下一節會深入。
群播一次把單一訊息送給整個群組的所有成員,傳送者對成員身分與群組大小無感;常用於容錯、服務探索、複製資料與事件通知。
生活妙喻
群播像廣播電台,不是一通通電話
從『一通通打』到『一次播送』
假設你要通知 100 位社團成員開會。
- 點對點作法:你一通一通打電話,打 100 次。累、慢,而且你得先有每個人的電話。
- 群播作法:你在廣播電台的某個頻道講一次,所有轉到那個頻道的人同時聽到。你不需要知道誰在聽、有幾個人聽。
這就是群播的精神:講一次,群組全收到,而且你對聽眾是誰無感。
頻道怎麼表示?
IP multicast 用一個特別的Class D 位址(IPv4 中前 4 個位元是 1110)來代表一個群組——就像電台的頻率。誰想收,就讓自己的 socket 加入 (joinGroup) 那個群組;不想收了就離開 (leaveGroup)。
而且你不必是成員也能對群組廣播——就像不是聽眾也能投稿到電台播出。
flowchart TD 傳送者 -->|送一次到群組位址| 群組頻道 群組頻道 --> 成員A 群組頻道 --> 成員B 群組頻道 --> 成員C
群播像在廣播電台某頻道講一次,所有轉到該頻道的人同時聽到;Class D 位址就是頻道,socket 用 joinGroup/leaveGroup 收聽或退出。
實用超能力
IP multicast 的實作細節與 Java 寫法
幾個關鍵機制
- 只能透過 UDP 使用:在應用層,IP multicast 是靠『送 UDP 資料報到群播位址+一般 port 號』來達成。
- 本地 vs 跨網路:本地群播用區網(如乙太網路)本身的群播能力;跨網路則靠multicast routers 把單一資料報轉送到其他網路的路由器,再在當地群播給成員。
- TTL(time to live):傳送者可指定資料報最多能經過幾個路由器,藉此限制傳播範圍。Java 預設 TTL 為 1,只在本地網路傳播。
- 位址分配:Class D 位址(224.0.0.0 ~ 239.255.255.255)由 IANA 管理,分成多個區塊;有些是永久群組(即使沒成員也存在,如 NTP 用 224.0.1.1),其餘給臨時群組用。
成員資格是動態的
電腦可隨時加入或離開群組,也可同時加入任意多個群組。當群播訊息抵達一台電腦,會被複製給所有加入了該位址且綁在該 port的本地 socket。
Java 的 MulticastSocket
InetAddress group = InetAddress.getByName("228.5.6.7");
MulticastSocket s = new MulticastSocket(6789);
s.joinGroup(group); // 加入群組才收得到
byte[] m = message.getBytes();
DatagramPacket out = new DatagramPacket(m, m.length, group, 6789);
s.send(out); // 送到群組
byte[] buffer = new byte[1000];
DatagramPacket in = new DatagramPacket(buffer, buffer.length);
s.receive(in); // 收群組裡其他人的訊息
s.leaveGroup(group); // 不收了就離開
MulticastSocket 是 DatagramSocket 的子類別,多了加入/離開群組的能力。
IP multicast 在應用層只能透過 UDP,用 Class D 位址表示群組、靠 multicast routers 跨網路、用 TTL 限制範圍;Java 的 MulticastSocket 提供 joinGroup/leaveGroup。
你在某頻道講一次,所有轉到該頻道的人同時聽到,而你不需知道誰在聽、有幾個人聽——成員對傳送者透明。
想收聽就把收音機轉到那個頻率(joinGroup),不想聽就轉走(leaveGroup);頻率就是群組位址。
功率小只能傳到附近(TTL 小、只在本地),功率大才能傳得遠(TTL 大、跨越更多路由器)。
本節字彙
群播的可靠性與順序
IP multicast 和 UDP 一樣會掉訊息(omission failure),且不同成員可能收到不同順序。某些應用需要 reliable multicast 與 totally ordered multicast。
深度探秘
IP multicast 會掉訊息,也可能亂序
跟 UDP 同款的毛病
IP multicast 的失效特性和 UDP 資料報一樣——會發生遺漏失效 (omission failures)。對群播而言,這意味著一則訊息不保證每個成員都收到:可能有些成員收到、有些沒收到。這稱為不可靠群播 (unreliable multicast)。
訊息會在哪裡掉?
- 一個 multicast router 送到另一個 router 的途中,封包遺失 → 該 router 之後的所有接收者都收不到。
- 區網群播時,某個接收者緩衝區滿了就把訊息丟掉。
- 若某個 multicast router 故障,它後面的群組成員都收不到(但本地成員可能照收)。
順序也是問題
IP 封包經過互連網路時不一定照送出順序到達,於是:
- 同一個傳送者送的資料報,不同成員可能收到不同的順序。
- 兩個不同傳送者送的訊息,不一定以相同順序到達所有成員。
IP multicast 與 UDP 一樣會遺漏失效,不保證每個成員都收到(不可靠群播);且不同成員可能收到不同順序。
生活妙喻
嘈雜教室裡的口頭傳話
老師在吵鬧的教室裡喊話
想像老師(傳送者)在一間吵鬧的大教室對全班(群組)口頭宣布事情:
- 坐後排的、戴耳機的同學可能根本沒聽到(遺漏失效——有人收到有人沒)。
- 如果消息是靠同學一排排往後傳,中間有人睡著沒傳(router 故障),他後面整排都漏聽。
- 老師連續喊了三件事,但因為回音與分心,有的同學記得的順序跟別人不一樣(亂序)。
為什麼這對某些事很要命?
- 如果是『今天可以早退喔~』漏聽一個人沒差(服務探索:偶爾漏掉沒關係)。
- 但如果是一群一模一樣的伺服器副本要『按相同順序執行相同操作以保持一致』,那麼:
- 只要有一個副本漏掉一個請求,它就和其他人不一致了。
- 只要順序不同,幾個副本的狀態就會分岔。
這就是為什麼某些應用需要比 IP multicast 更強的保證。
IP multicast 像吵鬧教室的口頭傳話:有人漏聽、傳話鏈斷一節後面全漏、記得的順序還可能不同;對要求一致的複製服務很致命。
實用超能力
什麼時候需要更強的群播保證?
看應用需求決定要多強
回頭看群播的幾種用途,需求差很多:
| 用途 | 對可靠性/順序的需求 |
|---|---|
| 容錯複製服務 | 很高:要嘛全收要嘛全不收,且多半要相同順序 |
| 服務探索(如 Jini) | 低:偶爾漏一個請求無所謂,下次再問 |
| 複製資料 | 視複製方法與『副本要多即時』而定 |
| 事件通知 | 依應用而定 |
兩種更強的保證
為了滿足高需求應用,需要兩個比 IP multicast 更強的性質:
- 可靠群播 (reliable multicast):一則訊息要嘛群組所有成員都收到,要嘛全都沒收到(all-or-nothing,原子性)。
- 全序群播 (totally ordered multicast):送給群組的所有訊息,抵達所有成員的順序都完全相同。這是最嚴格的順序保證。
flowchart TD
需求{應用要求多強} --> 低[偶爾漏可接受]
需求 --> 高[必須一致]
低 --> IP[IP multicast 就夠]
高 --> R[需要可靠群播 全收或全不收]
高 --> T[需要全序群播 所有人順序相同]
IP multicast 並不提供這兩種保證——它們要在更高層另外建構(第 15 章會說明如何實作)。所以工程上要先問清楚:我的應用到底需要多強的群播? 別為不需要的保證付出昂貴代價,也別在需要一致時誤用了不可靠群播。
依應用需求選保證強度:可靠群播是全收或全不收(原子性),全序群播是所有成員順序完全相同;IP multicast 兩者都不提供,需在更高層建構。
老師喊一句,後排或分心的同學可能沒聽到——有人收到有人沒,正是群播不保證每個成員都收到。
要嘛全班都簽收這則通知,要嘛當作沒發過;不會出現只有部分人收到的尷尬局面。
所有成員收到所有訊息的順序完全一致,就像每個人手上的筆記順序都一模一樣,狀態才不會分岔。
本節字彙
在網路上再疊一層:覆蓋網路與 MPI
覆蓋網路是疊在底層網路之上的虛擬網路,有自己的定址、協定與路由,能為特定應用提供新服務。Skype 就是用 super node 架起的覆蓋網路。
覆蓋網路與 Skype
覆蓋網路是疊在底層網路之上的虛擬網路,有自己的定址、協定與路由,能為特定應用提供新服務。Skype 就是用 super node 架起的覆蓋網路。
深度探秘
在現有網路之上『疊出』新網路
為什麼要網路虛擬化?
Internet 協定提供了很棒的通訊積木,但問題來了:各式各樣的應用(P2P 檔案分享、Skype、影音串流、多人遊戲…)需求天差地別。直接去改 Internet 協定來迎合每個應用是不切實際的——對一個有利的改動,可能害到另一個。
解法是網路虛擬化 (network virtualization):在既有網路(如 Internet)之上,建出許多不同的虛擬網路,每個都為某類應用量身打造。
覆蓋網路是什麼?
覆蓋網路 (overlay network) 就是這樣一個虛擬網路:由節點與虛擬連結組成,疊在底層網路(如 IP 網路)之上,提供底層原本沒有的東西:
- 為某類應用量身的服務(例如多媒體內容分送)
- 在特定環境下更有效率(例如 ad hoc 網路的路由)
- 額外功能(例如群播、安全通訊)
關鍵在於:覆蓋網路有自己的定址、協定與路由演算法,全部重新定義來滿足特定應用——它其實是一種『存在於標準架構之外的層 (layer)』,因而擁有重新定義核心元素的自由。
覆蓋網路是疊在底層網路之上的虛擬網路,有自己的定址、協定與路由,可在不改底層的前提下為特定應用提供新服務。
生活妙喻
在公路上畫出專屬路線
同一條公路,多套路線
把底層 Internet 想成一張既有的公路網。要為不同需求各蓋一條新公路(改底層協定)又貴又互相干擾。
覆蓋網路的作法是:不動公路,而是在地圖上疊一層自己的路線規劃。
- 公車路線(一套覆蓋):站牌、班次、走法都自己定,跑在現有馬路上。
- 觀光導覽路線(另一套覆蓋):同樣的馬路,卻有完全不同的停靠點與順序。
- 兩套路線同時存在、互不干擾,都跑在同一張公路網上。
這正呼應了 Saltzer 的端到端論點 (end-to-end argument):與其去改底層公路,不如在上層為特定應用打造專屬路線。
有得必有失
| 好處 | 代價 |
|---|---|
| 不改底層就能定義新服務 | 多一層間接 (indirection),可能有效能損失 |
| 鼓勵實驗與客製化 | 比單純的 TCP/IP 架構更複雜 |
| 多套覆蓋可共存,架構更開放 |
覆蓋網路像在同一張公路網上疊出多套自訂路線:不動底層、各應用各有專屬走法、彼此共存,代價是多一層間接與複雜度。
實用超能力
Skype:真實世界的覆蓋網路
Skype 是覆蓋網路的經典案例
Skype 是個提供 VoIP(網路語音) 的 P2P 應用。它是個虛擬網路:在『人』(目前在線的 Skype 用戶)之間建立連線,不需要 IP 位址或 port 就能發起通話——展示了如何在不改 Internet 核心架構的前提下提供進階功能。
它的架構:兩種節點
flowchart TD 登入伺服器[Skype 登入伺服器] --> 普通主機A 普通主機A[普通主機] --> 超級節點1[超級節點 SN] 超級節點1 --> 超級節點2[超級節點 SN] 超級節點2 --> 普通主機B[普通主機] 超級節點1 --> 超級節點3[超級節點 SN]
- 普通主機 (ordinary host):一般用戶的機器。
- 超級節點 (super node):剛好條件夠好的普通主機——頻寬足、有全域 IP(沒躲在 NAT 後)、上線時間久。它們扛起加強的角色。
運作三步驟
- 登入:透過知名的登入伺服器認證,再連上一個選定的超級節點。每個用戶端快取著一批超級節點的位址(首次約 7 個,後來累積到數百個)。
- 找人:超級節點負責搜尋分散在各超級節點上的全域使用者索引,靠『擴張式搜尋』直到找到目標(平均聯絡約 8 個超級節點,約 3~4 秒)。
- 通話:找到後,用 TCP 傳遞通話訊號(呼叫與結束),用 UDP 或 TCP 串流音訊(偏好 UDP,但為穿越防火牆有時改用 TCP 加中介節點)。
重點啟示:Skype 把『找人+通話』這套複雜功能,完全做在應用層的覆蓋網路裡,底層 Internet 一行都不用改。
Skype 是覆蓋網路經典案例:用普通主機與超級節點組成 P2P 虛擬網路,靠超級節點搜尋分散式使用者索引找人,再用 TCP/UDP 通話,全程不改 Internet 核心。
不動底層馬路,而是在上面規劃公車路線、觀光路線等各自的站點與走法,多套共存、互不干擾。
覆蓋網路為了彈性多繞一層,就像轉乘雖然路線更貼合需求,但通常比直達多花一點時間。
條件好(頻寬足、有公開門牌、常在家)的鄰居自願當聯絡中心,幫大家轉接與找人,分擔系統的協調工作。
本節字彙
案例研究:MPI
MPI 是高效能運算社群制定的訊息傳遞標準,把 send/receive 細分成同步/非同步、阻塞/非阻塞等多種變體,給程式設計師精細的控制權。
深度探秘
為高效能運算量身的訊息傳遞標準
MPI 是什麼?
MPI(Message Passing Interface,訊息傳遞介面) 是由高效能運算 (HPC) 社群在 1994 年制定的標準,目的是統一當時各家各自為政、互不相容的訊息傳遞作法。
它延續了訊息傳遞『簡單、實用、有效率』的精神,再加上可攜性 (portability):提供一個獨立於作業系統與語言特定 socket 介面的標準化介面。應用透過一個 MPI 函式庫來使用,支援多種作業系統與語言(如 C++、Fortran)。
為什麼用在 HPC?
因為這類系統效能至上——訊息傳遞輕量、開銷小,正合需求。MPI 也很有彈性,光是各種變體就定義了超過 115 個操作。
比 4.2 節多了什麼?
架構模型和 4.2.1 節類似,但多了一個關鍵:傳送端與接收端都有MPI 函式庫緩衝區 (MPI library buffer),由函式庫管理,用來暫存傳輸中的資料。這讓 MPI 能提供更細緻的『送』的語意。
MPI 是 HPC 社群制定的訊息傳遞標準,統一各家作法、強調可攜性與效率,定義超過 115 個操作;架構多了由函式庫管理的緩衝區。
生活妙喻
blocking 的真義:『卡到可以安全放手為止』
重新理解 blocking
在 MPI 裡,blocking 不是『卡到對方收到』,而是『卡到可以安全返回為止』——也就是『你的應用緩衝區可以被重複使用了』。但『安全』的定義,依不同變體而不同。
寄快遞的比喻
想像你要寄一份重要文件(應用緩衝區裡的資料),手邊只有這一份原稿,得確定『可以拿這張桌子做下一件事』才算放手:
| MPI 操作 | 什麼時候算『安全放手』 | 快遞比喻 |
|---|---|---|
| MPI_Ssend(同步) | 對方已收到才返回 | 一定要等收件人簽收回條才走 |
| MPI_Bsend(緩衝) | 資料已複製到 MPI 緩衝區就返回(仍在途中) | 交給快遞公司的暫存倉就走,不等送達 |
| MPI_Rsend(就緒) | 程式員保證接收端已準備好,省去握手 | 你已知對方在門口等,直接丟下就走(猜錯就出事) |
| MPI_Send(通用) | 只要求達到上述某種安全等級即可 | 交給可靠管道,細節不管 |
MPI_Rsend 是個『有點危險』的操作:如果『對方已就緒』的假設是錯的,它就會失敗——但賭對了就能省掉握手、跑得更快。
MPI 的 blocking 意指『卡到應用緩衝區可安全重用為止』;Ssend 等收到、Bsend 複製到緩衝就返回、Rsend 賭對方已就緒以省握手、Send 是通用版。
實用超能力
阻塞/非阻塞、同步/非同步,與群體通訊
兩個維度,優雅對稱
MPI 把『同步/非同步』和『阻塞/非阻塞』兩件事乾淨地分開,於是每個 send 變體都有對稱的兩款:
| 阻塞 (Blocking) | 非阻塞 (Non-blocking) | |
|---|---|---|
| 通用 | MPI_Send | MPI_Isend |
| 同步 | MPI_Ssend | MPI_Issend |
| 緩衝 | MPI_Bsend | MPI_Ibsend |
| 就緒 | MPI_Rsend | MPI_Irsend |
- 非阻塞版(名字裡有 I):呼叫立刻返回,給你一個通訊請求 handle,之後用 MPI_Wait(等它完成)或 MPI_Test(檢查完成沒)來追蹤進度。命名規則一致,很好記。
收也有兩款
- MPI_Recv:阻塞收
- MPI_Irecv:非阻塞收
而且送與收的變體可任意搭配,給程式員對訊息傳遞語意豐富的控制權。
不只一對一:群體通訊
除了點對點,MPI 還定義了豐富的多方通訊 (collective communication),例如:
- scatter(散播):一對多——把資料切開分送給多個行程
- gather(收集):多對一——把多個行程的資料匯集回來
flowchart TD 根行程[根行程] -->|scatter 一對多| 行程1 根行程 -->|scatter| 行程2 根行程 -->|scatter| 行程3 行程1 -->|gather 多對一| 匯集[根行程匯集] 行程2 -->|gather| 匯集 行程3 -->|gather| 匯集
一句話總結:MPI 用一致的命名與兩個正交維度,把訊息傳遞的各種語意拆得清清楚楚,再補上 scatter/gather 等群體操作,成為 HPC 的通用語言。
MPI 把同步/非同步與阻塞/非阻塞兩維度分開,每個 send 有對稱的阻塞與非阻塞版(含 I 者立即返回並用 MPI_Wait/MPI_Test 追蹤);還支援 scatter 一對多與 gather 多對一等群體通訊。
blocking 不是等對方收到,而是等到你的工作桌(應用緩衝區)可以拿來做下一件事為止;什麼時候算安全,依變體而定。
Bsend 把資料複製到 MPI 緩衝區就返回(像交給暫存倉),Ssend 則等對方真正收到才返回(像等簽收回條)。
你保證接收端已準備好,就能省掉確認握手跑得更快;但若這個假設錯了,操作就會失敗。