嵌入式Linux网络编程:IO多路复用实现


嵌入式Linux网络编程:IO多路复用实现高并发服务端

【本文代码已在 Linux 平台验证通过】


一、IO多路复用核心原理

1.1 三种IO复用对比

特性selectpollepoll
支持最大连接数1024(固定)无限制无限制
效率O(n)线性扫描O(n)线性扫描O(1)事件通知
内核支持所有平台所有平台Linux特有
内存拷贝每次复制全量fd每次复制全量fd内存映射
适用场景低并发嵌入式设备中等并发设备高并发服务器

1.2 epoll核心工作机制

epoll_create
创建epoll实例
epoll_ctl添加事件
epoll_wait等待事件
处理就绪事件

二、epoll高并发服务端实现

2.1 完整服务端代码(epoll_server.c)

#define _GNU_SOURCE  // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096
#define PORT 8080

int main() {
    int server_fd, epoll_fd;
    struct sockaddr_in addr;
    struct epoll_event ev, events[MAX_EVENTS];
    
    // 1. 创建TCP套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) == -1) {
        perror("socket创建失败");
        exit(EXIT_FAILURE);
    }

    // 2. 配置服务器地址
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);

    // 3. 绑定套接字
    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("绑定失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 开始监听
    if (listen(server_fd, SOMAXCONN) == -1) {
        perror("监听失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("服务端已启动,监听端口:%d\n", PORT);

    // 5. 创建epoll实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll创建失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 6. 注册监听套接字到epoll
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
        perror("epoll_ctl添加失败");
        close(server_fd);
        close(epoll_fd);
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 7. 等待事件(阻塞模式)
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait错误");
            continue;
        }

        // 8. 处理所有就绪事件
        for (int i = 0; i < nfds; ++i) {
            // 处理新连接
            if (events[i].data.fd == server_fd) {
                struct sockaddr_in client_addr;
                socklen_t addr_len = sizeof(client_addr);
                int client_fd = accept4(server_fd, 
                                      (struct sockaddr*)&client_addr,
                                      &addr_len, 
                                      SOCK_NONBLOCK);
                if (client_fd == -1) {
                    perror("接受连接失败");
                    continue;
                }

                // 打印客户端信息
                char client_ip[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &client_addr.sin_addr, 
                         client_ip, INET_ADDRSTRLEN);
                printf("新客户端连接: %s:%d\n", 
                      client_ip, ntohs(client_addr.sin_port));

                // 注册客户端socket到epoll
                ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
                ev.data.fd = client_fd;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                    perror("添加客户端到epoll失败");
                    close(client_fd);
                }
            } 
            // 处理客户端数据
            else {
                int client_fd = events[i].data.fd;
                char buffer[BUFFER_SIZE];
                
                // 边缘触发模式必须读取所有数据
                while (1) {
                    ssize_t count = recv(client_fd, buffer, BUFFER_SIZE, 0);
                    if (count == -1) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) 
                            break; // 数据读取完毕
                        perror("接收错误");
                        close(client_fd);
                        break;
                    } else if (count == 0) {
                        // 客户端断开连接
                        printf("客户端 %d 断开连接\n", client_fd);
                        close(client_fd);
                        break;
                    }

                    // 处理数据(示例:回显)
                    buffer[count] = '\0';
                    printf("收到来自 %d 的数据: %s", client_fd, buffer);
                    send(client_fd, buffer, count, 0);
                }
            }
        }
    }

    close(server_fd);
    close(epoll_fd);
    return EXIT_SUCCESS;
}

三、代码编译与测试

3.1 编译命令

gcc epoll_server.c -o epoll_server -Wall -O2

3.2 测试方法

# 启动服务端
./epoll_server

# 使用nc测试(新终端)
nc localhost 8080

# 并发测试(使用10个客户端)
for i in {1..10}; do (echo "Client $i" | nc localhost 8080 &); done


四、关键函数深度解析

4.1 epoll核心三剑客

1. epoll_create1
int epoll_create1(int flags);

核心作用
创建epoll实例的内核数据结构,返回用于操作该实例的文件描述符

参数详解

flags取值作用说明
0默认模式
EPOLL_CLOEXEC设置close-on-exec标志(推荐使用)

底层原理

  • 内核创建eventpoll结构体(红黑树+双向链表)
  • 返回的文件描述符指向该结构体
  • 每个epoll实例独立管理监控的文件描述符集合

使用示例

int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
    perror("epoll创建失败");
    exit(EXIT_FAILURE);
}

2. epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能解析
通过增删改操作管理epoll监控的文件描述符集合

参数解析

参数合法取值说明
opEPOLL_CTL_ADD添加新fd到监控集
EPOLL_CTL_MOD修改已存在fd的监控事件
EPOLL_CTL_DEL从监控集移除fd
eventstruct epoll_event事件配置结构体(详见下文)

epoll_event结构体

struct epoll_event {
    uint32_t     events;    // 监控的事件集
    epoll_data_t data;      // 用户数据(联合体)
};

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

