socket、bind、listen 與 accept(伺服器端呼叫)
開一家店來迎接顧客要四個步驟:租下空間、把你的號碼掛在門上讓人找得到你、開門讓人排隊,然後在顧客一個個走進來時逐一服務。TCP 程式的伺服器端正是依這個形狀,以四個呼叫進行:socket() 租下空間、bind() 在門上掛上位址、listen() 開門排隊、accept() 服務下一位來客。
逐步來看:socket() 建立端點並給你一個檔案描述符。bind() 把一個特定的 IP 位址與連接埠附加到它上面,好讓作業系統知道哪些進來的封包是屬於你的——這就是你宣告(譬如)連接埠 8080 的地方。listen() 把通訊端切換成被動模式,並告訴核心開始把連線請求收進一個等待佇列(你給一個 backlog 大小,也就是佇列的深度)。接著 accept() 會阻塞,直到有客戶端連上,然後回傳一個全新的通訊端——專屬於那一個客戶端的另一個檔案描述符。原本的通訊端繼續聆聽;新的那個則用來和這個特定客戶端交談。一個不停呼叫 accept() 的迴圈,就能一個接一個地處理客戶端。
為何重要:這套四呼叫之舞,是幾乎所有寫過的 TCP 伺服器的骨架,從玩具般的回聲伺服器,到正式上線的網頁伺服器都一樣。最常見的單一困惑,就是忘了 accept() 回傳的通訊端,和你聆聽用的那個並不相同——聆聽通訊端是大門,每個被接受的通訊端則是與某位訪客的私下對話。把這個區別弄懂,伺服器程式設計其餘的部分就會各歸其位。
int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, &addr, sizeof addr); listen(s, 128); for (;;) { int c = accept(s, NULL, NULL); /* 透過 c 與客戶端交談,然後 */ close(c); } 聆聽通訊端 s 活在整個伺服器的生命中;每一個 c 則是一位客戶端。
四個呼叫架起一個伺服器;迴圈把每次 accept() 化為一段客戶端對話。
accept() 回傳的每個通訊端,在它的客戶端結束時都必須關閉,否則你會洩漏檔案描述符、最終用罄。相對地,關閉聆聽通訊端則會讓伺服器停止接受新連線。