網路與通訊端

阻塞與非阻塞通訊端及逾時(blocking vs non-blocking sockets and timeouts)

想像你在一個櫃台前等候,被告知「只有食物準備好時你才能自取」。阻塞式的做法是你僵在櫃台前直到食物出現,期間什麼也不做。非阻塞式的做法則是你瞄一眼,若食物還沒好就走開去做別的事,稍後再回來查看。通訊端也是這樣運作:預設情況下通訊端是阻塞的——像 recv() 這樣的呼叫會讓你的程式睡著,直到資料抵達——但你可以把它切換成非阻塞,這時即使什麼都還沒準備好,呼叫也會立刻回傳。

預設情況下通訊端是阻塞的:在安靜的連線上做 recv() 就只是等待,accept() 等待客戶端,connect() 等待交握,而 send() 在送出緩衝區滿時也會等。這很簡單,對一次只服務一件事的程式來說也很好。一個非阻塞通訊端(用 fcntl() 與 O_NONBLOCK 旗標設定)則會立刻回傳:若沒事可做,recv() 回傳 -1,並把 errno 設為 EAGAIN 或 EWOULDBLOCK,意思是「稍後再試,我這裡沒東西給你」。這讓單一執行緒能同時周旋於許多通訊端,而不在任何一個上凍結。一個折衷做法是逾時:你可以叫一個阻塞呼叫去等,但(透過帶 SO_RCVTIMEO 的 setsockopt(),或使用 select()/poll())在譬如 5 秒後放棄,這樣一個失聯的對端就無法把你永遠掛住。

為何重要:阻塞容易推理,但單一個緩慢或無聲的對端就能凍結你整個程式。非阻塞與逾時,正是真實伺服器保持反應靈敏、並避免在一條已消失的連線上永遠掛住的方法。誠實的微妙之處:在非阻塞通訊端上,EAGAIN 不是一個該記錄並中止的錯誤——它是正常的「目前還沒有」的回答,你必須迴圈、或等待就緒後再重試。更深的事件迴圈機制(epoll 及其同類)正是建立在這個概念上,但留待後面再談。

在非阻塞通訊端上:ssize_t n = recv(fd, buf, len, 0); if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { /* 目前還沒就緒——不是錯誤,稍後再試 */ } 若改為替阻塞通訊端加上 5 秒接收逾時:setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);

非阻塞通訊端上的 EAGAIN/EWOULDBLOCK 代表「目前還沒有」、不是失敗——重試,別中止。

若沒有逾時,在一個無聲消失的對端上做阻塞 recv(),幾乎可能永遠等下去——核心並不總會察覺另一端已不在。當掛住的對端會造成問題時,務必為你的等待設下界限。

又稱
socket blocking modesocket timeout阻塞模式逾時