Linux软件编程学习笔记(十二)——TCP并发服务器模型

一、什么是TCP并发服务器

普通的 TCP 服务器通常只能处理一个客户端:

复制代码
客户端A ──→ 服务器
             ↓
          处理A
             ↓
          处理完成

如果客户端A一直连接,客户端B就可能无法及时得到处理。

所以需要使用TCP并发服务器模型,让服务器可以同时处理多个客户端。

常见的实现方式:

复制代码
TCP并发服务器
     │
     ├── 多线程
     │
     ├── 多进程
     │
     └── IO多路复用
           ├── select
           └── epoll

二、TCP并发服务器之多线程

多线程可以解决 TCP 并发问题。

基本思路:

复制代码
                 服务器
                   │
              accept客户端
                   │
        ┌──────────┼──────────┐
        ↓          ↓          ↓
      线程1       线程2       线程3
        ↓          ↓          ↓
      客户端A     客户端B     客户端C

服务器每接收到一个客户端,就创建一个线程处理这个客户端。

例如:

复制代码
while (1)
{
    newfd = accept(sockfd, NULL, NULL);

    pthread_create(&tid, NULL, client_fun, &newfd);
}

多线程的特点

线程拥有独立的栈空间,因此系统能够同时创建的线程数量是有限的。

如果同时连接的客户端非常多,就可能创建大量线程,从而导致系统资源消耗过大,甚至出现系统崩溃的风险。

所以实际开发中通常会配合线程池使用。

课程文档也指出,多线程通常搭配线程池进行线程管理。


三、4种常见的IO模型

Linux 中常见的 IO 模型有:

  1. 阻塞 IO

  2. 非阻塞 IO

  3. 信号驱动 IO

  4. 多路复用 IO


1. 阻塞IO

阻塞 IO 是最常见、最简单的 IO 方式。

例如:

复制代码
recv(sockfd, buf, sizeof(buf), 0);

如果当前没有数据:

复制代码
recv()
  ↓
没有数据
  ↓
进程阻塞
  ↓
等待数据到来
  ↓
收到数据
  ↓
继续执行

阻塞期间 CPU 不需要一直执行轮询,因此不会持续占用 CPU。


2. 非阻塞IO

非阻塞 IO 不会一直等待数据。

如果没有数据,函数会立即返回,程序可以继续执行。

例如:

复制代码
recv()
  ↓
没有数据
  ↓
立即返回
  ↓
程序继续运行
  ↓
再次检查

这种方式需要不断轮询检查数据是否到来。

因此,如果一直轮询,CPU 可能会产生较大的资源消耗。

课程文档中将其概括为:没有数据时,CPU 仍会执行代码来轮询检测数据。


3. 信号驱动IO

信号驱动 IO 的基本思想:

复制代码
设置文件描述符
       ↓
内核监测
       ↓
有事件发生
       ↓
发送SIGIO信号
       ↓
用户程序处理事件

也就是说,由内核监测文件描述符的事件,当事件发生时通知用户层。

课程文档中将其描述为:设置文件描述符为异步 IO,并指定通知对象,当事件发生时通知用户层,可以捕捉 SIGIO 信号进行处理。


四、多路复用IO

多路复用 IO 可以让一个程序同时监听多个文件描述符。

例如一个 TCP 服务器:

复制代码
        服务器
           │
     ┌─────┼─────┐
     ↓     ↓     ↓
   客户端A 客户端B 客户端C
     │     │     │
     └─────┼─────┘
           ↓
       select/epoll

当某个客户端有数据到来时,服务器处理对应的文件描述符。

常见的多路复用 IO:

  • select

  • epoll


五、select

select 用来监听一组文件描述符。

当至少有一个文件描述符产生事件时,select 不再阻塞。

1. 常用函数

复制代码
FD_ZERO()
FD_SET()
FD_CLR()
FD_ISSET()
select()

FD_ZERO

复制代码
FD_ZERO(&set);

功能:

将文件描述符集合清空。

FD_SET

复制代码
FD_SET(fd, &set);

功能:

将文件描述符 fd 加入集合。

FD_CLR

复制代码
FD_CLR(fd, &set);

功能:

将文件描述符 fd 从集合中删除。

FD_ISSET

复制代码
FD_ISSET(fd, &set);

功能:

判断文件描述符 fd 是否仍然在集合中。

这些接口的定义和用途见课程文档。


2. select函数

复制代码
int select(int nfds,
           fd_set *readfds,
           fd_set *writefds,
           fd_set *exceptfds,
           struct timeval *timeout);

主要参数:

