阻塞式IO與非阻塞式IO
套接字的默認狀態(tài)是阻塞的。可能阻塞套接字的調(diào)用可以分為下面4類:
-
輸入操作 包括
read,readv,recv,recvfrom和recvmsg共5個函數(shù)。
如果某個進程對一個阻塞的TCP套接字調(diào)用這些函數(shù),那么該套接字的接收緩沖區(qū)中沒有數(shù)據(jù)可讀,該進程將被投入睡眠,直到有一些數(shù)據(jù)達到。因為TCP是字節(jié)流協(xié)議,該進程的喚醒就是只要有一些數(shù)據(jù)到達,這些數(shù)據(jù)既可能是單個字節(jié),也可以是一個完整的TCP分節(jié)中的數(shù)據(jù)。而對于UDP而言,因為UDP是數(shù)據(jù)報協(xié)議,直達有UDP數(shù)據(jù)報到達,進程才會被喚醒。
所以對于非阻塞的套接字,如果輸入操作不能被滿足(對于TCP套接字即至少有一個字節(jié)的數(shù)據(jù),UDP套接字即有一個完整的數(shù)據(jù)報可讀),相應調(diào)用將立即返回一個
EWOULDBLOCK錯誤。 -
輸出操作 包括
write,writev,send,sendto,sendmsg共5個函數(shù)。- 阻塞的TCP套接字,內(nèi)核將從應用進程的緩沖區(qū)到該套接字的發(fā)送緩沖區(qū)復制數(shù)據(jù),如果其發(fā)送緩沖區(qū)中沒有空間,進程將被投入睡眠,直到有空間為止。
- 非阻塞的TCP套接字,如果其發(fā)送緩沖區(qū)中沒有空間,輸出函數(shù)將立即返回一個
EWOULDBLOCK錯誤。如果其發(fā)送緩沖區(qū)有一些空間,返回值將是內(nèi)核能夠復制到該緩沖區(qū)的字節(jié)數(shù)。
而對于UDP套接字,因為UDP套接字不存在真正的發(fā)送緩沖區(qū),內(nèi)核只是復制應用進程數(shù)據(jù)并把他們向下沿著協(xié)議棧傳遞,逐漸冠以UDP首部和IP首部。因此對于UDP套接字,輸出函數(shù)不會因為TCP一樣的原因而阻塞。但是可能會因為其他原因而阻塞。
-
接受外來連接
accept函數(shù)
對于 accept 函數(shù),如果對一個阻塞的套接字調(diào)用 accept 函數(shù),并且尚無新的連接到達,調(diào)用進程將被投入睡眠。while(true) { int c_fd = accept(listen_fd, NULL, NULL); if (c_fd == -1) { if (errno != EWOULDBLOCK) // 非阻塞accept的EWOULDBLOCK錯誤不處理 { fprintf(stderr, "%s: %d: ", __FILE__, __LINE__); perror("accept"); break; } } } 發(fā)起外出連接
connect函數(shù)
對于阻塞TCP套接字,connect 函數(shù)一直要等到三路握手完成才返回,這意味著每個connect 總會阻塞其調(diào)用進程至少一個到服務器的RTT時間。
非阻塞的TCP套接字,當連接不能立即建立(有些連接可以立即建立),那么連接的建立能照樣發(fā)起(發(fā)送三路握手的第一個分組),不過會返回一個EINPROGRESS錯誤。
如何將套接字設置為非阻塞
在 Linux 平臺上,我們可以使用 fcntl() 函數(shù)或 ioctl() 函數(shù)給創(chuàng)建的 socket 增加 O_NONBLOCK 標志來將 socket 設置成非阻塞模式。
int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);
在 Linux 下的 socket 函數(shù)也可以直接在創(chuàng)建時將 socket 設置為非阻塞模式
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
accept() 函數(shù)返回的代表與客戶端通信的 socket 也提供了一個擴展函數(shù) accept4(),直接將 accept 函數(shù)返回的 socket 設置成非阻塞的。
int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK);
非阻塞下的 send / write 返回值意義
| 返回值 n | 返回值含義 |
|---|---|
| 大于 0 | 成功發(fā)送 n 個字節(jié) |
| 0 | 對端關閉連接 |
| 小于 0( -1) | 出錯或者被信號中斷或者對端 TCP 窗口太小數(shù)據(jù)發(fā)不出去 |
- send 0 字節(jié)
send 函數(shù)發(fā)送 0 字節(jié)數(shù)據(jù),協(xié)議棧并不會做什么動作。
int sent_bytes = 0;
while (true)
{
int ret = send(clientfd, buf + sent_bytes , buf_length - sent_bytes , 0);
if (ret == -1)
{
// 非阻塞模式下send函數(shù)由于TCP窗口太小發(fā)不出去數(shù)據(jù),錯誤碼是EWOULDBLOCK
if (errno == EWOULDBLOCK)
{
// 嚴謹?shù)淖龇?,這里如果發(fā)不出去,應該緩存尚未發(fā)出去的數(shù)據(jù)
std::cout << "send data error as TCP Window size is too small." << std::endl;
break;
}
else if (errno == EINTR)
{
// 如果被信號中斷,我們繼續(xù)重試
std::cout << "sending data interrupted by signal." << std::endl;
continue;
}
else
{
std::cout << "send data error." << std::endl;
break;
}
}
else if (ret == 0)
{
// 對端關閉了連接,我們也關閉
std::cout << "send data error." << std::endl;
close(clientfd);
break;
}
sent_bytes++;
std::cout << "send data successfully, count = " << count << std::endl;
// 稍稍降低 CPU 的使用率
usleep(1);
}
非阻塞下 read / recv 返回值意義
| 返回值 n | 返回值含義 |
|---|---|
| 大于 0 | 成功接收 n 個字節(jié) |
| 0 | 對端關閉連接 |
| 小于 0( -1) | 出錯或者被信號中斷或者或者當前網(wǎng)卡緩沖區(qū)已無數(shù)據(jù)可讀取 |
while (true)
{
char recvbuf[32] = {0};
int ret = recv(clientfd, recvbuf, 32, 0);
if (ret > 0)
{
// 收到了數(shù)據(jù)
std::cout << "recv successfully." << std::endl;
}
else if (ret == 0)
{
// 對端關閉了連接
std::cout << "peer close the socket." << std::endl;
break;
}
else if (ret == -1)
{
if (errno == EWOULDBLOCK)
{
std::cout << "There is no data available now." << std::endl;
continue;
}
else if (errno == EINTR)
{
// 如果被信號中斷了,則繼續(xù)重試recv函數(shù)
std::cout << "recv data interrupted by signal." << std::endl;
continue;
}
else
{
// 真的出錯了
break;
}
}
}
參考資料
《UNIX 網(wǎng)絡編程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff