理解 C 语言中的阻塞套接字和非阻塞套接字

4 阅读5分钟

理解 C 语言中的阻塞套接字和非阻塞套接字

Blocking Sockets

阻塞套接字(也称为同步套接字)遵循一个简单的范式:I/O 操作会暂停程序的执行,直到操作完成。当您从阻塞套接字读取数据或向其写入数据时,程序会暂停,直到有数据可供读取或写入操作完成。这种同步行为简化了程序的流程,使其对开发人员(尤其是网络编程新手)来说更加直观易懂

阻塞式套接字的主要特点包括:

  • 阻塞行为:I/O 操作会阻塞程序的执行,直到操作完成。
  • 同步操作:操作以同步方式执行,这意味着程序会等待每个操作完成后再继续执行。
  • 简单性:阻塞套接字简单易懂,因此对于网络编程初学者来说是一个不错的选择。

然而,阻塞套接字的简单性是有代价的。设想这样一种场景:使用阻塞套接字同时与多个客户端通信。如果某个客户端的操作耗时过长,可能会阻塞整个程序,进而导致其他客户端的响应延迟。 为了说明这一点,我们来看一个在 TCP 客户端-服务器应用程序中使用阻塞套接字的基本示例:

当你调用 accept()、recv()、send() 等系统调用时,如果条件不满足,函数会一直等待(阻塞)直到操作完成。

也就是说:

  • accept() 会等到有新连接;
  • recv() 会等到有数据;
  • send() 会等到缓冲区可写。

下面是一段最基础的阻塞 socket 服务端代码(用 C 实现):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUF_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    char buffer[BUF_SIZE];
    socklen_t addr_len = sizeof(client_addr);

    // 1. 创建 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    // 3. 监听
    listen(server_fd, 5);
    printf("Server listening on port %d...\n", PORT);

    while (1) {
        // 4. 接受连接(阻塞直到有客户端)
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        printf("New client connected.\n");

        // 5. 处理客户端请求(阻塞)
        while (1) {
            int n = recv(client_fd, buffer, BUF_SIZE, 0);
            if (n <= 0) {
                printf("Client disconnected.\n");
                close(client_fd);
                break;
            }

            buffer[n] = '\0';
            printf("Received: %s\n", buffer);
            send(client_fd, buffer, n, 0); // echo 回去
        }
    }

    close(server_fd);
    return 0;
}

假设现在有多个客户端来连接:

  1. 第一个客户端连上,accept() 返回;

  2. 服务器进入 recv() 等待第一个客户端发数据;

  3. 此时第二个客户端连接过来……但服务器还卡在 recv()!

因为 recv() 是阻塞的:

  • 服务器此时完全“卡死”在第一个客户端;

  • 第二个客户端的连接还在等待 accept();

  • 第三个、第四个也会被阻塞;

  • 整个服务看起来就像“挂了”

# 启动服务器
./server

# 在另一个终端
nc 127.0.0.1 8080
# 输入一些文字,服务端打印后回显

# 再打开第三个终端
nc 127.0.0.1 8080
# 你会发现它迟迟没有反应,直到第一个客户端断开

Non-blocking Sockets

与阻塞式套接字不同,非阻塞式套接字以异步方式运行。当在非阻塞式套接字上发起 I/O 操作时,无论操作是否成功,程序都会立即继续执行。这种异步行为允许程序在等待 I/O 操作完成的同时执行其他任务,从而提高整体效率和响应速度

非阻塞套接字的主要特性包括:

  • 非阻塞行为:I/O 操作会立即返回,即使它们无法立即完成。

  • 异步操作:操作以异步方式执行,使程序能够继续执行,而无需等待每个操作完成。

  • 复杂性增加:非阻塞套接字会给程序逻辑带来额外的复杂性,因为它需要处理操作可能无法立即完成的情况。 非阻塞套接字关闭时

非阻塞套接字虽然响应速度更快、资源利用率更高,但对异步事件的处理却十分谨慎。开发者必须实现相应的机制来有效管理非阻塞套接字的异步特性,例如采用事件循环或使用诸如 select() 或 poll() 之类的多路复用技术。

// nonblock_echo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>

#define MAX_EVENTS 1024
#define BUF_SIZE 1024

// 设置套接字为非阻塞
int set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl(F_GETFL)");
        return -1;
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl(F_SETFL)");
        return -1;
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    int port = atoi(argv[1]);
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        exit(1);
    }

    // 允许端口复用
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);

    if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(listen_fd);
        exit(1);
    }

    if (listen(listen_fd, 128) < 0) {
        perror("listen");
        close(listen_fd);
        exit(1);
    }

    set_nonblock(listen_fd);

    int epfd = epoll_create1(0);
    if (epfd < 0) {
        perror("epoll_create1");
        exit(1);
    }

    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    printf("🚀 Non-blocking Echo Server listening on port %d\n", port);

    while (1) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nfds < 0) {
            if (errno == EINTR)
                continue;
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nfds; ++i) {
            int fd = events[i].data.fd;

            // 有新客户端连接
            if (fd == listen_fd) {
                while (1) {
                    int client_fd = accept(listen_fd, NULL, NULL);
                    if (client_fd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                            break; // 没有更多连接
                        else {
                            perror("accept");
                            break;
                        }
                    }

                    printf("🟢 New client connected: fd=%d\n", client_fd);
                    set_nonblock(client_fd);

                    ev.events = EPOLLIN | EPOLLET; // 边缘触发
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
                }
            } 
            // 客户端可读
            else if (events[i].events & EPOLLIN) {
                char buf[BUF_SIZE];
                while (1) {
                    ssize_t n = recv(fd, buf, sizeof(buf), 0);
                    if (n == 0) {
                        printf("🔴 Client fd=%d disconnected\n", fd);
                        close(fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                        break;
                    } else if (n < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                            break; // 数据读完
                        perror("recv");
                        close(fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                        break;
                    }

                    // 回显
                    send(fd, buf, n, 0);
                }
            }
        }
    }

    close(epfd);
    close(listen_fd);
    return 0;
}