多路io复用Select [补档-2023-07-16]

发布时间 2024-01-13 18:03:41作者: 小白同学_C

select

2.1 简介

​ select函数可以用于实现高效的多路复用 I/O,同时处理多个文件描述符的事件,包括监听可读、可写和异常条件,具有阻塞和非阻塞模式,并可以设置超时时间。这使得程序能够高效地处理并发任务,提高性能和响应性。

2.2 select函数

头文件:#include <sys/select>

函数原型:int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

函数参数:

nfds:监视文件描述符的最大值加一(文件描述符集合中所有的文件描述符的最大值+1)

readfds:指向一个文件描述符集合,用于监视是否有数据可读。

writefds:指向一个文件描述符集合,用于监视是否可以写入数据。

exceptfds:指向一个文件描述符集合,用于监视是否有异常情况。

timeout:指向一个struct timeval结构体,表示超时时间,即select函数最多等待的时间。

结构体struct timeval 的成员:

​ time_t tv_sec:表示秒数。

​ suseconds_t tv_usec:表示微妙数。

函数返回值

​ 成功返回准备就绪的文件描述符总数。

​ 监听超时则返回0。

​ 发生错误返回-1。

2.3 select的几个宏函数

2.3.1将指定的一个文件描述符从集合中清除

​ 函数原型:FD_CLR(int fd, fd_set *set);

​ 函数参数:fd为指定的文件描述符,set为指定的文件描述符集合

2.3.2 将指定的文件描述符集合清空,所有位置都置为0

​ 函数原型:FD_ZERO(fd_set *set);

​ 函数参数:set为指定的文件描述符集合

2.3.3 将指定的文件描述符加入到指定的集合中

​ 函数原型:FD_SET(int fd, fd_set *set);

​ 函数参数:fd为指定的文件描述符,set为指定的文件描述符集合

2.3.4 判断指定的文件描述符是否在某个文件描述符集合中

​ 函数原型:FD_ISSET(int fd, fd_set *set);

​ 函数参数:fd为指定的文件描述符,set为指定的文件描述符集合

2.4 使用select创建一个多路io复用的服务端:

​ 第一步:创建套接字。

​ 第二步:准备一些变量和常量,用于记录最大文件描述符,客户端发来的数据,循环次数,条件判断等待。

​ 第三步:初始化本地(服务器)地址结构体sockaddr_in。

​ 第四步:将套接字与本地地址绑定,并且建议判断一下是否绑定成功。

​ 第五步:切换为监听状态。

​ 第六步:进入一个无限循环然后持续监听。

​ 马上进入循环时初始化文件描述符集合,进入后将刚才监听的文件描述符添加到里面。

​ 在所有已连接的客户端文件描述符集合中寻找最大的文件描述符,然后记录下来。

​ 使用select函数对整个文件描述符集合进行监听,如果监听失败则退出程序,如果有文件描述符可操作, 则进行具体操作。

​ 当select监听有变化则检测监听的套接字是否有新的连接请求,如果有则使用accept接收连接,并且将accept返回的已经和客户端建立连接的描述符加入到已连接的客户端数组中。

​ 处理客户端的请求。遍历已连接的客户端数组,逐个判断每个客户端文件描述符数组中的元素是否中文件描述符集合中,如果在则代表有数据可读。

​ 从客户端接收数据,并且进行处理。如果无法接收数据则关闭客户端文件描述符然后将其从描述符集合中移除。否则就对客户端发来的数据进行相关操作。

​ 第七步:当循环结束后,记得关闭所有已连接的客户端文件描述符和服务器文件描述符。

2.5 使用select创建的多路io的服务端代码示例

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>

 

#define MAX_CLIENTS 100 //最大客户端描述符数
#define BUF_SIZE 128 //字符串数组的最大长度

 

int main()

