嵌入式学习 day39:TCP并发服务器模型

一、TCP并发服务器

1.1 为什么需要并发?

单线程TCP服务器的流程是:

text

复制代码
accept() → 接收一个客户端 → recv()/send() 处理 → 客户端断开 → 再 accept() 下一个

如果某个客户端不发送数据,recv 会一直阻塞,其他客户端只能干等着。实际服务器不可能这样------必须同时服务多个客户端。

1.2 多线程并发模型

思路 :主线程负责 accept 接受连接,每来一个客户端就创建一个新线程,由子线程负责处理该客户端的收发数据。

c

复制代码
// 主线程
while (1) {
    confd = accept(sockfd, NULL, NULL);
    pthread_create(&tid, NULL, client_handler, (void *)&confd);
    pthread_detach(tid);   // 分离线程,自动回收
}

// 子线程
void *client_handler(void *arg)
{
    int confd = *(int *)arg;
    char buf[128];
    while (1) {
        ssize_t n = recv(confd, buf, sizeof(buf), 0);
        if (n <= 0) break;   // 客户端断开
        send(confd, buf, n, 0);
    }
    close(confd);
    return NULL;
}

优点 :每个客户端独立处理,互不影响

缺点:每个客户端创建一个线程,并发量大时(成百上千)系统资源消耗大

二、IO模型

2.1 阻塞IO

特点:数据没准备好时,函数一直阻塞等待,不占用CPU。

c

复制代码
recv(confd, buf, sizeof(buf), 0);   // 没数据时阻塞

优点 :效率高(无数据时不占CPU)

缺点:一个阻塞的IO操作会卡住整个线程

2.2 非阻塞IO

特点:数据没准备好时立即返回,不阻塞。需要不断轮询检测是否有数据。

c

复制代码
// 设置文件描述符为非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 非阻塞读取
ssize_t n = recv(fd, buf, sizeof(buf), 0);   // 没数据时立即返回-1,errno=EAGAIN

优点 :不阻塞线程,可以同时处理多个任务

缺点:轮询会占用CPU

2.3 信号驱动IO(异步IO)

特点 :内核监测文件描述符,有事件发生时发送 SIGIO 信号通知进程。

c

复制代码
// 设置信号处理函数
signal(SIGIO, handler);
// 设置异步IO
fcntl(fd, F_SETOWN, getpid());
fcntl(fd, F_SETFL, O_ASYNC);

2.4 多路复用IO

特点:用一个线程监听多个文件描述符,哪个有事件就处理哪个。

三、select多路复用

3.1 基本概念

select 监听一个文件描述符集合,当集合中至少有一个描述符产生事件时返回。

3.2 相关函数

c

复制代码
void FD_SET(int fd, fd_set *set);    // 将fd加入集合
void FD_CLR(int fd, fd_set *set);    // 将fd从集合中移除
int FD_ISSET(int fd, fd_set *set);   // 判断fd是否仍在集合中
void FD_ZERO(fd_set *set);           // 清空集合

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

参数

  • nfds:最大文件描述符 + 1

  • readfds:读事件集合(数据可读)

  • writefds:写事件集合(可写)

  • exceptfds:异常事件集合

  • timeout:超时时间(NULL为阻塞,0为立即返回)

3.3 select示例

c

复制代码
fd_set readfds;
int maxfd = sockfd;

while (1) {
    FD_ZERO(&readfds);
    FD_SET(sockfd, &readfds);   // 监听监听套接字
    // 同时监听所有已连接的客户端...
    
    int ret = select(maxfd + 1, &readfds, NULL, NULL, NULL);
    if (ret <= 0) continue;
    
    // 有新连接
    if (FD_ISSET(sockfd, &readfds)) {
        confd = accept(sockfd, NULL, NULL);
        // 将confd加入监听集合
    }
    
    // 有客户端发数据
    for (i = 0; i < client_count; i++) {
        if (FD_ISSET(client_fds[i], &readfds)) {
            recv(client_fds[i], buf, sizeof(buf), 0);
            // 处理数据...
        }
    }
}

四、epoll多路复用

4.1 epoll vs select

select epoll
监听数量 受限制(通常1024) 无限制
效率 O(n)遍历 O(1) 直接返回活跃事件
内存拷贝 每次拷贝 只拷贝一次
触发模式 水平触发 水平触发 / 边沿触发

4.2 epoll函数

epoll_create :创建事件表

c

复制代码
int epoll_create(int size);

