网站搭建平台苏州网站制作 网站
- 作者: 五速梦信息网
- 时间: 2026年04月20日 08:14
当前位置: 首页 > news >正文
网站搭建平台,苏州网站制作 网站,aspcms中引文 网站修改配置,做网站哪间好本文主要介绍服务端对于网络并发模型以及Linux系统下常见的网络IO复用并发模型。文章内容一共分为两个部分。 第一部分主要介绍网络并发中的一些基本概念以及我们Linux下常见的原生IO复用系统调用#xff08;epoll/select#xff09;等。第二部分主要介绍并发场景下常见的网…本文主要介绍服务端对于网络并发模型以及Linux系统下常见的网络IO复用并发模型。文章内容一共分为两个部分。 第一部分主要介绍网络并发中的一些基本概念以及我们Linux下常见的原生IO复用系统调用epoll/select等。第二部分主要介绍并发场景下常见的网络IO复用模型以及各自的优缺点。 一、网络并发模型中的几个基本概念 1 流 开发过程中一般给流的定义有很多种这里面我们总结用三个特征来描述一个流的定义 1可以进行I/O操作的内核对象。 2传输媒介可以是文件、管道、套接字等。 3数据的入口是通过文件描述符(fd)。 2 I/O操作 其实所有对流的读写操作都可以称之为IO操作,如图1.1.2所示是向一个已经满的流再去执行写入操作那么这次的写入实际上就是一个IO操作。当然已经将流写满了那么这次写入就会发生阻塞。 图1.1 传输媒介已满再写入的IO操作 那么如果流为空的情况再执行读操作那么这次读取也是一个IO操作也依然会发生阻塞的情况。 如图1.1和图1.2所示他们都是对IO的操作当我们向一个容量已经满的传输媒介写数据的时候那么这个IO操作就会发生写阻塞。同理当我们从一个容量为空的传输媒介读数据的时候这个IO操作就会发生读阻塞。 3 阻塞等待 通过IO的读写过程我们得到了阻塞的概念。那么我们如何来形象地表示一个阻塞的现象呢 图1.3 阻塞等待 如图1.3所示假设您今天清闲在家无事可做您家里有一部座机这个是您唯一可以和外界建立沟通的媒介。假设您今天准备洗一双袜子但是缺少一块肥皂这个肥皂在等待快递员给您送过来而您又是一个“单细胞动物”今天必须要先把袜子洗完才能做其他的事否则无事可做。那么此时您的这种在一天的生活流程中因等待某个资源导致生活节奏暂停的状态就是阻塞等待状态。 4 非阻塞忙轮询 图1.4 非阻塞忙轮询 与阻塞等待相对应的状态是非阻塞忙轮询状态。假设您性子比较急躁每分钟必须要打电话询问快递小哥一次“到底有没有到”那么快递员每隔一段时间就会接听到你的电话询问并告诉你是否到了这样你就可以主动地指导你所缺的肥皂资源是否已经抵达。那么您此种不断通过通讯媒介询问对方并且循环往复的状态就是一种非阻塞忙轮询的状态。 5 阻塞与非阻塞对比 阻塞等待空出大脑可以安心睡觉不影响快递员工作不占用CPU宝贵的时间片。 非阻塞忙轮询浪费时间浪费电话费占用快递员时间占用CPU系统资源。 很明显通过上述的场景作为比较阻塞等待这种方式对于通信上是有明显优势的阻塞等待也并非是没有弊端的。 二、解决阻塞等待缺点的办法 1 阻塞死等待的缺点 阻塞等待也是有非常明显的缺点的现在我们比如有如下场景如图1.5所示。 图1.5 阻塞 同一时刻你只能被动地处理一个快递员的签收业务其他快递员打电话打不进来你的电话是座机在签收的时候接不到其他快递员的电话。所以阻塞等待的问题很明显我们无法在同一时刻解决多个IO的读写请求。 2 解决阻塞等待的办法一:多线程/多进程 那么解决这个问题即使家里多买N个座机 但是依然是你一个人接也处理不过来所以就需要用“影分身术”创建都个自己来接电话(采用多线程或者多进程来处理如图1.6所示。 图1.6 办法一提高资源(开多线程/多进程) 这种方式就是没有多路IO复用的情况的解决方案但是大量的开辟线程和进程也是非常浪费资源的我们知道一个操作系统能够同时运行的线程和进程都是有上限的尤其是进程占用内存的资源极高这样也就限定了能够同时处理IO数量的瓶颈。 3 解决阻塞等待的办法二非阻塞、忙轮询 那么如果我们不借助影分身的方式(多线程/多进程)该如何解决阻塞死等待的方法呢 如果我们是采用非阻塞的方式那么可以用一个办法来监控多个IO的状态我们可以采用粗暴的“非阻塞忙轮询”方式如图1.7所示。 图1.7 办法二非阻塞忙轮询 非阻塞忙轮询的方式可以让用户分别与每个快递员取得联系宏观上来看是同时可以与多个快递员沟通(并发效果)、 但是快递员在于用户沟通时耽误前进的速度(浪费CPU)。 非阻塞忙轮询的方式具体的实现逻辑伪代码如下 while true {for i in 流[] {if i has 数据 {读 或者 其他处理}} } 一层循环下不断的遍历流是否有数据如果没有或者有则进行逻辑处理然后不停歇地进入下一次遍历直到for循环出来又回到while true的无限重复次数中。所以非阻塞忙轮询虽然能够在短暂的时间内监控到每个IO的读写状态但是付出的代价是无限不停歇的判断这样往往会使CPU过于劳累把CPU资源打满所以非阻塞忙轮询并不是一个非常好的解决方案。 相关视频推荐 手写一个epoll组件为tcp并发实现epoll 服务端的网络并发详解网络io与线程进程的关系 准备好4台虚拟机实现服务器的百万级并发 Linux C/C开发后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全 需要C/C Linux服务器架构师学习资料加qun812855908获取资料包括C/CLinuxgolang技术NginxZeroMQMySQLRedisfastdfsMongoDBZK流媒体CDNP2PK8SDockerTCP/IP协程DPDKffmpeg等免费分享 4 解决阻塞等待的办法三select 我们可以开设一个代收网点让快递员全部送到代收点。这个网店管理员叫select。这样我们就可以在家休息了麻烦的事交给select就好了。当有快递的时候select负责给我们打电话期间在家休息睡觉就好了。 图1.8办法三select 但select 代收员比较懒她记不住快递员的单号还有快递货物的数量。她只会告诉你快递到了但是是谁到的你需要挨个快递员问一遍。实现的逻辑伪代码如下 while true {//阻塞select(流[]);//有消息抵达for i in 流[] {if i has 数据 {读 或者 其他处理}} } Select的实现逻辑在while true的外层循环下会有一个阻塞的过程这个阻塞并不是永久阻塞而是当select所监听的流中(多个IO传输媒介)有一个流可以读写那么select就会立刻返回当我们得知select已经返回说目前的流一定是具备读写能力的那么这时候就可以遍历这个流中如果流有数据我们就读出来处理如果没有就看下一个流是否有。 用select并不会出现非阻塞忙轮询的无限判断情况的出现因为select是可以阻塞的阻塞的时候是不占用任何CPU资源的但select有个明显的缺点因为他每次都会返回全量的流集合并不会告诉开发者哪个流可读写哪个流不可读写我们需要再循环全量的流集合再进行判断是否读写所以即使有一个流触发我们依然for要全部流都扫描一遍这显然是一种低效的方式。 5 解决阻塞等待的办法四epoll 现在这个快递代收驿站升级了服务更加友善且能力更强与select一样你依然可以在家休息被动的接收epoll发来的通知如图1.9所示。 图1.9 办法四epoll epoll的服务态度要比select好很多在通知我们的时候不仅告诉我们有几个快递到了还分别告诉我们是谁谁谁。我们只需要按照epoll给的答复来询问快递员取快递即可。实现逻辑的伪代码如下 while true {//阻塞可处理的流[] epoll_wait(epoll_fd);//有消息抵达全部放在 “可处理的流[]”中for i in 可处理的流[] {读 或者 其他处理} } 使用epoll来解决监听多个IO的逻辑和select极为相似在使用过程中有个重大的区别在于epoll_wait发生阻塞的时候如果监控的流中有IO可以读写那么epoll_wait会给我们返回一个可以读写流的集合那么不可以读写的流epoll并不会返回给我们。这样实际上会减少我们的无效的遍历这一点epoll要比select做得毕竟优秀。另外一个地方在实现逻辑中看不出来select能够最大监听IO的数量是一个固定的数这个可以修改但是毕竟困难需要重新编译操作系统而且这个数量也不是很大但是epoll能够监听的最大的IO数量是跟随当前操作系统的内存大小成正比的。所以epoll在监控IO数量这块也要比select优秀很多。 三、什么是epoll epoll与selectpoll一样是对I/O多路复用的技术它只关心“活跃”的链接无须遍历全部描述符集合它能够处理大量的链接请求(系统可以打开的文件数目取决于内存大小)。 1 Linux提供的epoll的系统调用 epoll的开发流程是属于linux操作系统提供给用户态开发者的一系列系统调用函数这些函数的直接接口都是C语言实现。所以开发者一般基于epoll进行开发的话一般都是基于C语言进行开发这样最接近操作系统性能上也是最优的方法。 epoll的开发流程基本分为三大步骤 第一步创建epoll 第二步控制epoll 第三步等待epoll。 接下来我们来看一下Linux给开发者提供的epoll的原生接口是什么样子的。 1创建EPOLL 原型如下 /*** param size 告诉内核监听的数目* returns 返回一个epoll句柄即一个文件描述符*/ int epoll_create(int size); int epfd epoll_create(1000); 当我们执行上述代码时在内核中实则是创建一颗红黑树平衡二叉树的根节点root如图1.10所示。 图1.10 epoll系统调用1 这个根节点的关系与epfd相对应。 2控制EPOLL 原型如下 /**
- param epfd 用epoll_create所创建的epoll句柄
- param op 表示对epoll监控描述符控制的动作 *
- EPOLL_CTL_ADD(注册新的fd到epfd)
- EPOLL_CTL_MOD(修改已经注册的fd的监听事件)
- EPOLL_CTL_DEL(epfd删除一个fd) *
- param fd 需要监听的文件描述符
- param event 告诉内核需要监听的事件 *
- returns 成功返回0失败返回-1, errno查看错误信息 */ int epoll_ctl(int epfd, int op, int fd, struct epoll_event event);struct epoll_event {__uint32_t events; / epoll 事件 /epoll_data_t data; / 用户传递的数据 / }/** events : {EPOLLIN, EPOLLOUT, EPOLLPRI,EPOLLHUP, EPOLLET, EPOLLONESHOT}/ typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64; } epoll_data_t;struct epoll_event new_event;new_event.events EPOLLIN | EPOLLOUT; new_event.data.fd 5;epoll_ctl(epfd, EPOLL_CTL_ADD, 5, new_event); 创建一个用户态的事件绑定到某个fd上然后添加到内核中的epoll红黑树中如图1.11所示。 图1.11 epoll系统调用2 3等待EPOLL 原型如下 /** *
- param epfd 用epoll_create所创建的epoll句柄
- param event 从内核得到的事件集合
- param maxevents 告知内核这个events有多大,
- 注意: 值 不能大于创建epoll_create()时的size.
- param timeout 超时时间
- -1: 永久阻塞
- 0: 立即返回非阻塞
- 0: 指定微秒 *
- returns 成功: 有多少文件描述符就绪,时间到时返回0
- 失败: -1, errno 查看错误 */ int epoll_wait(int epfd, struct epoll_event *event,int maxevents, int timeout);struct epoll_event my_event[1000];int event_cnt epoll_wait(epfd, my_event, 1000, -1); epoll_wait是一个阻塞的状态如果内核检测到IO的读写响应会抛给上层的epoll_wait, 返回给用户态一个已经触发的事件队列同时阻塞返回。开发者可以从队列中取出事件来处理其中事件里就有绑定的对应fd是具体哪一个(之前添加epoll事件的时候已经绑定)如图1.12所示。 图1.12 epoll系统调用3 比如这次epoll_wait返回的就是一个my_event集合其中每一个元素均是一个event结构体结构体里面有两个重要的元素第一个是当前的事件类型如EPOLLIN或者EPOLLOUT分别对应读写事件一个是当前event所绑定的fd也可以绑定任意指针。如图10.12所示epoll_wait触发了2个事件那么开发者只需要遍历my_event依次处理每个event事件就可以了。 4使用epoll编程主流程骨架 下面一段代码是基于epoll开发的一段主干代码 int epfd epoll_create(1000);//将 listen_fd 添加进 epoll 中 epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd,listen_event);while (1) {//阻塞等待 epoll 中 的fd 触发int active_cnt epoll_wait(epfd, events, 1000, -1);for (i 0 ; i active_cnt; i) {if (evnets[i].data.fd listen_fd) {//accept. 并且将新accept 的fd 加进epoll中.}else if (events[i].events EPOLLIN) {//对此fd 进行读操作}else if (events[i].events EPOLLOUT) {//对此fd 进行写操作}} } 这里并没有加数据处理和网络处理实现只是说明epoll的事件交互流程。我们一般写一个服务端代码首先要对listen_fd监听端口的fd进行读事件的监听并且将这个事件放置在epoll堆里当listen_fd触发可读事件那就说明有新的客户端链接创建过来那么epoll_wait的阻塞就会返回我们通过判断fd是否为listen_fd来做与客户端建立链接的动作 那么将建立好的链接添加放置到epoll堆里等待下次可读可写事件触发这就是epoll开发的基本流程。 四、epoll的触发模式 本节作为附加小节实则介绍epoll的触发模式种类如果不想进一步关心epoll的触发方式的读者可以越过本节。 epoll给开发者提供了两种触发模式他们分别是水平触发与边缘触发。 1 水平触发 水平触发Level Triggered简称LT的主要特点是如果用户在监听epoll事件当内核有事件的时候会拷贝给用户态事件但是如果用户只处理了一次那么剩下没有处理的会在下一次epoll_wait再次返回该事件如图1.13和1.14所示。 图1.13 epoll系统调用4 图1.14 epoll系统调用5 这样如果用户永远不处理这个事件就导致每次都会有该事件从内核到用户的拷贝如图1.15所示耗费性能但是水平触发相对安全最起码事件不会丢掉除非用户处理完毕。 图1.15 epoll系统调用6 2 边缘触发 边缘触发Edge Triggered简称ET相对跟水平触发相反当内核有事件到达 只会通知用户一次至于用户处理还是不处理以后将不会再通知。这样减少了拷贝过程增加了性能但是相对来说如果用户马虎忘记处理将会产生事件丢的情况如图1.16所示。 图1.16 epoll系统调用7 五、简单的epoll服务器 1 服务端实现 #include stdio.h #include stdlib.h #include ctype.h #include string.h#include unistd.h #include sys/types.h #include sys/socket.h #include arpa/inet.h#include sys/epoll.h#define SERVER_PORT (7778) #define EPOLL_MAX_NUM (2048) #define BUFFER_MAX_LEN (4096)char buffer[BUFFER_MAX_LEN];void str_toupper(char *str) {int i;for (i 0; i strlen(str); i ) {str[i] toupper(str[i]);} }int main(int argc, char **argv) {int listen_fd 0;int client_fd 0;struct sockaddr_in server_addr;struct sockaddr_in client_addr;socklen_t client_len;int epfd 0;struct epoll_event event, my_events;/ socketlisten_fd socket(AF_INET, SOCK_STREAM, 0);// bindserver_addr.sin_family AF_INET;server_addr.sin_addr.s_addr htonl(INADDR_ANY);server_addr.sin_port htons(SERVER_PORT);bind(listen_fd, (struct sockaddr)server_addr, sizeof(server_addr));// listenlisten(listen_fd, 10);// epoll createepfd epoll_create(EPOLL_MAX_NUM);if (epfd 0) {perror(epoll create);goto END;}// listen_fd - epollevent.events EPOLLIN;event.data.fd listen_fd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, event) 0) {perror(epoll ctl add listen_fd );goto END;}my_events malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM);while (1) {// epoll waitint active_fds_cnt epoll_wait(epfd, my_events, EPOLL_MAX_NUM, -1);int i 0;for (i 0; i active_fds_cnt; i) {// if fd listen_fdif (my_events[i].data.fd listen_fd) {//acceptclient_fd accept(listen_fd, (struct sockaddr*)client_addr, client_len);if (client_fd 0) {perror(accept);continue;}char ip[20];printf(new connection[%s:%d]\n, inet_ntop(AF_INET, client_addr.sin_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port));event.events EPOLLIN | EPOLLET;event.data.fd client_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, event);}else if (my_events[i].events EPOLLIN) {printf(EPOLLIN\n);client_fd my_events[i].data.fd;// do readbuffer[0] \0;int n read(client_fd, buffer, 5);if (n 0) {perror(read);continue;}else if (n 0) {epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, event);close(client_fd);}else {printf([read]: %s\n, buffer);buffer[n] \0;str_toupper(buffer);write(client_fd, buffer, strlen(buffer));printf([write]: %s\n, buffer);memset(buffer, 0, BUFFER_MAX_LEN);/event.events EPOLLOUT;event.data.fd client_fd;epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, event);/}}else if (my_events[i].events EPOLLOUT) {printf(EPOLLOUT\n);/client_fd my_events[i].data.fd;str_toupper(buffer);write(client_fd, buffer, strlen(buffer));printf([write]: %s\n, buffer);memset(buffer, 0, BUFFER_MAX_LEN);event.events EPOLLIN;event.data.fd client_fd;epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, event);/}}}END:close(epfd);close(listen_fd);return 0; } 2 客户端实现 #include stdio.h #include stdlib.h #include string.h #include strings.h#include sys/types.h #include sys/socket.h #include arpa/inet.h #include unistd.h #include fcntl.h#define MAX_LINE (1024) #define SERVER_PORT (7778)void setnoblocking(int fd) {int opts 0;opts fcntl(fd, F_GETFL);opts opts | O_NONBLOCK;fcntl(fd, F_SETFL); }int main(int argc, char *argv) {int sockfd;char recvline[MAX_LINE 1] {0};struct sockaddr_in server_addr;if (argc ! 2) {fprintf(stderr, usage ./client SERVER_IP\n);exit(0);}// 创建socketif ( (sockfd socket(AF_INET, SOCK_STREAM, 0)) 0) {fprintf(stderr, socket error);exit(0);}// server addr 赋值bzero(server_addr, sizeof(server_addr));server_addr.sin_family AF_INET;server_addr.sin_port htons(SERVER_PORT);if (inet_pton(AF_INET, argv[1], server_addr.sin_addr) 0) {fprintf(stderr, inet_pton error for %s, argv[1]);exit(0);}// 链接服务端if (connect(sockfd, (struct sockaddr) server_addr, sizeof(server_addr)) 0) {perror(connect);fprintf(stderr, connect error\n);exit(0);}setnoblocking(sockfd);char input[100];int n 0;int count 0;// 不断的从标准输入字符串while (fgets(input, 100, stdin) ! NULL){printf([send] %s\n, input);n 0;// 把输入的字符串发送 到 服务器中去n send(sockfd, input, strlen(input), 0);if (n 0) {perror(send);}n 0;count 0;// 读取 服务器返回的数据while (1){n read(sockfd, recvline count, MAX_LINE);if (n MAX_LINE){count n;continue;}else if (n 0){perror(recv);break;}else {count n;recvline[count] \0;printf([recv] %s\n, recvline);break;}}}return 0; } 六、Linux下常见的网络IO复用并发模型 本节主要介绍常见的Server的并发模型这些模型与编程语言本身无关有的编程语言可能在语法上直接透明了模型本质所以开发者没必要一定要基于模型去编写只是需要知道和了解并发模型的构成和特点即可本节的一些使用模型需要读者了解基本的多路IO复用知识如一~六章节介绍。 1 模型一单线程Accept无IO复用 模型一是单线程的Server并且不适用任何IO复用机制来实现一个基本的网络服务器。其结构如图1.17所示。 图1.17 网络并发模型一单线程Accept
- 流程 1我们首先启动一个Server服务端进程其中进程包括主线程main thread。我们知道一个基本的服务端Socket编程需要的几个关键步骤创建一个ListenFd服务端监听套接字将这个ListenFd绑定到需要服务的IP和端口上然后执行阻塞Accept被动等待远程的客户端建立链接每次客户端Connect链接过来main thread中accept响应并建立连接。 2这里第一个链接过来的Client1请求服务端链接服务端Server创建链接成功得到Connfd1套接字后, 依然在main thread串行处理套接字读写并处理业务。 3 在2处理业务中如果有新客户端Connect过来Server无响应直到当前套接字全部业务处理完毕。 4当前客户端处理完后完毕链接处理下一个客户端请求。 以上是模型一的服务端整体执行逻辑我们来分析一下模型一的优缺点 2优点 模型一的socket编程流程清晰且简单适合学习使用可以基于模型一很快地了解socket基本编程流程。 3缺点 该模型并非并发模型是串行的服务器同一时刻监听并响应最大的网络请求量为1。即并发量为1。 所以综上仅适合学习基本 socket编程不适合任何服务器Server构建。 2 模型二单线程Accept多线程读写业务无IO复用 模型二是主进程启动一个main thread线程其中main thread在进行socket初始化的过程和模型一是一样的那么对于如果有新的Client建立链接请求进来就会出现和模型一不同的地方如图1.18所示。 图1.18 网络并发模型二单线程Accept多线程读写-1 1流程 1主线程main thread执行阻塞Accept每次客户端Connect链接过来main thread中accept响应并建立连接。 2创建链接成功得到Connfd1套接字后创建一个新线程thread1用来处理客户端的读写业务。main thead依然回到Accept阻塞等待新客户端。 3thread1通过套接字Connfd1与客户端进行通信读写。 4server在2处理业务中如果有新客户端Connect过来main thread中Accept依然响应并建立连接重复2过程如图1.19所示。 图1.19 网络并发模型二单线程Accept多线程读写-2 以上是模型二的服务端整体执行逻辑我们来分析一下模型一的优缺点 2优点 基于模型一单线程Accept无IO复用 支持了并发的特性。使用灵活一个客户端对应一个线程单独处理server处理业务内聚程度高客户端无论如何写服务端均会有一个线程做资源响应。 3缺点 随着客户端的数量增多需要开辟的线程也增加客户端与server线程数量1:1正比关系一次对于高并发场景线程数量收到硬件上限瓶颈。对于长链接客户端一旦无业务读写只要不关闭server的对应线程依然需要保持连接(心跳、健康监测等机制)占用连接资源和线程开销资源浪费。仅适合客户端数量不大并且数量可控的场景使用。仅适合学习基本socket编程不适合任何服务器Server构建。 3 模型三、单线程多路IO复用 1流程 模型三是在单线程的基础上添加多路IO复用机制这样就减少了多开销线程的弊端模型三的流程如下 1主线程main thread创建 listenFd 之后采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求I/O复用机制检测到ListenFd触发读事件则进行Accept建立连接并将新生成的connFd1加入到监听I/O集合中如图1.20所示。 图1.20 网络并发模型三单线程Accept多路IO复用-1 2Client1再次进行正常读写业务请求main thread的多路I/O复用机制阻塞返回会触该套接字的读/写事件等如图1.21所示。 图1.21 网络并发模型三单线程Accept多路IO复用-2 3对于Client1的读写业务Server依然在main thread执行流程提继续执行此时如果有新的客户端Connect链接请求过来Server将没有及时响应如图1.22所示。 图1.22 网络并发模型三单线程Accept多路IO复用-3 4等到Server处理完一个连接的ReadWrite操作继续回到多路I/O复用机制阻塞其他链接过来重复2、3流程。 以上是模型二的服务端整体执行逻辑我们来分析一下模型一的优缺点 2优点 单流程解决了可以同时监听多个客户端读写状态的模型不需要1:1与客户端的线程数量关系。多路I/O复用阻塞非忙询状态不浪费CPU资源 CPU利用率较高。 3缺点 虽然可以监听多个客户端的读写状态但是同一时间内只能处理一个客户端的读写操作实际上读写的业务并发为1。多客户端访问Server业务为串行执行大量请求会有排队延迟现象当Client3占据main thread流程时Client1,Client2流程卡在IO复用等待下次监听触发事件。 4 模型四-单线程多路IO复用多线程读写业务(业务工作池) 模型四是基于模型三的一种改进版但是赶紧的地方是在处理应用层消息业务本身将这部分承担的压力交给了一个Worker Pool工作池来处理。 1流程 1主线程main thread创建listenFd之后采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求I/O复用机制检测到ListenFd触发读事件则进行Accept建立连接并将新生成的connFd1加入到监听I/O集合中如图1.23所示。 图1.23 网络并发模型四单线程多路IO复用业务工作池-1 2当connFd1有可读消息触发读事件并且进行读写消息。 3main thread按照固定的协议读取消息并且交给worker pool工作线程池 工作线程池在server启动之前就已经开启固定数量的thread里面的线程只处理消息业务不进行套接字读写操作如图1.24所示。 图1.24 网络并发模型四单线程多路IO复用业务工作池-2 4工作池处理完业务触发connFd1写事件将回执客户端的消息通过main thead写给对方如图1.25所示。 图1.25 网络并发模型四单线程多路IO复用业务工作池-3 那么接下来Client2的读写请求的逻辑就是重复上述1-4的过程一般我们把这种基于消息事件的业务层处理的线程称之为业务工作池如图1.26所示。 图1.26 网络并发模型四单线程多路IO复用业务工作池-4 以上是模型四的服务端整体执行逻辑我们来分析一下模型一的优缺点 2优点 对于模型三, 将业务处理部分通过工作池分离出来减少多客户端访问Server业务为串行执行大量请求会有排队延迟时间。实际上读写的业务并发为1但是业务流程并发为worker pool线程数量加快了业务处理并行效率。 3缺点 读写依然为main thread单独处理最高读写并行通道依然为1。虽然多个worker线程处理业务但是最后返回给客户端依旧需要排队因为出口还是main thread的Read Write。 5 模型五单线程IO复用多线程IO复用(链接线程池) 模型五是单线程IO复用机制上再加上多线程的IO复用机制看上去很繁琐但是这种模型确实当下最通用和高效的解决方案。 1流程 1Server在启动监听之前开辟固定数量(N)的线程用Thead Pool线程池管理如图1.27所示。 图1.27 网络并发模型五单线程多路IO复用多线程IO复用-1 2主线程main thread创建listenFd之后采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求I/O复用机制检测到ListenFd触发读事件则进行Accept建立连接并将新生成的connFd1分发给Thread Pool中的某个线程进行监听。 3 Thread Pool中的每个thread都启动多路I/O复用机制(select、epoll),用来监听main thread建立成功并且分发下来的socket套接字。 图1.28 网络并发模型五单线程多路IO复用多线程IO复用-2 4如图1.28所示 thread监听ConnFd1、ConnFd2, thread2监听ConnFd3,thread3监听ConnFd4. 当对应的ConnFd有读写事件对应的线程处理该套接字的读写及业务。 所以我们将这些固定承担epoll多路IO监控的线程集合称之为线程池如图1.29所示。 图1.29 网络并发模型五单线程多路IO复用多线程IO复用-3 以上是模型五的服务端整体执行逻辑我们来分析一下模型一的优缺点。 2优点 将main thread的单流程读写分散到多线程完成这样增加了同一时刻的读写并行通道并行通道数量N N为线程池Thread数量。server同时监听的ConnFd套接字数量几乎成倍增大之前的全部监控数量取决于main thread的多路I/O复用机制的最大限制 (select 默认为1024 epoll默认与内存大小相关约3~6w不等)所以理论单点Server最高响应并发数量为N*(3~6W)(N为线程池Thread数量建议与CPU核心成比例1:1)。如果良好的线程池数量和CPU核心数适配那么可以尝试CPU核心与Thread进行绑定从而降低CPU的切换频率提升每个Thread处理合理业务的效率降低CPU切换成本开销。 3缺点 虽然监听的并发数量提升但是最高读写并行通道依然为N而且多个身处同一个Thread的客户端会出现读写延迟现象实际上每个Thread的模型特征与模型三单线程多路IO复用一致。 6 模型五(进程版)单进程多路I/O复用多进程IO复用 模型五进程版和模型五的流程大致一样这里的区别是由线程池更编程进程池如图1.30所示。 图1.30 网络并发模型五-进程池版本-1 在模型五进程版要注意的是进程之间的资源都是独立的所以当有客户端如Client1建立请求的时候main process主进程的IO复用会监听到ListenFd的可读事件如果在线程模型中可以直接Accept将链接创建并且将新创建的ConnFd交给线程中的某个线程中的IO复用机制来监控因为线程与线程中资源是共享的。但是在多进程中则不能这么做。Main Process如果进行Accept得到的ConnFd并不能传递给子进程因为他们都有各自的文件描述符序列。所以在多进程版本主进程listenFd触发读事件应该由主进程发送信号告知子进程目前有新的链接可以建立最终应该由某个子进程来进行Accept完成链接建立过程同时得到与客户端通信的套接字ConnFd。最终在用自己的多路IO复用机制来监听当前进程创建的ConnFd。 图1.31 网络并发模型五-进程池版本-2 如图1.31所示进程版与“模型五-单线程IO复用多线程IO复用链接线程池”无大差异。 1不同 1进程和线程的内存布局不同导致main process主进程不再进行Accept操作而是将Accept过程分散到各个子进程process)中。 2进程的特性资源独立所以main process如果Accept成功的fd其他进程无法共享资源所以需要各子进程自行Accept创建链接。 3main process只是监听ListenFd状态一旦触发读事件有新连接请求。通过一些IPC(进程间通信如信号、共享内存、管道)等, 让各自子进程Process竞争Accept完成链接建立并各自监听。 2优缺点 与五、单线程IO复用多线程IO复用(链接线程池)无大差异。多进程内存资源空间占用稍微大一些多进程模型安全稳定型较强这也是因为各自进程互不干扰的特点导致。 7 模型六单线程多路I/O复用多线程I/O复用多线程 本小节介绍一个更加复杂的模型六我们在基于模型五上再加上一个多线程处理读写服务端逻辑如下。 1流程 1Server在启动监听之前开辟固定数量(N)的线程用Thead Pool线程池管理如图1.32所示。 图1.32 网络并发模型六-1 2主线程main thread创建listenFd之后采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求I/O复用机制检测到ListenFd触发读事件则进行Accept建立连接并将新生成的connFd1分发给Thread Pool中的某个线程进行监听如图1.33所示。 图10.33 网络并发模型六-2 3Thread Pool中的每个thread都启动多路I/O复用机制(select、epoll),用来监听main thread建立成功并且分发下来的socket套接字。一旦其中某个被监听的客户端套接字触发I/O读写事件,那么会立刻开辟一个新线程来处理I/O读写业务如图1.34所示。 图1.34 网络并发模型六-3 4但某个读写线程完成当前读写业务如果当前套接字没有被关闭那么将当前客户端套接字如:ConnFd3重新加回线程池的监控线程中同时自身线程自我销毁。 以上是模型六的处理逻辑我们来分析一下他的优缺点 2优点 在模型五、单线程IO复用多线程IO复用(链接线程池)基础上除了能够保证同时响应的最高并发数又能解决读写并行通道局限的问题。同一时刻的读写并行通道达到最大化极限一个客户端可以对应一个单独执行流程处理读写业务读写并行通道与客户端数量1:1关系如图1.35所示。 图1.35 网络并发模型六-4 3缺点 该模型过于理想化因为要求CPU核心数量足够大。如果硬件CPU数量可数(目前的硬件情况)那么该模型将造成大量的CPU切换成本浪费。因为为了保证读写并行通道与客户端1:1的关系那么Server需要开辟的Thread数量就与客户端一致那么线程池中做多路I/O复用的监听线程池绑定CPU数量将变得毫无意义。如果每个临时的读写Thread都能够绑定一个单独的CPU那么此模型将是最优模型。但是目前CPU的数量无法与客户端的数量达到一个量级目前甚至差的不是几个量级的事。 七、小结 本章我们首先介绍了多路IO复用机制解决的问题以及Epoll的常用接口和基本的结构分析那么在基于IO复用机制的理论基础上我们整理了七种Server服务器并发处理结构模型每个模型都有各自的特点和优势那么对于多少应付高并发和高CPU利用率的模型目前多数采用的是模型五(或模型五进程版如Nginx就是类似模型五进程版的改版)。 至于并发模型并非设计的越复杂越好也不是线程开辟得越多越好我们要考虑硬件的利用与和切换成本的开销。模型六设计就极为复杂线程较多但以当今的硬件能力无法支撑反倒导致该模型性能极差。所以对于不同的业务场景也要选择适合的模型构建并不是一定固定就要使用某个来应用。
- 上一篇: 网站搭建价格表html5个人网页制作代码
- 下一篇: 网站搭建平台有哪些网站建设 化工
相关文章
-
网站搭建价格表html5个人网页制作代码
网站搭建价格表html5个人网页制作代码
- 技术栈
- 2026年04月20日
-
网站搭建环境h5说 网站
网站搭建环境h5说 网站
- 技术栈
- 2026年04月20日
-
网站搭建和网页设计友点cms
网站搭建和网页设计友点cms
- 技术栈
- 2026年04月20日
-
网站搭建平台有哪些网站建设 化工
网站搭建平台有哪些网站建设 化工
- 技术栈
- 2026年04月20日
-
网站搭建平台有哪些下载安装百度
网站搭建平台有哪些下载安装百度
- 技术栈
- 2026年04月20日
-
网站搭建设计是什么意思怎么做网站管理系统
网站搭建设计是什么意思怎么做网站管理系统
- 技术栈
- 2026年04月20日