{

  int client_fds[MAX_CLIENTS]; //用于存储客户端文件描述符,用来辅助文件描述符集使用

  int server_fd; //服务端文件描述符(用于监听)

  fd_set readfds; //可读文件描述符集

  char data[BUF_SIZE];//用于存储接收来自客户端的信息

  int maxfd; //最大文件描述符

  int i;//下面可能会用到循环,它用来循环

  int retval; //用于记录select的值

 

  //创建套接字,ipv4,tcp

  server_fd = socket(AF_INET, SOCK_STREAM, 0);

  if (server_fd == -1) {

​     printf("创建套接字失败!\n");

​     exit(-1);

  }

  //分别用于存储服务端和客户端的地址信息

  struct sockaddr_in server_addr, client_addr;

  //初始化本地地址信息

  server_addr.sin_family = AF_INET;

  server_addr.sin_port = htons(10066);

  server_sddr.sin_addr.s_addr = htonl(INADDR_ANY);

  

  //将套接字与本地服务器地址绑定

  int ret;//用于检测绑定或者切换至监听是否成功

  ret = bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

  if (ret == -1) {

​     printf("本地地址与套接字失败!\n");

​     exit(-1);

  }

 

  //切换监听

  ret = listen(server_fd, MAX_CLIENTS);

  if (ret == -1) {

​     printf("切换监听态失败!\n");

​     exit(-1);

  }

 

  printf("服务器开始监听,监听端口:10066 监听地址:任意\n");

 

  //因为接下来的select需要接收一个集合的最大描述符,先用一个已有的文件描述符给他赋值

  maxfd = server_fd;

 

  //初始化客户端文件描述符集合,-1代表可用

  for (i = 0; i < MAX_CLIENTS; i++) {

​     client_fds[i] = -1;

  }

 

  FD_ZERO(&readfds); //初始化可读文件描述符集

 

  while (1) {

​     FD_SET(server_fd, &readfds); //将当前监听的描述符加入到里面

 

​     //将已连接的文件描述符加入到集合中

​     for (i = 0; i < MAX_CLIENTS; i++) {

​       int client_fd = client_fds[i];

​       if (client_fd != -1) {

​         //不等于-1则代表在被使用中代表已连接

​         FD_SET(client_fd, &readfds);

​         if (client_fd > maxfd) {

​           //如果当前已连接的文件描述符比之前的最大描述符要大,记得交换

​           //因为select函数接收的是集合中值最大的

​           maxfd = client_fd; 

​         }

​       }

​     }

 

​     //使用select函数进程监听整个集合

​     retval = select(maxfd + 1, &readfds, NULL, NULL, NULL);

​     if (retval == -1) {

​       //等于-1代表监听失败

​       printf("监听失败!!!\n");

​       exit(-1);

​     }

​     else if (retval > 0) {

​       //大于0代表描述符集readfds有变化

​       //检测socket_fd是否有新的链接

​       if (FD_ISSET(server_fd, &readfds)) {

​         socklen_t client_len = sizeof(client_addr);

​         int client_fd = accept(server_fd,(struct sockaddr *)&client_addr,&client_len);

​         if (client_fd == -1) {

​           printf("接收链接失败!!!\n");

​         }

​         else {

​           //将刚才接收到的新的来自客户端的socket添加到结合中

​           for (i = 0; i < MAX_CLIENTS; i++) {

​             if (client_fds[i] == -1) {

​                //-1则代表当前数组位置可用

​                client_fds[i] = client_fd;

​                break;

​             }

​           }

 

​         }

​       }

 

​       //处理来自客户端的请求

​       for (i = 0; i < MAX_CLIENTS; i++) {

​         int client_fd = client_fds[i];

​         if (client_fd != -1 && FD_ISSET(client_fd, &readfds)) {

​           //首先数组元素是被使用的状态,并且要判断数组元素i对应的描述符是否中集合中

​           memset(data, 0, BUF_SIZE);

 

​           //接收信息

​           ssize_t n = recv(client_fd, data, sizeof(data),0);

​           if (n <= 0) {

​             printf("未能接收客户端数据!\n");

​             close(client_fd);//关闭文件描述符

​             client_fds[i] = -1;//记得将这个坑置为-1,因为咱们操作完了

​           }

​           else {

​             //回应客户端

​             printf("服务端已经接收到客户端数据:\n%s\n",data);

​             if (send(client_fd, data, sizeof(data), 0) == -1) {

​                printf("回送服务端数据失败!\n");

​             }

​           }

​         }

​       }

 

 

​     }

 

  }

 

  //用完了记得循环关闭链接

  for (i = 0; i < MAX_CLIENTS; i++) {

​     int client_fd = client_fds[i];

​     if (client_fd != 1) {

​       close(client_fd);

​     }

  }

 

  close(server_fd);

  return 0;

 

}

2.6 select的优缺点

select优点:

​ 1 一个进程可以支持多个客户端

​ 2 select支持跨平台

select缺点:

​ 1 代码编写困难

​ 2 会涉及到用户区到内核区的来回拷贝,销毁资源大。

​ 3 当客户端多个连接, 但少数活跃的情况, select效率较低

​ 例如: 作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低 下

4 最大支持1024个客户端连接

​ select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的, 而是由 FD_SETSIZE=1024限制的.

​ FD_SETSIZE=1024 fd_set使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做.