epoll_ctl : 操作事件表

c

复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op 含义
EPOLL_CTL_ADD 添加事件
EPOLL_CTL_MOD 修改事件
EPOLL_CTL_DEL 删除事件

c

复制代码
struct epoll_event {
    uint32_t events;     // EPOLLIN / EPOLLOUT / EPOLLET
    epoll_data_t data;   // 用户数据(通常放fd)
};

epoll_wait : 等待事件

c

复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

4.3 epoll示例

c

复制代码
#include <sys/epoll.h>

// 1. 创建epoll实例
int epfd = epoll_create(1);

// 2. 添加监听套接字到事件表
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

struct epoll_event events[1024];

while (1) {
    // 3. 等待事件发生
    int nfds = epoll_wait(epfd, events, 1024, -1);
    if (nfds <= 0) continue;
    
    for (int i = 0; i < nfds; i++) {
        int fd = events[i].data.fd;
        
        if (fd == sockfd) {
            // 有新连接
            int confd = accept(sockfd, NULL, NULL);
            // 将confd加入事件表
            ev.events = EPOLLIN;
            ev.data.fd = confd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, confd, &ev);
        } else {
            // 有数据可读
            char buf[128];
            ssize_t n = recv(fd, buf, sizeof(buf), 0);
            if (n <= 0) {
                // 客户端断开
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
            } else {
                // 处理数据...
                send(fd, buf, n, 0);
            }
        }
    }
}

4.4 水平触发 vs 边沿触发

水平触发(默认) 边沿触发(EPOLLET)
触发时机 数据可读就触发 数据状态变化时触发一次
处理方式 可分批处理 必须一次性读完
难度 简单 需要非阻塞 + 循环读

五、今日总结

今天学了TCP并发服务器IO模型,核心要点如下:

TCP并发服务器

  1. 多线程模型:主线程 accept,子线程处理每个客户端

  2. 并发量大时可用线程池管理

IO模型

  1. 阻塞IO:效率高,但单线程无法处理多路IO

  2. 非阻塞IO:不阻塞但轮询浪费CPU

  3. 信号驱动IO:内核信号通知

  4. 多路复用IO:select / epoll

select vs epoll

  1. select 简单但效率低(O(n)),有数量限制

  2. epoll 高效(O(1)),无数量限制,支持边沿触发

今日感悟

多线程模型的好处是每个客户端互不干扰,一个客户端卡住不会影响其他。但线程创建和切换是有开销的,如果同时有1000个客户端,系统会扛不住,因此会使用模型来解决这个问题。

select 的核心思想是"把所有要等的文件描述符放到一个篮子里,谁有动静就处理谁"。这样单线程也能处理多个客户端,避免了创建大量线程的开销。但 select 有它的限制,文件描述符数量上限(通常是1024),而且每次都要遍历整个集合来找哪些有事件。

epollselect 的升级版,核心优化是"不遍历所有,只通知有动静的"。如果监听10000个连接,select 每次要检查10000个,epoll 只返回那少数几个有事件的,效率差距非常大。这也是为什么Nginx这类高并发服务器都用 epoll

相关推荐
Doraemomo40 分钟前
网络编程-复习
网络
智购无人售货机厂家1 小时前
2026自动售货机软硬件版本管理策略:从版本号规范到兼容性矩阵的工程实践~YH
运维·服务器·人工智能·单片机·嵌入式硬件·线性代数·矩阵
2601_966377131 小时前
云上系统零改造过密评|数达安全发布通关四件套
网络·安全
知识分享小能手1 小时前
深度学习学习教程,从入门到精通,概率与信息论 — 知识点详解(3)
人工智能·深度学习·学习·数据挖掘·概率论
spencer_tseng1 小时前
[j2cache ehcache.xml]/tmp/.ehcache-diskstore.lock (Permission denied)
xml·linux·python·ehcache·[j2cache
Wendy不吃榴莲1 小时前
# AI短剧教程 - 《后西游记》开播后,AI影视为什么更考验“连续讲故事”?
人工智能·笔记·学习·ai·视频
Agudamu11611 小时前
B站学习视频怎么变笔记:用 Ai好记 + Obsidian 搭建个人知识库的完整教程
人工智能·笔记·学习·音视频
内核笔记1 小时前
休眠唤醒 standby 调试
linux·bsp
小白不想画工图1 小时前
银河麒麟V10系统安装QT5.12
linux·开发语言·qt·银河麒麟