> 文章列表 > 多路I/O转接服务器

多路I/O转接服务器

多路I/O转接服务器

 总览概述

        多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件

        主要使用的方法有三种:select        poll        epoll    (三者通信原理一样)   

select 多路IO转接

原理: 借助内核, select 来监听, 客户端连接、数据通信事件

select工作原理:

        传入要监听的文件描述符集合(可读、可写或异常)开始监听,select处于阻塞状态,当有事件发生或设置的等待时间timeout到了就会返回返回之前自动去除集合中无事件发生的文件描述符返回时传出有事件发生的文件描述符集合。但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符,需要用户后续进行遍历操作

多路I/O转接原理分析一https://www.bilibili.com/video/BV1iJ411S7UA?p=52&spm_id_from=pageDriver&vd_source=d239c7cf48aa4f74eccfa736c3122e65多路IO转接原理分析二https://www.bilibili.com/video/BV1iJ411S7UA/?p=56&spm_id_from=pageDriver&vd_source=d239c7cf48aa4f74eccfa736c3122e65

select 函数原型

函数作用:select 就是监听 文件描述符 是否有读事件
        监听等待参数集合中的两类文件描述符产生动静,产生动静后集合中只会剩下产生动静的文件描述符

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
  • nfds:监听的所有文件描述符中,最大文件描述符+1
  • readfds: 读 文件描述符监听集合。    传入、传出参数
  • writefds:写 文件描述符监听集合。    传入、传出参数        NULL
  • exceptfds:异常 文件描述符监听集合    传入、传出参数        NULL
  • timeout: > 0:     设置监听超时时长。

                        NULL:    阻塞监听

                        0:    非阻塞监听,轮询

  • 返回值:

            > 0:   所有监听集合(3个)中, 满足对应事件的总数。

            0:      没有满足监听条件的文件描述符

            -1:     errno

从 select函数返回后,内核告诉我们一下信息:

  • 对我们的要求已经做好准备的描述符的个数
  • 对于三种条件哪些描述符已经做好准备.(读,写,异常)

有了这些返回信息,我们可以调用合适的I/O函数(通常是 read 或 write),并且这些函数不会再阻塞

select函数配套使用的四个宏 

void FD_ZERO(fd_set *set);	--- 清空一个文件描述符集合。fd_set rset;FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set);	--- 将待监听的文件描述符,添加到监听集合中FD_SET(3, &rset);	FD_SET(5, &rset);	FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set);	--- 将一个文件描述符从监听集合中 移除。FD_CLR(4, &rset);
int  FD_ISSET(int fd, fd_set *set);	--- 判断一个文件描述符是否在监听集合中。返回值: 在:1;不在:0;FD_ISSET(4, &rset);

 select实现多路IO转接原理分析

select实现多路IO转接原理分析https://www.bilibili.com/video/BV1iJ411S7UA/?p=57&spm_id_from=pageDriver&vd_source=d239c7cf48aa4f74eccfa736c3122e65

Resize icon

select使用经典案例 

        用select函数编写一个简单的高并发服务器,且假设服务器启动时处于无连接状态,满足以下功能:
        a)可处理来自一个新客户端的连接请求;
        b)监听可读事件,若已连接客户端的已连接描述符发生可读事件,服务器从客户端读取数据并处理;

select实现多路I/O转接

/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>#include "wrap.h"#define SERV_PORT 6666int main(int argc, char *argv[])
{int listenfd, connfd;               // connect fdchar buf[BUFSIZ];         /* #define INET_ADDRSTRLEN 16 */struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;listenfd = Socket(AF_INET, SOCK_STREAM, 0);  int opt = 1;                // 设置端口复用setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family= AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port= htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);fd_set rset, allset;                     // 定义 读集合,备份集合allsetint ret, maxfd = 0, n, i, j;maxfd = listenfd;                       // 最大文件描述符FD_ZERO(&allset);                       // 清空 监听集合FD_SET(listenfd, &allset);              // 将待监听fd添加到监听集合中while (1) {rset = allset;                      // 备份ret = select(maxfd+1, &rset, NULL, NULL, NULL);         // 使用select 监听if (ret < 0) {perr_exit("select error");}// 1、select监听listenfd  是否有客户端的连接请求if (FD_ISSET(listenfd, &rset)) {                        // listenfd 满足监听的 读事件clie_addr_len = sizeof(clie_addr) ;connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);   // 建立链接  --- 不会阻塞FD_SET(connfd, &allset);                            // 将新产生的fd,添加到监听集合中, 监听数据读事件if (maxfd < connfd)                 // 修改maxfdmaxfd = connfd;if (ret == 1)                   // 说明select 只返回一个,并且是listenfd, 后续执行无须执行.continue;}for (i = listenfd+1; i <= maxfd; i++) {         // 处理满足读事件的 fd // 2、select监听connfd  客户端是否有动静if (FD_ISSET(i, &rset)) {                   // 找到满足读事件的那个 fdn = Read(i, buf, sizeof(buf));if (n == 0) {                           // 检测到客户端已经关闭链接.Close(i);                           FD_CLR(i, &allset);                 // 将关闭的fd, 移除出监听集合.} else if (n == -1) {perr_exit("read error");}for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);write(i, buf, n);write(STDOUT_FILENO, buf, n);}}}Close(listenfd);return 0;
}

