IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 【服务器系统设计】select函数的用法及其原理总结 -> 正文阅读

[系统运维]【服务器系统设计】select函数的用法及其原理总结

在Linux上,为我们提供了三种IO多路复用的函数供我们使用,select函数是网络通信编程中很常用的一个函数。select函数一般用于检测在一组socket中是否有事件准备就绪。

select的声明:

#include <sys/time.h>   //for struct timeval
#include <unistd.h>     //for select

/**
* return 状态变化的文件描述符的个数
* @param nfds: linux上的socket也是一种fd(文件描述符),将这个参数的值设置为所有需要使用select函数检测事件的fd的最大值加1
* @param readfds:需要监听可读事件的fd集合
* @param writefds:需要监听可写事件的fd集合
* @param exceptfds: 需要监听的异常事件的fd集合
* @param timeout:超时时间,即在这个参数设定的时间内检测这些fd的事件,超过这个时间后,select函数立即返回。
**/
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

fd_set是一个结构体信息,其定义位于/usr/include/sys/select.h中,其定义如下:

typedef struct
{
    long int __fds_bits[16]; //可以看作128bit的数组
} fd_set

在将一个fd添加到fd_set这个集合中时需要使用FD_SET宏,其定义如下:

 void FD_SET(int fd, fd_set* set);

将一个fd从fd_set中删除需要使用FD_CLR,其定义如下:

void FD_CLR(int fd, fd_set* set);

如果需要将fd_set中所有fd全都清除,则使用FD_ZERO,其定义如下:

void FD_ZERO(fd_set* set);

当select函数返回时,我们使用FD_ISSET宏判断在某个fd是否有我们关心的事件,FD_ISSET宏的定义如下:

int FD_ISSET(int fd, fd_set* set);

select函数使用的基本流程

在这里插入图片描述

示例代码

#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>

static const int INVALID_FD = -1; //自定义代表无效fd的值

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)
    {
        st::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 clientfds_length = clientfds.size();
        
        for(int i = 0; i < clientfds_length; ++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);
        
        if(ret == -1)
        {
            if(errno != EINTR)
            {
                break; //出错,退出程序
            }
        }
        else if(ret == 0)
        {
            //超时,继续
            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;
}

关于以上代码,在实际开发中有几个需要注意的点,如下:

  • select 函数在调用前后可能会修改readfds, writefds和exceptfds这三个集合中的内容,如果想在下次调用select函数时复用这些fd_set变量,则要在下次调用前使用FD_ZERO将fd_set清零,然后调用FD_SET将需要检测事件的fd重新添加到fd_set中。
  • select函数也会修改timeval结构体的值,如果想复用这个变量,则必须给timeval变量重新设置值。
  • select函数的timeval结构体的tv_sec和tv_usec如果都被设置为0,即检测事件的总时间被设置为0,其行为是select检测相关集合中的fd,如果没有需要的事件,则立即返回

select 函数的缺点

  • 每次调用select函数时,都需要把fd集合从用户态复制到内核态中,这个开销在fd较多时会很大,同时每次调用select函数都需要在内核中遍历传递进来的所有fd,这个开销在fd较多时也很大。
  • 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过先修改宏定义然后重新编译内核来调整这一限制,但这样非常麻烦且效率低下。
  • select函数在每次调用之前都要对传入的参数进行重新设定,这样做也比较麻烦。
  • 在Linux上,select函数的实现原理是其底层使用了poll函数
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 18:25:00  更:2022-04-18 18:27:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 20:39:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码