騰訊后端 C++一面:recv 返回值,什么錯誤是可接受的?
在 C/C++網絡編程中,尤其是在處理 TCP 套接字時,recv函數扮演著基石般的角色。它是從連接對端讀取傳入數據的主要機制。
1. recv 函數原型
我們先從標準的 POSIX 函數原型開始:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd: 接收數據的套接字文件描述符。
- buf: 指向用于存儲接收數據的緩沖區的指針。
- len: buf緩沖區的最大長度,即本次調用最多接收的字節數。
- flags: 控制接收操作的標志位(例如 MSG_PEEK, MSG_WAITALL)。通常設為 0 表示標準行為。
返回值 ssize_t 類型對于理解調用的結果至關重要。
2. 解讀 recv 返回值
recv 函數的返回值主要有以下三種情況:
(1) 返回值 > 0: 成功接收數據
- 含義: 調用成功,并從對端接收到了數據。
- 值: 返回值表示實際接收到并放入buf中的字節數。
- 注意: 這個值可能小于你請求的 len,即使發送方發送了更多的數據。這是正常現象,原因包括網絡緩沖區大小、TCP 分段、以及調用時套接字接收緩沖區中實際可用的數據量等。
- 處理: 處理接收到的 返回值 這么多字節的數據。如果你的應用層協議期望更多的數據,你可能需要在一個循環中再次調用 recv,直到接收到完整的消息或發生錯誤/連接關閉。
(2) 返回值 == 0: 對端已正常關閉連接
- 含義: 遠端對等方(peer)已經執行了有序關閉序列(發送了 FIN 包),表示它不會再發送任何數據了。
- 值: 0。
- 注意: 這不是一個錯誤。這是一個信號,表明連接的讀取方向已經由對端關閉。你無法再從此連接接收到任何數據。
- 處理: 識別到連接正在關閉。通常應停止嘗試接收數據,可能需要關閉你自己的寫端(如果還沒關閉,使用 shutdown(sockfd, SHUT_WR)),并最終調用 close(sockfd) 來釋放套接字資源。
(33) 返回值 == -1: 發生錯誤
- 含義: recv 調用失敗。
- 值: -1。
- 處理: 這是錯誤處理的關鍵所在,不同的值處理方式是不一樣的。
錯誤處理:哪些 errno 值是“可接受”的?
當 recv 返回 -1 時,并非所有的 errno 值都意味著連接已死或必須立即放棄。有些錯誤指示的是臨時狀態或需要特定處理邏輯的情況,而不是直接終止連接。這些就是在面試題里面說的“可接受”的錯誤。
(1) “可接受” / 非致命錯誤
① EAGAIN 或 EWOULDBLOCK: (這兩個宏通常具有相同的值)
- 場景: 主要發生在套接字被設置為非阻塞模式 (O_NONBLOCK) 時。
- 含義: 當前套接字的接收緩沖區中沒有數據可讀,并且在不阻塞的情況下無法立即完成讀取操作。這不是一個真正的錯誤,而是對套接字狀態的一種描述。
- 處理: 不要關閉連接。這是非阻塞 I/O 中的預期行為。標準的處理方式是使用 I/O 多路復用機制(如 epoll, select, poll)。這些機制會通知你套接字何時變為可讀狀態,屆時你再安全地重試 recv 調用。在一個緊密循環中反復調用 recv 而不等待(忙等待)是非常低效的。
// 概念性的非阻塞循環(結合epoll/select)
while (true) {
// 使用epoll_wait, select等等待sockfd變為可讀
// ... 等待邏輯 ...
ssize_tbytes_received= recv(sockfd, buffer, sizeof(buffer), 0);
if (bytes_received > 0) {
// 處理數據...
} elseif (bytes_received == 0) {
// 對端關閉連接
handle_close(sockfd);
break;
} else { // bytes_received == -1
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 當前無數據可用,返回繼續等待(epoll/select)
continue;
} elseif (errno == EINTR) {
// 被信號中斷,直接重試
continue;
} else {
// 發生其他錯誤
perror("recv failed"); // 打印錯誤信息
handle_error_close(sockfd); // 處理錯誤并關閉連接
break;
}
}
}
② EINTR:
- 含義:recv系統調用被一個應用程序捕獲到的信號(signal)所中斷,并且中斷發生在任何數據傳輸完成之前。
- 處理:這通常被認為是一個暫時的中斷。操作并未完成,但這并不一定意味著套接字本身有問題。標準的做法是簡單地在循環中立即重試 recv調用。
(2) 致命 / 不可恢復錯誤
這些錯誤通常表明連接本身、套接字狀態或程序邏輯存在問題。在這些錯誤發生后繼續在該套接字上操作通常是無意義或不可能的。
- ECONNRESET: 連接被對端重置。對方發送了 RST(重置)包,很可能是因為對方異常終止或網絡問題。連接已失效。
- ENOTCONN: 套接字未連接(例如,在 TCP 套接字上調用 connect 或 accept 之前,或連接已斷開后嘗試 recv)。
- ETIMEDOUT: 連接超時。可能在連接建立階段發生,或在數據傳輸過程中由于網絡狀況極差或設置了 SO_RCVTIMEO 并發生較長超時而發生。通常意味著連接不可用。
- ECONNREFUSED: 遠程主機主動拒絕連接(更常見于 connect 調用,但在特定的 UDP recvfrom 場景下也可能遇到)。
- EBADF: 無效的文件描述符 (sockfd 沒有指向一個打開的套接字)。這是程序邏輯錯誤。
- EFAULT: 傳入的緩沖區指針 buf 指向了進程地址空間之外的無效內存。這是程序邏輯錯誤。
- EINVAL: 提供了無效的參數(例如,無效的 flags)。程序邏輯錯誤。
- ENOTSOCK: 文件描述符 sockfd 指的不是一個套接字。程序邏輯錯誤。
對于致命錯誤的處理: 記錄具體的 errno 值和錯誤信息(使用 perror 或 strerror),清理與該連接相關的資源,并調用 close(sockfd) 關閉套接字。
回答
- 區分出 recv 的三種返回值 (>0, 0, -1) 及其含義。
- 知道 -1 需要檢查 errno。
- 明確指出 EAGAIN/EWOULDBLOCK 和 EINTR 是可接受的、需要特殊處理(等待/重試)的錯誤,尤其是在非阻塞 I/O 場景下 EAGAIN/EWOULDBLOCK 是正常情況。
對于“什么錯誤是可接受的?”這個問題,最核心的答案是 EAGAIN (或 EWOULDBLOCK) 和 EINTR。