服务器搭建(TCP套接字)-select版(服务端)

2023-09-19 15:56:25

一、select头文件

#include <sys/select.h>

二、select原型

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

    select() 是一个系统调用函数,用于在多个文件描述符上进行 I/O 多路复用。通过 select() 函数,可以监视多个文件描述符的状态,以确定是否有读写事件准备就绪

多路复用:

  • “多路”指的是多个来源或通路。它表示可以同时处理来自多个不同源的 I/O 事件
  • "复用"指的是复用一个线程或进程来同时处理多个 I/O 事件。它允许在单个线程或进程中监视和处理多个文件描述符的读写操作,而不需要为每个事件创建一个独立的线程或进程

入参:

  • nfds:要检查的最大文件描述符值加 1。它表示 select 函数需要扫描的文件描述符范围,即从 0 到 nfds-1 的文件描述符将被监视。
  • readfds:用于检查可读性的文件描述符集合。
  • writefds:用于检查可写性的文件描述符集合。
  • exceptfds:用于检查异常条件的文件描述符集合。
  • timeout:超时时间,指定 select() 调用的最长等待时间。

select() 函数会阻塞当前进程,直到满足以下条件之一:

  • 有一个或多个文件描述符准备好进行读操作。
  • 有一个或多个文件描述符准备好进行写操作。
  • 发生了异常情况,如带外数据到达。

返回值:

  • 如果返回值大于 0:表示有文件描述符就绪,且返回值是就绪文件描述符的总数。
  • 如果返回值等于 0:表示超时,即在指定的超时时间内没有文件描述符就绪。
  • 如果返回值等于 -1:表示出现错误,可以通过查看 errno 变量来获取具体的错误信息。

2.1、fd_set

    fd_set 是一个数据结构,用于表示文件描述符的集合。它是一个位图,每个文件描述符在 fd_set 中占据一个位,用于标识该文件描述符的状态。

typedef struct fd_set {
    unsigned int fd_count;      // 文件描述符的数量
    int fd_array[FD_SETSIZE];   // 文件描述符数组
} fd_set;

其中,fd_count 表示文件描述符的数量,fd_array 是一个数组,用于存储文件描述符的值。

fd_set 数据结构是一个固定大小的数组,其大小由宏 FD_SETSIZE 定义。在大多数系统中,FD_SETSIZE 的默认值是 1024,因此 fd_set 可以容纳的文件描述符数量通常是有限的。如果需要监听更多的文件描述符,可能需要对 FD_SETSIZE 进行修改或使用其他更高效的多路复用机制。

fd_set 提供了一些宏函数来操作文件描述符集合,常用的宏函数有:

  • FD_ZERO(fd_set *set):将文件描述符集合中的所有位清零

  • FD_SET(int fd, fd_set *set):将指定的文件描述符加入文件描述符集合。

  • FD_CLR(int fd, fd_set *set):从文件描述符集合中移除指定的文件描述符。

  • FD_ISSET(int fd, fd_set *set)检查指定的文件描述符是否在文件描述符集合中。

2.2、timeval

timeval 是一个结构体,用于表示时间值(time value)。

struct timeval {
    long tv_sec;        // 秒数
    long tv_usec;       // 微秒数
};

这个参数有以下三种可能:

  • 永远等待
    仅在一个描述符准备好I/O时才返回,为此,我们可以把该参数置为空指针
  • 等待一段固定时间
    在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数
  • 根本不等待
    检查描述符后立即返回,这称为"轮询"。该参数必须指向一个timeval结构,并且其中的定时器值(秒数和微秒数)必须为0.

三、代码实现

服务端使用select函数的基本流程:

1、创建并初始化套接字:服务端需要创建一个监听套接字,用于接受客户端的连接请求。同时,需要将监听套接字添加到select监视的文件描述符集中。

2、设置文件描述符集:在使用select之前,需要将所有需要监视的文件描述符(包括监听套接字和已连接的客户端套接字)添加到文件描述符集中。可以使用FD_SET宏将套接字添加到集合中。

3、调用select函数:使用select函数开始监听文件描述符集中的事件。select函数会阻塞程序执行,直到集合中的文件描述符有可读、可写或异常事件发生。

4、处理就绪的文件描述符:当select函数返回后,需要遍历文件描述符集,确定哪些套接字上发生了事件。可以使用FD_ISSET宏来检查文件描述符是否准备就绪。

5、处理连接请求:如果监听套接字(通常是服务器套接字)准备就绪,表示有新的客户端连接请求。此时,可以调用accept函数接受客户端的连接,并将其加入到文件描述符集中进行监视。

6、处理客户端数据:如果已连接的客户端套接字准备就绪,表示有数据可读或可写。此时,可以使用recv函数读取客户端发送的数据,或使用send函数向客户端发送数据。

7、循环监听:在处理完所有就绪的文件描述符后,可以再次调用select函数,继续监听新的事件。可以使用循环来反复执行这个过程,以实现持续的事件驱动。

