I/O多路复用之select

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

#include <sys/select.h>

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 <iostream>
#include <cstring>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>

// 初始化服务端的监听端口。
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

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;
  }
} 

测试结果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_WAWA鱼_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值