IO多路复用之poll函数-非阻塞版本

程序员成长:技术、职场与思维模式实战指南 10w+人浏览 1k人参与

1. poll的基础

2. 阻塞/非阻塞分类:poll()阻塞,poll()非阻塞,socket阻塞,socket非阻塞

1. poll()是否阻塞通过它的timeout参数控制,如下表

timeout 值行为模式等待特性返回时机常见用途
-1阻塞模式永久阻塞等待,直到有事件发生事件发生才返回长连接服务器(推荐)
0非阻塞模式立即返回,不等待立刻返回(轮询)轮询检测、调试场景
> 0定时阻塞模式最多等待指定毫秒数事件触发或超时返回有超时机制的等待,如心跳检测

2. socket阻塞与非阻塞区别

对比项阻塞模式(Blocking Socket非阻塞模式(Non-Blocking Socket
行为特性系统调用(read / write / accept)会等待完成才返回系统调用立即返回,如果操作无法立即完成返回 EAGAINEWOULDBLOCK
CPU 占用线程被挂起,不占用 CPU调用立即返回,程序继续执行,CPU 利用率更高
编程复杂度简单直观,易实现逻辑较复杂,需要轮询或事件驱动机制
并发能力需要多线程或多进程单线程即可处理大量连接(配合 poll/epoll
适合场景小规模、阻塞式服务或简单客户端高并发服务器、实时系统、事件驱动架构
典型返回行为read() 等待数据 → 返回读取字节数read() 若无数据 → 立即返回 -1 并设置 errno = EAGAIN
优点编程简单,调试方便不阻塞线程,高性能
缺点线程容易被卡死代码逻辑复杂,需处理 EAGAIN/EWOULDBLOCK

3. poll()与socket阻塞与非阻塞对比表

项目阻塞版非阻塞版
poll() 调用调用会等待事件(阻塞线程)调用立即返回
socket 读写read / write 可能阻塞read / write 立即返回 EAGAIN
常见用法简单单任务程序高并发网络服务
优点实现简单效率高、不卡线程
缺点线程可能被卡死代码逻辑更复杂(要处理 EAGAIN)

4. poll()与socket的组合关系特点表

poll() 阻塞性socket 阻塞性特点
阻塞阻塞简单服务器,单连接处理(容易卡)
阻塞非阻塞常见组合;poll 阻塞等待事件,socket 非阻塞读写
非阻塞阻塞意义不大;poll 立即返回但 socket 仍可能卡住
非阻塞非阻塞完全轮询型事件循环(高 CPU 占用)
  • 其中,最常见、最实用的是
poll 阻塞(timeout=-1+ socket 非阻塞  # 这种模式效率高,不会卡线程,也不会白忙 CPU。

5. socket设置非阻塞方式代码


// 设置 socket 为非阻塞,调用该函数并将服务器fd传入这个函数即可
void set_nonblocking(int fd) 
{
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

6. poll()阻塞+socket非阻塞服务器

1. 服务器代码

  • 由于socket是非阻塞,所以读数据时需要特别注意,有可能多次读到数据,所以需要自己服务器能组装数据,下方使用read(fd, buf, 1)代替read(fd, buf, sizeof(buf)来控制一次只读取一个数据
  • 由于poll()是水平触发(LT),有数据就会一直返回信号,所以即使客户端只发送一次数据,服务器一次只接收1byte,也会将其接收完毕(但是需要组装数据,所以一般情况下,服务器与客户端通信都是采取先发送即将发送的数据大小,然后再发送指定大小的数据,这里就不演示了,后续的文章epoll()会详细介绍)。
  • 编译:
gcc  poll_server_nonblock.c -o poll_server
  • 运行
./poll_server 8888
// poll_server_nonblock.c

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAX_CLIENTS 1024
#define BUF_SIZE 4096

// 设置 socket 为非阻塞
void set_nonblocking(int fd)
{
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "用法: %s <端口号>\n", argv[0]);
        return 1;
    }

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

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in servaddr = {};
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(port);

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("bind");
        exit(1);
    }
    if (listen(listenfd, SOMAXCONN) < 0)
    {
        perror("listen");
        exit(1);
    }

    set_nonblocking(listenfd);
    printf("服务器启动:poll 阻塞 + 非阻塞 socket,端口 %d\n", port);

    struct pollfd fds[MAX_CLIENTS];
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;
    for (int i = 1; i < MAX_CLIENTS; i++)
        fds[i].fd = -1;

    char buf[BUF_SIZE];

    while (1)
    {
        // poll 阻塞等待事件(timeout = -1)
        int nready = poll(fds, MAX_CLIENTS, -1);
        if (nready < 0)
        {
            if (errno == EINTR)
                continue;
            perror("poll");
            break;
        }

        // 监听 socket 可读 → 新连接
        if (fds[0].revents & POLLIN)
        {
            struct sockaddr_in cli;
            socklen_t len = sizeof(cli);
            int connfd = accept(listenfd, (struct sockaddr *)&cli, &len);
            if (connfd >= 0)
            {
                set_nonblocking(connfd);
                char ip[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
                printf("新连接:%s:%d fd=%d\n", ip, ntohs(cli.sin_port), connfd);

                int i;
                for (i = 1; i < MAX_CLIENTS; i++)
                {
                    if (fds[i].fd == -1)
                    {
                        fds[i].fd = connfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }
                if (i == MAX_CLIENTS)
                {
                    printf("连接过多,拒绝 fd=%d\n", connfd);
                    close(connfd);
                }
            }
            if (--nready <= 0)
                continue;
        }

        // 遍历所有客户端
        for (int i = 1; i < MAX_CLIENTS; i++)
        {
            int fd = fds[i].fd;
            if (fd < 0)
                continue;

            // 检查异常
            if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL))
            {
                close(fd);
                fds[i].fd = -1;
                continue;
            }

            // 可读事件
            if (fds[i].revents & POLLIN)
            {
                ssize_t n;
                // while ((n = read(fd, buf, sizeof(buf))) > 0)
                while ((n = read(fd, buf, 1)) > 0)
                {
                    printf("%s\n", buf);
                    /*
                    // 回显数据
                    ssize_t sent = 0;
                    while (sent < n)
                    {
                        ssize_t w = write(fd, buf + sent, n - sent);
                        if (w > 0)
                            sent += w;
                        else if (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
                            break;
                        else
                        {
                            perror("write");
                            close(fd);
                            fds[i].fd = -1;
                            break;
                        }
                    }
                    */
                }
                printf("=========1\n");

                if (n == 0)
                {
                    printf("客户端 fd=%d 断开连接\n", fd);
                    close(fd);
                    fds[i].fd = -1;
                }
                else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
                {
                    printf("errno != EAGAIN && errno != EWOULDBLOCK\n");
                    perror("read");
                    close(fd);
                    fds[i].fd = -1;
                }
                printf("=========2\n");
            }
        }
    }

    close(listenfd);
    return 0;
}

2. 客户端工具(Netcat)

  • Netcat(简称nc)是一款功能强大的网络工具,被誉为网络安全界的“瑞士军刀”。它可以通过TCP或UDP协议进行网络连接,读写数据,广泛应用于网络调试、探测和数据传输等场景。
  • 在运行上方服务器后,使用nc 127.0.0.1 8888连接服务器,然后输入数据按回车发送就能看见服务器输出。

3. Netcat基础用法(当tcp/udp协议的服务器/客户端)

  • 连接tcp服务器:nc ip 端口
    nc  127.0.0.1 8888
    
  • 连接udp服务器(参数-u代表使用udp,默认是tcp):nc -u ip 端口
    nc  -u 127.0.0.1 8888
    
  • 使用nc当tcp服务器:nc -l ip 端口
    nc -l 127.0.0.1 8888
    
  • 使用nc当udp服务器:nc -ul ip 端口
    nc -ul 127.0.0.1 8888
    

4. socket非阻塞注意事项

类别注意事项说明
1️⃣ 设置非阻塞使用 fcntl(fd, F_SETFL, O_NONBLOCK)确保 socket 已正确设置为非阻塞模式,否则 read 仍可能阻塞线程。
2️⃣ read() 行为变化无数据可读时立即返回 -1 并设置 errno = EAGAINEWOULDBLOCK这不是错误,只代表“现在暂时没有数据”。应等待下一次 poll/epoll 通知后再读。
3️⃣ 必须循环读取在收到“可读事件”后,应循环 read() 直到返回 EAGAIN因为一次可读事件可能对应多次可读数据,读不完会导致数据滞留内核缓冲区。
4️⃣ 处理断开连接read() 返回 0 表示对端关闭连接应立即关闭 fd 并清理资源。
5️⃣ 错误处理EAGAIN 外的其他错误需立即处理ECONNRESETEPIPE 表示连接异常,应关闭 fd。
6️⃣ 与 poll/epoll 配合poll 等待事件 + 非阻塞 read 循环读取经典组合:poll 阻塞等待事件触发,socket 非阻塞读写数据。
7️⃣ 大量连接优化使用非阻塞 IO 减少线程数量单线程可同时处理成千上万连接(尤其配合 epoll)。
8️⃣ 避免忙等不要在无事件时不断调用 read正确做法:等待 poll/epoll 的可读事件。
9️⃣ 结合缓冲设计应有用户态缓冲区存储未处理数据read 可能多次读到数据,应用层应能拼接/拆包。
🔟 调试建议打印 errno 日志并识别 EAGAIN 与真错误避免误判为异常关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值