#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
//select
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>



#define PORT 8596
#define MESSAGE_SIZE 1024
#define FD_SIZE 1024

int main(){

  int ret=-1;
  int socket_fd=-1;
  int accept_fd=-1;
  int accept_fds[FD_SIZE]={-1,};
  
  //可用fd索引
  int canUseFDIndex=-1;
  //最大的fd索引
  int maxFDIndex=0;
  int max_fd=-1;
  fd_set fd_sets;
  int ready=0;
  int backlog=10;
  int flags=1;

  struct sockaddr_in local_addr,remote_addr;

  //create socket
  socket_fd=socket(AF_INET,SOCK_STREAM,0);
  if(socket_fd == -1){
    perror("create socket error");
    exit(1);
  }
 //set option of socket
  ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
  if ( ret == -1 ){
    perror("setsockopt error");
  }

 //set socket address
  local_addr.sin_family=AF_INET;
  local_addr.sin_port=htons(PORT);
  local_addr.sin_addr.s_addr=INADDR_ANY;
  bzero(&(local_addr.sin_zero),8);
 //bind socket
 ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
 if(ret == -1){
    perror("bind socket error");
    exit(1);
 }

  ret=listen(socket_fd, backlog);
  if(ret ==-1){
   perror("listen error");
   exit(1);
  }
  //重置max_fd;
  max_fd=socket_fd;
  for(int i=0;i < FD_SIZE;i++){
     accept_fds[i]=-1;
  }
  //loop to accept client
  for(;;){
   //清空
   FD_ZERO(&fd_sets);
   //socket_fd加入集合
   FD_SET(socket_fd,&fd_sets);
   //同步集合中最大的文件描述符
   for(int j=0;j<maxFDIndex;j++){
     if(accept_fds[j] !=-1){
      if(accept_fds[j] > max_fd){
          max_fd=accept_fds[j];
      }
      //重新加入需要监听的文件描述符到集合里
      FD_SET(accept_fds[j],&fd_sets);
     }
   }
   struct timeval timeout;
   timeout.tv_sec = 5;  // 设置超时时间为 5 秒
   timeout.tv_usec = 0;
   ready=select(max_fd+1,&fd_sets,nullptr,nullptr,timeout);
   if(ready<0){
      perror("error in select");
      break;
   }else if(ready==0){
      perror("select time out!");
      continue;
   }else if(ready){
     printf("ready:%d\n",ready);
     //socket有新的连接请求
     if(FD_ISSET(socket_fd,&fd_sets)){
       //找到没有使用的位置
       int k=0;
       for(;k<FD_SIZE;k++){
         if(accept_fds[k] == -1){
           canUseFDIndex=k;
           break;
         }
       }
       if(k==FD_SIZE){
         perror("the connected is full!\n");
         continue;
       }

      socklen_t addrlen = sizeof(remote_addr);
      accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);

      accept_fds[canUseFDIndex]=accept_fd;
      if(canUseFDIndex+1 >maxFDIndex){
         maxFDIndex=canUseFDIndex+1;
      }
      //同步最大文件描述符
      if(accept_fd > max_fd){
         max_fd=accept_fd;
      }
     }
     for(int p=0;p<maxFDIndex;p++){
       if(accept_fds[p] !=-1 && FD_ISSET(accept_fds[p],&fd_sets)){
         char in_buf[MESSAGE_SIZE]={0,};
         memset(in_buf,0,MESSAGE_SIZE);
         //read data
         int ret =recv(accept_fds[p], (void*)in_buf, MESSAGE_SIZE, 0);
         if(ret ==0){
            close(accept_fds[p]);
            accept_fds[p]=-1;
            break;
        }
        printf("receive data:%s\n",in_buf);
        send(accept_fds[p], (void *)in_buf, MESSAGE_SIZE, 0);
       }
     }
   }
  }
  printf("quit server....");
  close(socket_fd);
  return 0;
}

四、总结

select是一种用于多路复用(multiplexing)的系统调用,常用于实现异步I/O操作。它在编程中具有一些优点和缺点:

优点:

  • 高效的事件驱动:select允许程序同时监视多个文件描述符(如套接字),并在其中任何一个文件描述符准备好进行I/O操作时通知程序。这种事件驱动的方式可以提高程序的效率,避免了不必要的忙等待。

  • 跨平台兼容性:select是标准的POSIX接口,因此在大多数主流操作系统上都有良好的支持。这使得可以使用相同的代码在不同的平台上进行开发,提高了可移植性。

  • 简单易用:select的接口相对简单,适用于处理少量的文件描述符。它使用简洁的参数和返回值,易于理解和使用。对于简单的I/O多路复用需求,select是一个较为直观的选择。