select优缺点

优点:

  • select的可移植性较好,可以跨平台;win、linux、macOS、Unix、类Unix、mips
  • select可设置的监听时间timeout精度更好,可精确到微秒,而poll为毫秒。

缺点:

  • 监听上限受文件描述符限制。 最大 1024;检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
  • select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;
  • select返回的就绪文件描述符集合,需要用户循环遍历所监听的所有文件描述符是否在该集合中,当监听描述符数量很大时效率较低。

select案例改进(自定义数组)

select自定义数组改进经典案例

select 监听的文件描述符 比较散乱且数量较少 则效率较低 考虑改进

/* server */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>#include "wrap.h"#define SERV_PORT 6666int main(int argc, char *argv[])
{int i, j, n, maxi;int nready, client[FD_SETSIZE];                 /* 自定义数组client, 防止遍历1024个文件描述符  FD_SETSIZE默认为1024 */int maxfd, listenfd, connfd, sockfd;char buf[BUFSIZ], str[INET_ADDRSTRLEN];         /* #define INET_ADDRSTRLEN 16 */struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;fd_set rset, allset;                            /* rset 读事件文件描述符集合 allset用来暂存 */listenfd = Socket(AF_INET, SOCK_STREAM, 0);int opt = 1;                                    /* 设置端口复用 */setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family= AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port= htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);maxfd = listenfd;                                           /* 起初 listenfd 即为最大文件描述符 */maxi = -1;                                                  /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1;                                         /* 用-1初始化client[] */FD_ZERO(&allset);FD_SET(listenfd, &allset);                                  /* 构造select监控文件描述符集 */while (1) {   rset = allset;                                          /* 每次循环时都从新设置select监控信号集 */nready = select(maxfd+1, &rset, NULL, NULL, NULL);  //2  1--lfd  1--connfdif (nready < 0)perr_exit("select error");if (FD_ISSET(listenfd, &rset)) {                        /* 说明有新的客户端链接请求 */clie_addr_len = sizeof(clie_addr);connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       /* Accept 不会阻塞 */printf("received from %s at PORT %d\\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) {                            /* 找client[]中没有使用的位置 */client[i] = connfd;                         /* 保存accept返回的文件描述符到client[]里 */break;}if (i == FD_SETSIZE) {                              /* 达到select能监控的文件个数上限 1024 */fputs("too many clients\\n", stderr);exit(1);}FD_SET(connfd, &allset);                            /* 向监控文件描述符集合allset添加新的文件描述符connfd */if (connfd > maxfd)maxfd = connfd;                                 /* select第一个参数需要 */if (i > maxi)maxi = i;                                       /* 保证maxi存的总是client[]最后一个元素下标 */if (--nready == 0)continue;} for (i = 0; i <= maxi; i++) {                               /* 检测哪个clients 有数据就绪 */if ((sockfd = client[i]) < 0)continue;if (FD_ISSET(sockfd, &rset)) {if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {    /* 当client关闭链接时,服务器端也关闭对应链接 */Close(sockfd);FD_CLR(sockfd, &allset);                        /* 解除select对此文件描述符的监控 */client[i] = -1;} else if (n > 0) {for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Write(sockfd, buf, n);Write(STDOUT_FILENO, buf, n);}if (--nready == 0)break;                                          /* 跳出for, 但还在while中 */}}}Close(listenfd);return 0;
}