I/O多路复用之select

select() 是一个系统调用,用于在一组文件描述符上等待 I/O 事件的发生。在 C++ 中,可以通过 select() 函数来实现网络编程中的 I/O 多路复用,以实现同时监听多个网络套接字上的事件,避免长时间的阻塞等待,提高程序的效率和可靠性。
select() 函数的原型如下:

#include 

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

其中,各个参数的含义如下:

nfds:需要监听的文件描述符集合的最大文件描述符值加1,即需要监听的最大文件描述符数目;
readfds、writefds、exceptfds:三个文件描述符集合分别用于监听需要读取、写入和异常条件发生的文件描述符。这三个文件描述符集合是指向 fd_set 结构体的指针,其中 fd_set 是一个位向量类型的结构体,用于表示所有文件描述符的集合。在使用 select() 函数前,必须使用 FD_ZERO() 宏定义将文件描述符集合初始化为一个空集,然后使用 FD_SET() 宏定义将需要监听的文件描述符加入集合中;
timeout:等待事件的超时时间,如果超过这个时间仍然没有任何事件发生,则 select() 函数将返回 0,表示超时。timeout 参数是一个 timeval 结构体类型的指针,包含两个成员:tv_sec 表示等待时间的秒数,tv_usec 表示等待时间的微秒数。

select() 函数返回时,可以通过检查传递给它的三个文件描述符集合来确定哪些文件描述符发生了事件,以及是什么类型的事件。对于 readfds、writefds 和 exceptfds 参数,如果一个套接字发生了对应事件,那么对应的文件描述符集合中的相应位将被置为 1。
以下是一个简单的示例程序,演示了如何使用 select() 函数等待多个套接字上的事件:

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
  // 创建两个 TCP 套接字
  int socket1 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  int socket2 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

  // 监听地址和端口
  struct sockaddr_in addr1 = {0};
  addr1.sin_family = AF_INET;
  addr1.sin_port = htons(8888);
  inet_pton(AF_INET, "0.0.0.0", &addr1.sin_addr);
  bind(socket1, (struct sockaddr*)&addr1, sizeof(addr1));

  struct sockaddr_in addr2 = {0};
  addr2.sin_family = AF_INET;
  addr2.sin_port = htons(8889);
  inet_pton(AF_INET, "0.0.0.0", &addr2.sin_addr);
  bind(socket2, (struct sockaddr*)&addr2, sizeof(addr2));

  // 设置套接字为监听状态,等待连接
  listen(socket1, 10);
  listen(socket2, 10);

  // 创建文件描述符集合,将套接字加入集合
  fd_set read_fds;
  FD_ZERO(&read_fds);
  FD_SET(socket1, &read_fds);
  FD_SET(socket2, &read_fds);

  // 设定超时时间
  struct timeval timeout = {5, 0};

  // 等待事件的发生
  int res = select(std::max(socket1, socket2) + 1, &read_fds, nullptr, nullptr, &timeout);

  if (res == -1) {
    std::cerr << "select error: " << std::strerror(errno) << std::endl;
    return 1;
  }

  if (res == 0) {
    std::cout << "select timed out." << std::endl;
    return 0;
  }

  // 检查哪些套接字上发生了事件
  if (FD_ISSET(socket1, &read_fds)) {
    std::cout << "socket1 readable." << std::endl;
    // TODO: 处理 socket1 的 I/O 事件
  }

  if (FD_ISSET(socket2, &read_fds)) {
    std::cout << "socket2 readable." << std::endl;
    // TODO: 处理 socket2 的 I/O 事件
  }

  return 0;
}

服务端和客户端代码示例

server

#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 初始化服务端的监听端口。
int initserver(int port);

