[linux] socket 非阻塞模式使用注意事项

在使用 socket 的一些 api 的时候,默认情况下都是阻塞模式。比如使用 tcp socket 时,客户端调用 connect() 创建连接,connect() 返回的时候要么是创建连接成功了,要么是出现了错误,反正 connect() 返回的时候结果是确定的;tcp 服务端使用 accept() 接收连接的时候,accept() 返回的时候一般就是接收到了新的连接;使用 recv() 接收数据的时候,直到接收到数据之后,才会返回;使用 send() 发送数据的时候,所有数据都写到 tcp 缓冲区才会返回。以上就是这些 api 在阻塞模式下的工作原理。

在实际使用 socket 的时候,我们往往使用多路复用技术(select, poll, epoll)来监听 socket 中的事件,当监听到有事件到来时才会调用 accept() 或者 recv()。在这种情况下,因为有现成的事件,所以即使在阻塞模式,accept() 和 recv() 也能立即返回。对于 connect() 和 send(),我们一般是直接使用,而不是多路复用监听到有事件之后再调用。

在开发中,大部分情况下使用阻塞模式就已经足够了,但是在一些场景下,我们也需要使用 socket 的非阻塞模式。本文就记录一下上述 api 在非阻塞模式下使用的注意事项。

fd 默认是阻塞的,如果要设置为非阻塞模式,可以通过如下代码来完成。通过 fcntl 先获取 fd 的选项,然后再通过 fcntl 将非阻塞选项设置进去。

static int32_t set_fd_non_blocking(int32_t fd) {
    int opts = fcntl(fd, F_GETFL);
    if (opts < 0) {
        printf("fd[%d] GETFL failed[%s]", fd, strerror(errno));
        return -1;
    }
    if (fcntl(fd, F_SETFL, opts | O_NONBLOCK) < 0) {
        printf("fd[%d] SETFL failed[%s]", fd, strerror(errno));
        return -1;
    }
    return 0;
}

 对于 recv() 和 send() 来说,除了使用 fcntl 设置为非阻塞模式之外,还可以再 recv 和 send 的最后一个入参中设置 MSG_DONTWAIT 来将这次调用设置为非阻塞模式,两者效果是一样的。

1 connect

非阻塞模式下调用 connect,有以下 3 种情况需要分类处理:

(1)如果返回值是 0,说明连接已经建立成功

(2)返回 -1,并且错误码不是 EINPROGRESS,说明出现了错误,连接失败

(3)返回 -1,并且错误码是 EINPROGRESS,说明连接已经发起,正在处理中。这种情况下,就需要后续做监控来判断连接什么时候建立完成。

怎么监听正在处理的 connect 是不是已经完成,需要做如下两件事:

(1)使用多路复用技术(select, poll 或者 epoll) 监听套接字,监听写事件

(2)如果套接字有写事件,就通过 getsockopt 获取 socket 的 ERROR 状态,如果没有 ERROR,说明建立连接成功;如果有 ERROR,说明建立连接失败。使用多路复用技术监听的时候,也可以设置超时时间,超时监听不到,也说明建立连接失败。

获取 socket ERROR 状态的代码如下。

int error = 0;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) < 0) {
  return;
}
if (error != 0) {
  return;
}

2 accept

在实际使用中,一般会将服务端套接字加入到 epoll 中,使用 epoll 来监听,这样当服务端套接字有事件时,说明有新的连接到来,即使套接字处于阻塞状态,accept 也是不需要阻塞的。

如果监听套接字工作在非阻塞模式下,那么 accept 是否成功,和返回值和错误码有关系。

(1)返回值 > 0,说明接收到了新的连接,返回值就是新连接的 fd

(2)返回值是 -1,并且错误码不是 EAGAIN || EWOULDBLOCK || EINTR,说明出现了错误,获取失败

(3)返回值是 -1,并且错误码是 EAGAIN || EWOULDBLOCK || EINTR,说明需要重试

如下代码,创建一个 listening fd,然后将 fd 设置为非阻塞模式,这样在 accept 的时候,就会返回 EAGIN 错误。

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_IP     ("0.0.0.0")
#define SERVER_PORT   (12345)
#define MAX_LISTENQ   (32)

static int32_t set_fd_non_blocking(int32_t fd) {
    int opts = fcntl(fd, F_GETFL);
    if (opts < 0) {
        printf("fd[%d] GETFL failed[%s]", fd, strerror(errno));
        return -1;
    }
    if (fcntl(fd, F_SETFL, opts | O_NONBLOCK) < 0) {
        printf("fd[%d] SETFL failed[%s]", fd, strerror(errno));
        return -1;
    }
    return 0;
}

int main() {
    int ret = -1;
    int accept_fd = -1;
    int listen_fd = -1;

    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }
    set_fd_non_blocking(listen_fd);

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /**< 0.0.0.0 all local ip */
    server_addr.sin_port = htons(SERVER_PORT);

    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind error: ");
        return -1;
    }

    if (listen(listen_fd, MAX_LISTENQ) < 0) {
        printf("listen error.\n");
        return -1;
    }

    while (1) {
      accept_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client);
      if(accept_fd < 0) {
        perror("accept error");
      }
      sleep(1);
    }

    return 0;
}

EAGIN 对应的字符串是 Resource temporarily unavailable。

3 recv

在阻塞模式下,recv 也不会等所有数据都到来之后才会返回,recv 是至少有收到一个字节便会返回。

仍然是根据返回值和错误码来判断:

(1)返回值 > 0,返回值表示读取的数据的字节数

(2)返回值为 -1,并且错误码是 EAGAIN || EWOULDBLOCK || EINTR 的话,说明当前没数据,需要重试;如果错误码不是 EAGAIN || EWOULDBLOCK || EINTR 的话,说明发生了错误

4 send

在阻塞模式下,当所有数据都发送完毕之后,send 才会返回。在非阻塞模式下,send 的情况要看返回值和错误码。

(1)返回值 > 0,表示发送的字节数

(2)返回值为 -1,并且错误码是 EAGAIN || EWOULDBLOCK || EINTR 的话,那么需要重试;如果错误码不属于 EAGAIN || EWOULDBLOCK || EINTR,那么说明发生了错误

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值