IO复用-select模型

277 阅读3分钟

select模型:

事件:select()等待事件的发生(读事件,写事件)
    1)新客户端的连接请求accept;
    2)客户端有报文到达recv,可以读;
    3)客户端连接已断开;
    4)可以向客户端发送报文send,可以写。

写事件:
    TCP有缓存区,如果缓冲区己填填满,send函数会阻塞
    如果发送端关闭了socket,缓冲区中的数据会继续发送给接收端
    可以用发送端发快一点,接收端收慢一点,会出现发送端停一会,发一会儿。
    如果tcp缓冲区没有满,那么socket连接是可写的(一般情况是填不满的,所以如果关心可写事件,select会立即返回)
    tcp发送缓存区2.5M, 接收缓存区1M
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen); //获取发送缓存区的大小
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen); //获取接收缓存区的大小
    在高并发和流媒休传输场景中,缓冲区有填满的可能
    百度查询TCP的缓存区

超时机制
    第5个参数,超时时间。
        >0表示超过多久没有事件发生,则返回=0
        NULL,表示不设置超时,直到有事件发生才返回

水平触发
    如果事件和数据已经在缓存冲区里,程序调用select()时会报告事件,数据也不会丢失。
    服务端select()之前先sleep 20秒,客户端发送完数据后退出,
    服务端sleep完后,调用select(),都可以接收到客户端连接请求,发送数据,以及断开链接的事件,一个都没有丢失
    
    如果select()己经报告了事件,但是程序没有处理它,下次调用select()的时个会重接报告。   

性能测试
    1000000/s个报文

存在的问题
    支持的连接数太小,才1024, 调整的意义不大
    每次调用 select(), 要把fdset从用户态拷贝到内核, 调用select()之后,把fdset从内核态拷贝到用户态
    select()返回后,需要遍历bitmap, 效率比较低
    
    
/*
 * 程序名:tcpselect.cpp,此程序用于演示采用select模型的使用方法。
 * 
*/
#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 readfds;        // 读事件socket的集合,包括监听socket和客户端连接上来的socket。
  FD_ZERO(&readfds);     // 初始化读事件socket的集合。
  FD_SET(listensock,&readfds); // 把listensock添加到读事件socket的集合中。

  int maxfd=listensock;        // 记录集合中socket的最大值。

  while (true)
  {
    // 事件:1)新客户端的连接请求accept;2)客户端有报文到达recv,可以读;3)客户端连接已断开;
    //       4)可以向客户端发送报文send,可以写。
    // 可读事件  可写事件
    // select() 等待事件的发生(监视哪些socket发生了事件)。

    fd_set tmpfds=readfds;
    fd_set tmpfds1=readfds;   //用于监听可写事件
    struct timeval timeout;    timeout.tv_sec=10; timeout.tv_usec=0;
    int infds=select(maxfd+1,&tmpfds,&tmpfds1,NULL,&timeout); 

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

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

/*  因为缓存区未满,一直可写,会一直打印输出,所以在这里监视可写事件没有意义,在马上要往fd发送数据时
    for (int eventfd=0;eventfd<=maxfd;eventfd++)
    {
      if(FD_ISSET(eventfd, &tmpfds1)<=0) continue;    //如果没有可写事件,continue
      printf("eventfd=%d\n", eventfd);      //这里会一直打印,因为缓存区未满,一直可写。
    }
*/

    // 如果infds>0,表示有事件发生的socket的数量。
    for (int eventfd=0;eventfd<=maxfd;eventfd++)
    {
      if (FD_ISSET(eventfd,&tmpfds)<=0) continue;   // 如果没有事件,continue

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

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

        // 把新客户端的socket加入可读socket的集合。
        FD_SET(clientsock,&readfds);
        if (maxfd<clientsock) maxfd=clientsock;    // 更新maxfd的值。
      }
      else
      {
        // 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。

        char buffer[1024]; // 存放从客户端读取的数据。
        memset(buffer,0,sizeof(buffer));
        if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
        {
          // 如果客户端的连接已断开。
          printf("client(eventfd=%d) disconnected.\n",eventfd);
          close(eventfd);            // 关闭客户端的socket
          FD_CLR(eventfd,&readfds);  // 把已关闭客户端的socket从可读socket的集合中删除。
          
          // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
          if (eventfd == maxfd)
          {
            for (int ii=maxfd;ii>0;ii--)  // 从后面往前找。
            {
              if (FD_ISSET(ii,&readfds))
              {
                maxfd = ii; break;
              }
            }
          }
        }
        else
        {
          // 如果客户端有报文发过来。
          printf("recv(eventfd=%d):%s\n",eventfd,buffer);
          // 把接收到的报文内容原封不动的发回去。
          // tcp缓存满了send也会阻塞,所以在这里判断是否可写事件(写事件),只在发送是关注
          fd_set tmpfds;
          FD_ZERO(&tmpfds);
          FD_SET(eventfd,&tmpfds);
          if (select(eventfd+1,NULL,&tmpfds,NULL,NULL)<=0)
            perror("select() failed");
          else
            send(eventfd,buffer,strlen(buffer),0);
        }
      }
    }
  }

  return 0;
}

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

  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&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 )
  {
    perror("bind() failed"); close(sock); return -1;
  }

  if (listen(sock,5) != 0 )
  {
    perror("listen() failed"); close(sock); return -1;
  }

  return sock;
}