阻塞式IO與非阻塞式IO

阻塞式IO與非阻塞式IO

套接字的默認狀態(tài)是阻塞的。可能阻塞套接字的調(diào)用可以分為下面4類:

  • 輸入操作 包括read, readv, recv, recvfromrecvmsg 共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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容