int main(int argc,char *argv[])
{
  if (argc != 2)
  {
    printf("usage: ./tcpselect port\n"); return -1;
  }

  // 初始化服务端用于监听的socket。
  int listensock = initserver(atoi(argv[1]));
  printf("listensock=%d\n",listensock);

  if (listensock < 0)
  {
    printf("initserver() failed.\n"); return -1;
  }

  fd_set readfdset;  // 读事件的集合,包括监听socket和客户端连接上来的socket。
  int maxfd;  // readfdset中socket的最大值。

  // 初始化结构体,把listensock添加到集合中。
  FD_ZERO(&readfdset);

  FD_SET(listensock,&readfdset);
  maxfd = listensock;

  while (1)
  {
    // 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。
    fd_set tmpfdset = readfdset;

    int infds = select(maxfd+1,&tmpfdset,NULL,NULL,NULL);
    // printf("select infds=%d\n",infds);

    // 返回失败。
    if (infds < 0)
    {
      printf("select() failed.\n"); perror("select()"); break;
    }

    // 超时,在本程序中,select函数最后一个参数为空,不存在超时的情况,但以下代码还是留着。
    if (infds == 0)
    {
      printf("select() timeout.\n"); continue;
    }

    // 检查有事情发生的socket,包括监听和客户端连接的socket。
    // 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。
    for (int eventfd=0; eventfd <= maxfd; eventfd++)
    {
      if (FD_ISSET(eventfd,&tmpfdset)<=0) continue;

      if (eventfd==listensock)
      { 
        // 如果发生事件的是listensock,表示有新的客户端连上来。
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
        if (clientsock < 0)
        {
          printf("accept() failed.\n"); continue;
        }

        printf ("client(socket=%d) connected ok.\n",clientsock);

        // 把新的客户端socket加入集合。
        FD_SET(clientsock,&readfdset);

        if (maxfd < clientsock) maxfd = clientsock;

        continue;
      }
      else
      {
        // 客户端有数据过来或客户端的socket连接被断开。
        char buffer[1024];
        memset(buffer,0,sizeof(buffer));

        // 读取客户端的数据。
        ssize_t isize=read(eventfd,buffer,sizeof(buffer));

        // 发生了错误或socket被对方关闭。
        if (isize <=0)
        {
          printf("client(eventfd=%d) disconnected.\n",eventfd);

          close(eventfd);  // 关闭客户端的socket。

          FD_CLR(eventfd,&readfdset);  // 从集合中移去客户端的socket。

          // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
          if (eventfd == maxfd)
          {
            for (int ii=maxfd;ii>0;ii--)
            {
              if (FD_ISSET(ii,&readfdset))
              {
                maxfd = ii; break;
              }
            }

            printf("maxfd=%d\n",maxfd);
          }

          continue;
        }

        printf("recv(eventfd=%d,size=%d):%s\n",eventfd,isize,buffer);

        // 把收到的报文发回给客户端。
        write(eventfd,buffer,strlen(buffer));
      }
    }
  }

  return 0;
}

// 初始化服务端的监听端口。
int initserver(int port)
{
  int sock = socket(AF_INET,SOCK_STREAM,0);
  if (sock < 0)
  {
    printf("socket() failed.\n"); return -1;
  }

  // Linux如下
  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);
  setsockopt(sock,SOL_SOCKET,SO_KEEPALIVE,&opt,len);

  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(port);

  if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
  {
    printf("bind() failed.\n"); close(sock); return -1;
  }

  if (listen(sock,5) != 0 )
  {
    printf("listen() failed.\n"); close(sock); return -1;
  }

  return sock;
}

client

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    printf("usage:./tcpclient ip port\n"); return -1;
  }

  int sockfd;
  struct sockaddr_in servaddr;
  char buf[1024];
 
  if ((sockfd=socket(AF_INET,SOCK_STREAM,0))<0) { printf("socket() failed.\n"); return -1; }
	
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family=AF_INET;
  servaddr.sin_port=htons(atoi(argv[2]));
  servaddr.sin_addr.s_addr=inet_addr(argv[1]);

  if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)
  {
    printf("connect(%s:%s) failed.\n",argv[1],argv[2]); close(sockfd);  return -1;
  }

  printf("connect ok.\n");

  for (int ii=0;ii<10000;ii++)
  {
    // 从命令行输入内容。
    memset(buf,0,sizeof(buf));
    printf("please input:"); scanf("%s",buf);
    // sprintf(buf,"1111111111111111111111ii=%08d",ii);

    if (write(sockfd,buf,strlen(buf)) <=0)
    { 
      printf("write() failed.\n");  close(sockfd);  return -1;
    }
		
    memset(buf,0,sizeof(buf));
    if (read(sockfd,buf,sizeof(buf)) <=0) 
    { 
      printf("read() failed.\n");  close(sockfd);  return -1;
    }

    printf("recv:%s\n",buf);

    // close(sockfd); break;
  }
} 

测试结果

在这里插入图片描述

你可能感兴趣的:(网络)