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