select

1. select模型

在这里插入图片描述

2. select()函数

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

fd_set类型

readfds和writefds, exceptfds的类型都是fd_set,那么fd_set类型是什么呢?

  • fd_set类型本质是一个位图,位图的位置 表示
    相对应的文件描述符,内容表示该文件描述符是否有效,1代表该位置的文件描述符有效,0则表示该位置的文件描述符无效
  • 如果将文件描述符2,3设置位图当中,则位图表示的是为1100
  • fd_set的上限是1024个文件描述符

readfds

  • readfds是 等待读事件的文件描述符集合,.如果不关心读事件(缓冲区有数据),则可以传NULL值
  • 应用进程和内核都可以设置readfds,应用进程设置readfds是为了通知内核去等待readfds中的文件描述符的读事件.而
    内核设置readfds是为了告诉应用进程哪些读事件生效

在这里插入图片描述

writefds

与readfds类似,writefds是等待写事件(缓冲区中是否有空间)的集合,如果不关心写事件,则可以传值NULL

exceptfds

如果内核等待相应的文件描述符发生异常,则将失败的文件描述符设置进exceptfds中,如果不关心错误事件,可以传值NULL

timeout

设置select在内核中阻塞的时间,如果想要设置为非阻塞,则设置为NULL。如果想让select阻塞5秒,则将创建一个struct timeval time={5,0};

其中struct timeval的结构体类型是:

           struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };

返回值

  • 如果没有文件描述符就绪就返回0;
  • 如果调用失败返回-1;
  • 如果在timeout时间内,readfds中有事件发生,则返回timeout剩下的时间

3. select的工作流程

应用进程和内核都需要从readfdswritefds获取信息,其中,内核需要从readfds和writefds知道哪些文件描述符需要等待,应用进程需要从readfdswritefds中知道哪些文件描述符的事件就绪
在这里插入图片描述

select采用水平触发的方式,如果报告了fd后事件没有被处理或数据没有被全部读取,那么下次select时会再次报告该fd。

select 模型是一种多路复用 I/O 模型,它可以同时监视多个文件描述符,等待其中任意一个文件描述符上有事件发生,从而实现 I/O 多路复用。select 模型的原理如下:

调用 select 函数,将要监视的文件描述符集合传递给它。

select 函数将进程挂起,等待文件描述符集合中的任意一个文件描述符上有事件发生。

当文件描述符集合中的任意一个文件描述符上有事件发生时,select 函数返回,并将有事件发生的文件描述符集合返回给进程。

进程遍历有事件发生的文件描述符集合,对每个文件描述符进行相应的操作,如读、写等

4. 缺陷

  • 由于fd_set的上限是1024,所以select能等待的读事件的文件描述符和写事件的文件描述是有上限的,如果作为一个大型服务器,能够同时链接的客户端是远远不够的
  • 内核每一次等待文件描述符 都会重新扫描所有readfds或者writefds中的所有文件描述符,如果有较多的文件描述符,则会影响效率

5. 源码

client.cpp

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc,char *argv[])
{
  if (argc!=3)
  {
    printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n"); return -1;
  }

  // 第1步:创建客户端的socket。
  int sockfd;
  if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
 
  // 第2步:向服务器发起连接请求。
  struct hostent* h;
  if ( (h = gethostbyname(argv[1])) == 0 )   // 指定服务端的ip地址
  { printf("gethostbyname failed.\n"); close(sockfd); return -1; }
  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
  if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)  // 向服务端发起连接清求。
  { perror("connect"); close(sockfd); return -1; }

  char buffer[1024];
 
  // 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
  for (int ii=0; ii<5; ii++)
  {
    int iret;
    memset(buffer, 0, sizeof(buffer));
    printf("Please input:");
    scanf("%s", buffer);
    //sprintf(buffer, "这是第%d个消息, 编号%03d。", ii+1, ii+1);
    if ( (iret=send(sockfd, buffer, strlen(buffer), 0))<=0) // 向服务端发送请求报文。
    { perror("send"); break; }
    printf("发送: %s\n",buffer);

    memset(buffer, 0, sizeof(buffer));
    if ( (iret=recv(sockfd, buffer, sizeof(buffer),0)) <= 0) // 接收服务端的回应报文。
    {
       printf("iret=%d\n", iret); break;
    }
    printf("接收: %s\n", buffer);
    sleep(1);
  }
 
  // 第4步:关闭socket,释放资源。
  close(sockfd);
}


tcpselect.cpp

#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的最大值。 maxfd是集合中socket中最大的值

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

    while (1)
    {
        // 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。
        fd_set tmpfdset = readfdset;
        struct timeval time = {5,0};
        int infds = select(maxfd+1, &tmpfdset, NULL, NULL, &time); //select会改变tmpfdset集合
        //printf("select infds=%d\n",infds);//select会阻塞在这里,

        // 返回失败。
        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;//更新maxfd

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

                // 读取客户端的数据。  通过 iSzie 的值判断客户端是否断开连接 iSize <= 0 
                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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值