select函数的用法和原理

发布时间 2023-11-22 18:03:00作者: 野原丶广志

Linux 上的 select 函数

select 函数用于检测在一组 socket 中是否有事件就绪。事件分为以下三类:

  1. 读就绪事件
  • 在 socket 内核中,接收缓冲区中的字节数大于或等于低水位标记 SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞地读该文件描述符,并且返回值大于 0。
  • TCP 连接的对端关闭连接,此时本端调用 recv 和 read 函数对 socket 进行读操作,recv 或 read 函数会返回 0 值。
  • 在监听 socket 上有新的连接请求。
  • 在 socket 上有未处理的错误。
  1. 写就绪事件
  • 在 socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小)大于或等于低水位标记 SO_SENDLOWAT 时,可以无阻塞的写,并且返回值大于 0。
  • socket 的写操作被关闭(即调用了 close 或 shutdown 函数)时,对一个写操作被关闭的 socket 进行写操作,会触发 SIGPIPE 信号。
  • socket 使用阻塞 connect 连接成功或失败时。
  1. 异常就绪事件

函数签名:

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

//timeval类型:
struct timeval
{
  long tv_sec;  /* 秒 */
  long tv_usec; /* 微秒 */
}

//fd_set结构体类型,简写如下
typedef struct
{
  long int __fds_bits[16];  //可以看做 128 bit 的数组 
} fe_set;

__fds_bits 是 long int 类型的数组, long int 占 8 字节,每字节都有 8bit,每个 bit 都对应一个 fd 的事件状态,0 表示无事件,1 表示有事件,数组长度是 16。因此一共可以表示 8816=1024 个 fd 的状态,这是 select 函数支持的最大 fd 数量。(位图法)

具体示例:

点击查看代码
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>
#include <vector>
#include <errno.h>

//自定义代表无效 fd 的值
#define INVALID_FD -1

int main(int argc, char* argv[])
{
    //创建一个监听 socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == INVALID_FD){
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }

    //初始化服务器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if(bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1){
        std::cout << "bind listen socket error." << std::endl;
        close(listenfd);
        return -1;
    }

    //启动监听
    if(listen(listenfd, SOMAXCONN) == -1){
        std::cout << "listen error." << std::endl;
        close(listenfd);
        return -1;
    }

    //存储客户端 socket 的数组
    std::vector<int> clientfds;
    int maxfd;

    while(true)
    {
        fd_set readset;
        FD_ZERO(&readset);

        //将监听的socket加入待检测的可读事件中
        FD_SET(listenfd, &readset);

        maxfd = listenfd;
        //将客户端fd加入待检测的可读事件中
        int clientfdslength = clientfds.size();
        for(int i = 0; i < clientfdslength; i++){
            if(clientfds[i] != INVALID_FD){
                FD_SET(clientfds[i], &readset);

                if(maxfd < clientfds[i]) maxfd = clientfds[i];
            }
        }

        timeval tm;
        tm.tv_sec = 1;
        tm.tv_usec = 0;
        //暂且只检测可读事件,不检测可写和异常事件
        int ret = select(maxfd+1, &readset, NULL,NULL,&tm);
        std::cout << "select success" << std::endl;
        if(ret == -1){
            //出错,退出程序
            if(errno == EINTR){
                break;
            }
        }else if(ret == 0){
            //select函数超时,下次继续
            continue;
        }else{
            //检测到某个socket有事件
            if(FD_ISSET(listenfd, &readset)){
                //监听socket的可读事件,表明有新的连接到来
                struct sockaddr_in clientaddr;
                socklen_t clientaddrlen = sizeof(clientaddr);
                //接受客户端连接
                int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen);
                if(clientfd == INVALID_FD){
                    //接受连接出错,退出程序
                    break;
                }

                //只接受连接,不调用recv收取任何数据
                std::cout << "accept a client connection, fd: " << clientfd << std::endl;
                clientfds.push_back(clientfd);

            }
            else{
                //假设对端发来的数据长度不超过63个字符
                char recvbuf[64];
                int clientfdslength = clientfds.size();
                for(int i = 0; i < clientfdslength; i++){
                    if(clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i], &readset)){
                        memset(recvbuf, 0, sizeof(recvbuf));
                        //非监听socket, 接收数据
                        int length = recv(clientfds[i], &recvbuf, 64, 0);
                        if(length <= 0){
                            //收取数据出错
                            std::cout << "recv data error, clientfd:" << clientfds[i] << std::endl;
                            
                            close(clientfds[i]);
                            //不直接删除该元素,将该位置的元素标记为 INVALID_FD
                            clientfds[i] = INVALID_FD;
                            continue;
                        }

                        std::cout << "clientfd:" << clientfds[i] << ", recv data:" << recvbuf << std::endl;

                    }
                    
                }
            }
        }

    }

    //关闭所有客户端 socket
    int clientfdslength = clientfds.size();
    for(int i = 0; i < clientfdslength; ++i){
        if(clientfds[i] != INVALID_FD)
        {
            close(clientfds[i]);
        }
    }

    //关闭监听socket
    close(listenfd);
    return 0;
}