缺点:

  • 低效的扩展性:select的一个主要缺点是其在处理大量文件描述符时的低效性。它采用线性扫描的方式遍历所有待监视的文件描述符,当文件描述符数量较大时,性能会明显下降

  • 需要维护文件描述符集:使用select需要维护一个文件描述符集,包含所有要监视的文件描述符。这要求开发人员在程序中维护一个数据结构来管理这些文件描述符,增加了一定的复杂性。

  • 不支持高级特性:相比其他更高级的I/O多路复用机制(如epoll、kqueue等),select的功能相对有限。它不支持一些高级特性,如边缘触发(edge-triggered)模式和自动扩展等。

总体而言,select是一种简单易用且可移植的多路复用机制,适用于处理少量文件描述符的情况。然而,在高并发和大规模的I/O操作中,可能需要考虑其他更高效和功能更强大的替代方案。

更多推荐

延长周末,获得高质量休息:工作与学习党的生活策略

🌷🍁博主猫头虎带您GotoNewWorld.✨🍁🦄博客首页——猫头虎的博客🎐🐳《面试题大全专栏》文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺🌊《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐🌊《100天精通Golang(基础入门篇)》学会Golang语言,畅玩云原生,走遍大

开源库源码分析:Okhttp源码分析(一)

开源库源码分析:OkHttp源码分析导言接下来就要开始分析一些常用开源库的源码了,作为最常用的网络请求库,OkHttp以其强大的功能深受Android开发者的喜爱(比如说我),还有对该库进行二次封装而成的热门库,比如说Retrofit。本文我们将从源码入手看看OkHttp是如何运作的。注意本文解析的是OkHttp3库,

LVS负载均衡集群

一、集群含义:由多台主机构成,但对外只表现为一一个整体,只提供一个访问入口(域名或IP地址),相当于一台大型计算机。二、群集的类型:1)负载均衡群集LB:提高系统响应效率,处理更多的访问请求,减少延迟,实现高并发、高负载的能力典型代表:软件类:LVSNginxHAProxy等硬件类:F5绿盟2)高可用群集HA:提高系统

docker 存储挂载比较

docker存储概述接触docker的朋友都知道,docker镜像是以layer概念存在的,一层一层的叠加,最终成为我们需要的镜像。但该镜像的每一层都是ReadOnly只读的。只有在我们运行容器的时候才会创建读写层。文件系统的隔离使得:容器不再运行时,数据将不会持续存在,数据很难从容器中取出。无法在不同主机之间很好的进

QT基础教程(QPalette和QIcon)

文章目录前言一、QPalette类二、QIcon类三、QPalette和QIcon之间的转换总结前言本篇文章继续讲解QT中的知识,主要为大家讲解QPalette和QIcon。QPalette和QIcon都是Qt框架中用于图形界面设计的类,它们分别用于管理调色板和图标的相关功能。一、QPalette类QPalette(调

MySQL中的表与视图:解密数据库世界的基石

🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年6月CSDN上海赛道top4。🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。🏆本文已收录于PHP专栏:MySQL的100个知识点。🎉欢迎👍点赞✍评论⭐收藏文章目录🚀一、前言🚀二、基

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)

1、链表的概念及结构概念:链表是一种物理存储结构上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。注意:​1、从上图可看出:链式结构在逻辑上是连续的,但是在物理上不一定连续。​2、现实中的结点一般都是从堆上申请出来的。​3、从堆上申请空间,是按照一定策略来分配的,再次申请的空间可能连续,不

k8s pod概念、分类及策略

目录一.pod相关概念2.Kubrenetes集群中Pod两种使用方式3.pause容器的Pod中的所有容器共享的资源4.kubernetes中的pause容器主要为每个容器提供功能:6.Pod分为两类:二.Pod容器的分类1.基础容器(infrastructurecontainer)(1)作用(2)配置2.初始化容器

【网络安全】黑客自学笔记

1️⃣前言🚀作为一个合格的网络安全工程师,应该做到攻守兼备,毕竟知己知彼,才能百战百胜。计算机各领域的知识水平决定你渗透水平的上限🚀【1】比如:你编程水平高,那你在代码审计的时候就会比别人强,写出的漏洞利用工具就会比别人的好用;【2】比如:你数据库知识水平高,那你在进行SQL注入攻击的时候,你就可以写出更多更好的S

大数据时代元数据的重要性

元数据,是描述了数据本身(如数据库、数据元素、数据模型),数据表示的概念(如业务流程、应用系统、软件代码、技术基础设施,数据与概念之间的联系。元数据可以帮助组织理解其自身的数据、系统和流程,同时帮助用户评估数据质量,对数据库与其他应用程序的管理来说是不可或缺的。它有助于处理、维护、集成、保护和治理其他数据。在大数据的海

spark 精华总结

面试题:Hadoop的基于进程的计算和Spark基于线程方式优缺点?答案:Hadoop中的MR中每个map/reducetask都是一个java进程方式运行,好处在于进程之间是互相独立的,每个task独享进程资源,没有互相干扰,监控方便,但是问题在于task之间不方便共享数据,执行效率比较低。比如多个maptask读取

热文推荐