事件标志详解

事件类型触发条件
EPOLLIN关联的fd可读(包括对端socket关闭)
EPOLLOUT关联的fd可写
EPOLLRDHUP流式socket对端关闭连接或半关闭
EPOLLPRI紧急数据到达
EPOLLERRfd发生错误
EPOLLHUPfd被挂起
EPOLLET开启边缘触发模式(默认水平触发)
EPOLLONESHOT单次触发(需重新注册)

使用示例

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 边缘触发读事件
ev.data.fd = sockfd;

if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
    perror("epoll_ctl添加失败");
    close(sockfd);
}

3. epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,
              int maxevents, int timeout);

核心机制

  • 阻塞等待直到有事件就绪或超时
  • 返回就绪事件数量,填充到events数组中

参数详解

参数作用范围特殊说明
maxevents>0每次最多返回的事件数
timeout-1(无限等待)单位:毫秒
0(立即返回)
>0(超时时间)

返回值解析

返回值含义典型处理场景
>0就绪事件数量遍历处理所有事件
0超时无事件可执行定时任务
-1错误(检查errno)处理信号中断等特殊情况

典型用法

#define MAX_EVENTS 1024
struct epoll_event events[MAX_EVENTS];

int ready = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
if (ready == -1) {
    if (errno == EINTR) {
        // 被信号中断,可继续等待
        continue;
    }
    perror("epoll_wait错误");
    break;
}

for (int i = 0; i < ready; ++i) {
    // 处理每个就绪事件...
}

4.2 网络编程基础函数

1. socket
int socket(int domain, int type, int protocol);

参数组合推荐

应用场景domaintypeprotocol
TCP流式socketAF_INETSOCK_STREAM | SOCK_NONBLOCK0
UDP数据报socketAF_INETSOCK_DGRAM | SOCK_NONBLOCK0

非阻塞模式优势

  • 避免在connect/accept等操作中阻塞线程
  • 配合epoll实现全异步IO

2. bind/listen
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);

关键参数

  • backlog:已完成连接队列的最大长度
    (Linux内核2.2+实际值为min(backlog, net.core.somaxconn)

最佳实践

int enable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); // 端口复用

3. accept4
int accept4(int sockfd, struct sockaddr *addr,
           socklen_t *addrlen, int flags);

推荐flags

SOCK_NONBLOCK  // 直接设置为非阻塞模式
SOCK_CLOEXEC   // 执行exec时关闭fd

高并发处理技巧

while ((client_fd = accept4(...)) != -1) {
    // 批量接受新连接
    // 设置epoll监控...
}
if (errno != EAGAIN && errno != EWOULDBLOCK) {
    perror("accept错误");
}

使用条件

// 宏定义要求
#define _GNU_SOURCE
#include <sys/socket.h>

uname -r
版本 ≥ 2.6.28

ldd --version
glibc 版本 ≥ 2.10


4.3 数据收发函数

1. recv/send
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

关键flags

标志位作用说明
MSG_DONTWAIT非阻塞操作(等同O_NONBLOCK)
MSG_NOSIGNAL禁止发送SIGPIPE信号
MSG_MORE提示内核有更多数据待发送(优化TCP)

错误处理要点

ssize_t count = recv(fd, buf, size, MSG_DONTWAIT);
if (count == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 数据未就绪,下次再试
    } else {
        // 真实错误,关闭连接
        close(fd);
    }
}

五、综合使用流程图

App Kernel epoll_create1() socket()+bind()+listen() epoll_ctl(EPOLL_CTL_ADD) epoll_wait() 返回就绪事件 accept4()[无阻塞] epoll_ctl(EPOLL_CTL_ADD) recv() send() alt [新连接] [数据到达] loop [事件循环] close() App Kernel

六、环境优化建议

6.1 边缘触发(ET) vs 水平触发(LT)

// 边缘触发模式(需循环读取)
ev.events = EPOLLIN | EPOLLET;

// 水平触发模式(默认)
ev.events = EPOLLIN;

6.2 性能优化技巧

  1. 调整最大事件数

    #define MAX_EVENTS 4096 // 根据系统资源调整
    
  2. 使用内存池

    // 预分配缓冲区内存
    static char buffer_pool[MAX_EVENTS][BUFFER_SIZE];
    
  3. 设置合理超时

    int timeout = 100; // 100毫秒
    epoll_wait(epfd, events, MAX_EVENTS, timeout);
    

6.3 资源管理

// 监控系统资源
struct rlimit lim;
getrlimit(RLIMIT_NOFILE, &lim);
printf("最大文件描述符数: %ld\n", lim.rlim_cur);

// 设置最大连接数
lim.rlim_cur = 100000;
setrlimit(RLIMIT_NOFILE, &lim);

通过本文实现的epoll服务端,可在嵌入式Linux设备上轻松应对数千并发连接。后续可扩展实现协议解析、负载均衡等高级功能,构建高性能物联网网关!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

银河码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值