1. poll的基础
 
 
2. 阻塞/非阻塞分类:poll()阻塞,poll()非阻塞,socket阻塞,socket非阻塞
 
1. poll()是否阻塞通过它的timeout参数控制,如下表
 
| timeout 值 | 行为模式 | 等待特性 | 返回时机 | 常见用途 | 
|---|
| -1 | 阻塞模式 | 永久阻塞等待,直到有事件发生 | 事件发生才返回 | 长连接服务器(推荐) | 
| 0 | 非阻塞模式 | 立即返回,不等待 | 立刻返回(轮询) | 轮询检测、调试场景 | 
| > 0 | 定时阻塞模式 | 最多等待指定毫秒数 | 事件触发或超时返回 | 有超时机制的等待,如心跳检测 | 
2. socket阻塞与非阻塞区别
 
| 对比项 | 阻塞模式( Blocking Socket) | 非阻塞模式( Non-Blocking Socket) | 
|---|
| 行为特性 | 系统调用( read/write/accept)会等待完成才返回 | 系统调用立即返回,如果操作无法立即完成返回 EAGAIN或EWOULDBLOCK | 
| 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设置非阻塞方式代码
 
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
 
#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
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)
    {
        
        int nready = poll(fds, MAX_CLIENTS, -1);
        if (nready < 0)
        {
            if (errno == EINTR)
                continue;
            perror("poll");
            break;
        }
        
        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, 1)) > 0)
                {
                    printf("%s\n", buf);
                    
                }
                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 = EAGAIN或EWOULDBLOCK | 这不是错误,只代表“现在暂时没有数据”。应等待下一次 poll/epoll 通知后再读。 | 
| 3️⃣ 必须循环读取 | 在收到“可读事件”后,应循环 read()直到返回EAGAIN | 因为一次可读事件可能对应多次可读数据,读不完会导致数据滞留内核缓冲区。 | 
| 4️⃣ 处理断开连接 | 当 read()返回 0 表示对端关闭连接 | 应立即关闭 fd 并清理资源。 | 
| 5️⃣ 错误处理 | 除 EAGAIN外的其他错误需立即处理 | 如 ECONNRESET、EPIPE表示连接异常,应关闭 fd。 | 
| 6️⃣ 与 poll/epoll 配合 | poll 等待事件 + 非阻塞 read 循环读取 | 经典组合: poll阻塞等待事件触发,socket 非阻塞读写数据。 | 
| 7️⃣ 大量连接优化 | 使用非阻塞 IO 减少线程数量 | 单线程可同时处理成千上万连接(尤其配合 epoll)。 | 
| 8️⃣ 避免忙等 | 不要在无事件时不断调用 read | 正确做法:等待 poll/epoll 的可读事件。 | 
| 9️⃣ 结合缓冲设计 | 应有用户态缓冲区存储未处理数据 | read 可能多次读到数据,应用层应能拼接/拆包。 | 
| 🔟 调试建议 | 打印 errno 日志并识别 EAGAIN 与真错误 | 避免误判为异常关闭。 |