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

一、select头文件

#include 

二、select原型

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

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

入参:

  • 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 
//socket
#include 
#include 
//close
#include 
//exit
#include 
//perror
#include 
//memset
#include 
//htons
#include 
//select
#include 
/* According to earlier standards */
#include 



#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操作中,可能需要考虑其他更高效和功能更强大的替代方案。

你可能感兴趣的:(C/C++,服务器,tcp/ip,数据库)