在使用 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,那么说明发生了错误