参数 含义
nfds 最大文件描述符值 + 1
readfds 读文件描述符集合
writefds 写文件描述符集合
exceptfds 异常文件描述符集合
timeout 超时时间

课程文档对 select 的接口和参数进行了上述定义。


六、epoll

epoll 也是 Linux 中常用的 IO 多路复用机制。

主要使用三个函数:

复制代码
epoll_create()
epoll_ctl()
epoll_wait()

课程文档将这三个函数作为 epoll 的核心接口。


1. epoll_create

复制代码
int epoll_create(int size);

功能:

创建内核监听事件表。

返回值:

复制代码
成功:返回文件描述符
失败:返回-1

课程文档说明 size 参数目前可以忽略。


2. epoll_ctl

复制代码
int epoll_ctl(int epfd,
              int op,
              int fd,
              struct epoll_event *event);

功能:

对 epoll 的事件表进行操作。

常用操作:

复制代码
EPOLL_CTL_ADD

添加事件。

复制代码
EPOLL_CTL_MOD

修改事件。

复制代码
EPOLL_CTL_DEL

删除事件。

这些操作以及参数含义见课程文档。


3. struct epoll_event

复制代码
struct epoll_event
{
    uint32_t events;
    epoll_data_t data;
};

常见事件:

复制代码
EPOLLIN

表示文件描述符可以进行读操作。

复制代码
EPOLLOUT

表示文件描述符可以进行写操作。

复制代码
EPOLLET

表示使用边沿触发模式。

默认情况下使用水平触发模式。


七、epoll_wait

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

功能:

等待 epoll 事件表中的事件发生。

参数:

参数 含义
epfd epoll 文件描述符
events 保存发生事件的数组
maxevents 数组大小
timeout 超时时间

timeout:

复制代码
-1      一直阻塞等待
0       立即返回
>0      等待指定时间

返回值:

复制代码
>0   发生事件的文件描述符数量
 0   超时,没有事件
-1   出错

课程文档给出了 epoll_wait 的完整功能、参数和返回值说明。


八、select和epoll的区别

这是本节非常重要的内容。

对比 select epoll
文件描述符集合 有上限 没有这个上限限制
监听位置 应用层 内核
事件通知 需要用户查找 可以直接获得产生事件的文件描述符
触发模式 水平触发 水平触发、边沿触发
效率 相对较低 更适合大量连接

select

复制代码
用户空间
   ↓
文件描述符集合
   ↓
内核检查
   ↓
返回用户空间
   ↓
用户自己查找哪个fd发生事件

epoll

复制代码
用户程序
   ↓
epoll事件表
   ↓
内核持续监听
   ↓
产生事件
   ↓
epoll_wait返回事件
   ↓
直接处理发生事件的fd

课程文档指出:

  • select 的文件描述符集合本质是数组,有上限;

  • select 需要用户手动查找产生事件的文件描述符;

  • epoll 的事件表位于内核;

  • epoll 可以自动获得产生事件的文件描述符;

  • epoll 支持水平触发和边沿触发。


九、TCP并发服务器的核心思想

最终可以把 TCP 并发服务器理解成:

复制代码
                TCP服务器
                    │
              socket/bind/listen
                    │
                  等待
                    │
        ┌───────────┼───────────┐
        ↓           ↓           ↓
      客户端A      客户端B      客户端C
        │           │           │
        └───────────┼───────────┘
                    ↓
              并发处理客户端
                    │
          ┌─────────┴─────────┐
          ↓                   ↓
       多线程              IO多路复用
                              │
                       ┌──────┴──────┐
                       ↓             ↓
                     select        epoll

十、本节重点

1. TCP并发服务器

解决:

一个服务器同时处理多个客户端的问题。

2. 多线程

复制代码
pthread_create()

一个客户端对应一个线程,但客户端过多时线程数量会受到限制,因此通常搭配线程池。

3. select

核心函数:

复制代码
FD_ZERO()
FD_SET()
FD_CLR()
FD_ISSET()
select()

4. epoll

核心函数:

复制代码
epoll_create()
epoll_ctl()
epoll_wait()

5. epoll事件

复制代码
EPOLLIN
EPOLLOUT
EPOLLET

6. 最重要的区别

复制代码
select:
需要自己遍历查找哪个fd发生事件

epoll:
可以直接获取发生事件的fd
相关推荐
小羊没烦恼!3 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里3 天前
java中的继承和多态的区别
java
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕3 天前
JSONObject与JSONArray封装数据格式区别
java·json
虎头金猫3 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
胡写代码3 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖3 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时3 天前
01-06-A-JVM排查实战详解
java·jvm
vipxieliang3 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
ba_pi3 天前
mysql 查询所有表名并授权
java