socket 的阻塞模式和非阻塞模式

发布时间 2023-11-23 11:41:50作者: 野原丶广志

1.socket 的阻塞模式和非阻塞模式

在阻塞和非阻塞模式下,常讨论的具有不同行为表现的 socket 函数一般有 connect、accept、send 和 recv。在 Linux 上对 socket 进行操作时也包括 write 函数和 read 函数。

在 Linux 上, 可以使用 fcntl 函数或 ioctl 函数给创建的 socket 增加 O NONBLOCK 标志来将 socket 设置为非阻塞模式。代码如下:

int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);

ioctl 函数与 fcntl 函数的使用方式基本一致。

Linux 上的 socket 函数也可以直接在创建时将 socket 设置为非阻塞模式,socket 函数签名如下:

int socket(int domain, int type, int protocol);

//给 type 参数增加一个 SOCK_NONBLOCK 标志即可,如:
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

同时,在 Linux 上利用 accept 函数返回的代表与客户端通信的 socket 也提供了一个扩展 accept4,直接将 accept 函数返回的 socket 设置为非阻塞的:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
//只要将 accept4 函数的最后一个参数 flags 设置为 SOCK_NONBLOCK 即可

2.send 和 recv 函数在阻塞和非阻塞模式下的表现

  • 当 socket 是阻塞模式时,继续调用 send/recv 函数,程序会阻塞在 send/recv 调用处;
  • 当 socket 时非阻塞模式时,继续调用 send/recv 函数,send/recv 函数不会阻塞程序执行流,而是立即出错并返回,我们会得到一个相关的错误码,在 Linux 上该错误码为 EWOULDBLOCK 或 EAGAIN。
返回值 返回值的含义
大于0 成功发送(send)或接收(recv)n 字节
0 对端关闭连接
小于0(-1) 出错、被信号中断、对端 TCP 窗口太小导致数据发送不出去或者当前网卡缓冲区已经无数据可接收
返回值和错误码 send函数 recv函数 操作系统
返回-1,错误码时EWOUDBLOCK或EAGAN TCP窗口太小,数据暂时发不出去 在当前内核缓冲区无可读数据 Linux
返回-1,错误码时EINTR 被信号中断,需要重试 被信号中断,需要重试 Linux
返回-1,错误码不是以上3种 出错 出错 Linux

非阻塞模式一般用于支持高并发多 QPS 的场景(如服务器程序),但是这种模式让程序的执行流和控制逻辑变得复杂。

使 send 函数的返回值为 0 的情况:

  • 对端关闭连接时,我们正好尝试调用 send 函数发送数据
  • 本端尝试调用 send 函数发送 0 字节数据。

recv 函数只有在对端关闭连接时才会返回 0,对端发送 0 字节数据,本段的 recv 函数时不会收到 0 字节数据的,即对端操作系统协议栈不会把 0 字节数据发送过来。

3.connect 函数在阻塞和非阻塞模式下的行为

在实际项目中,一般倾向于使用异步 connect 技术(非阻塞 connect),一般有如下步骤:

  • 1.创建 socket,将 socket 设置为非阻塞模式。
  • 2.调用 connect 函数,此时无论 connect 函数是否连接成功,都会返回;如果返回-1,则并不一定表示连接出错,如果此时错误码时 EINPROGRESS,则表示正在尝试连接。
  • 3.调用 select 函数,在指定的时间内判断该 socket 是否可写,如果可写,则说明连接成功,反之认为连接失败。

按上述流程代码编写如下:

点击查看代码
/**
 * 异步的 connect 写法,nonblocking_connect.cpp
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"

int main(int argc, char *argv[])
{
    //1.创建一个socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if(clientfd == -1){
        std::cout << "create client socket error." << std::endl;
        return -1;
    }

    //将clientfd设置为非阻塞模式
    int oldSocketFlag = fcntl(clientfd,F_GETFL,0);
    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    if(fcntl(clientfd, F_SETFL, newSocketFlag) == -1){
        close(clientfd);
        std::cout << "set socket to nonblock error." << std::endl;
        return -1;
    }

    //2.连接服务器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    for(;;)
    {
        int ret = connect(clientfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
        if(ret == 0)
        {
            std::cout << "connect to server seccessfully." << std::endl;
            close(clientfd);
            return 0;
        }
        else if(ret == -1)
        {
            if(errno == EINTR)
            {
                //connect 动作被信号中断,重试connect
                std::cout << "connecting interrupted by signal, try again." << std::endl;
                continue;
            }
            else if(errno == EINPROGRESS)
            {
                //正在尝试连接
                break;
            }
            else
            {
                //真的出错了
                close(clientfd);
                return -1;
            }
        }
    }

    fd_set writeset;
    FD_ZERO(&writeset);
    FD_SET(clientfd, &writeset);
    struct timeval tv;
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    //3.调用select函数判断socket是否可写
    if(select(clientfd+1,NULL,&writeset,NULL,&tv) != 1)
    {
        std::cout << "[select] connect to server error." << std::endl;
    }

    int err;
    socklen_t len = static_cast<socklen_t>(sizeof err);
    //4.调用getsockopt检测此时socket是否出错
    if(::getsockopt(clientfd, SOL_SOCKET,SO_ERROR, &err, &len) < 0)
    {
        close(clientfd);
        return -1;
    }

    if(err == 0)
    {
        std::cout << "connect to server successfully." << std::endl;
    }
    else{
        std::cout << "connect to server error." << std::endl;
    }


    close(clientfd);

    return 0;
}

在 Linux 上,一个 socket 在没有建立连接之前,用 select 函数检测其是否可写,我们也会得到可写的结果,所以,在 connect 之后,不仅要用 select 检测是否可写,还要调用 getsockopt 检测此时 socket 是否出错,通过错误码来检测和确定是否连接上,错误码为 0 时表示连接上,反之表示未连接上。可以使用 poll 函数代替 select 函数判断 socket 是否可写;