Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux多路转接之select函数

Linux多路转接之select函数使用方式

作者:zzu_ljk

这篇文章主要介绍了Linux多路转接之select函数使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

首先我们要了解一下,什么是多路转接?

多路转接也叫多路复用,是一种用于管理多个IO通道的技术。

它能实现同时监听和处理多个IO事件,而不是为每个IO通道创建单独的线程或者进程,多路转接允许在单个进程或线程中同时处理多个IO操作,从而提高程序的性能和效率。

本篇文章介绍的select函数,就用于select系统调用的多路转接技术。

1. 认识select函数

select函数是系统提供的一个多路转接接口。

IO = 等待就绪 + 数据拷贝,而select是只负责等。

2. select函数原型

参数说明:

参数timeout的取值:

返回值说明:

select调用失败,错误码可能被设置为:

fd_set 结构

fd_set 结构与 sigset_t 结构类似,fd_set 本质也是一个位图,用位图中对应的位来表示要监听的文件描述符。

调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统专门提供了一组专门的接口,用户对fd_set位图进行各种操作。

timeval 结构

传入select函数的最后一个参数timeout,就是一个指向timeval结构的指针,timeval结构用于描述一段时间长度,该结构当中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微妙。

3. socket就绪条件

读就绪

写就绪

异常就绪

4. select工作流程

这里我们只介绍select处理读取的操作。

如果我们要实现一个简单的select服务器,该服务器要做的就是读取客户端发来的数据并进行打印,那么这个select服务器的工作流程应该是这样的:

注意:

5. select服务器

Socket类

我们编写一个Socket类,对套接字相关的接口进行一定程序的封装,为了让外部能够直接调用Socket类当中的函数,我们将这些成员函数定义成静态成员函数。

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>

class Socket
{
public:
    // 创建套接字
    static int SocketCreate()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            std::cerr << "socket error" << std::endl;
            exit(2);
        }

        // 设置端口复用
        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        return sock;
    }

    // 绑定
    static void SocketBind(int sock, int port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        socklen_t len = sizeof(local);

        if (bind(sock, (struct sockaddr*)&local, len) < 0)
        {
            std::cerr << "bind error" << std::endl;
            exit(3);
        }
    }

    // 监听
    static void SocketListen(int sock, int backlog)
    {
        if (listen(sock, backlog) < 0)
        {
            std::cerr << "listen error" << std::endl;
            exit(4);
        }
    }
};

SelectServer类

编写SelectServer类,因为我当前使用的是云服务器,所以编写的select服务器在绑定时只需将IP地址设置为INADDR_ANY即可,所以类中只包含监听套接字和端口号两个成员变量即可。

#pragma once

#include "Socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5

class SelectServer
{
public:
    SelectServer(int port)
        : _port(port)
    {}

    void InitSelectServer()
    {
        _listen_sock = Socket::SocketCreate();
        Socket::SocketBind(_listen_sock, _port);
        Socket::SocketListen(_listen_sock, BACK_LOG);
    }

    ~SelectServer()
    {
        if (_listen_sock >= 0) close(_listen_sock);
    }

private:
    int _listen_sock;
    int _port;
};

运行服务器

服务器初始化完毕之后就可以周期性地执行某种动作了,而select服务器要做的就是不断调用select函数,当事件就绪对应执行某种动作即可。

#pragma once

#include "Socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5
#define NUM 1024
#define DFL_FD - 1

class SelectServer
{
public:
    SelectServer(int port)
        : _port(port)
    {}

    void InitSelectServer()
    {
        _listen_sock = Socket::SocketCreate();
        Socket::SocketBind(_listen_sock, _port);
        Socket::SocketListen(_listen_sock, BACK_LOG);
    }

    ~SelectServer()
    {
        if (_listen_sock >= 0) close(_listen_sock);
    }

    void Run()
    {
        fd_set readfds; // 创建读文件描述符集
        int fd_array[NUM]; // 保存需要被监视读事件是否就绪的文件描述符
        ClearFdArray(fd_array, NUM, DFL_FD); // 将数组中的所有位置设置为无效
        fd_array[0] = _listen_sock; // 将监听套接字添加到fd_array数组中的第0个位置
        while (1)
        {
            FD_ZERO(&readfds); // 清空readfds
            // 将fd_array数组当中的文件描述符添加到readfds中,并记录最大的文件描述符
            int maxfd = DFL_FD;
            for (int i = 0; i < NUM; ++i)
            {
                if (fd_array[i] == DFL_FD) continue; // 跳过无效的位置
                FD_SET(fd_array[i], &readfds);  // 将有效位置的文件描述符添加到readfds中
                if (fd_array[i] > maxfd) maxfd = fd_array[i]; // 更新最大文件描述符
            }

            switch (select(maxfd + 1, &readfds, nullptr, nullptr, nullptr))
            {
                case 0:
                    std::cout << "timeout..." << std::endl;
                    break;
                case -1:
                    std::cerr << "select error" << std::endl;
                    break;
                default:
                    std::cout << "有事件发生..." << std::endl;
                    break; 
            }
        }

    }

private:
    void ClearFdArray(int fd_array[], int num, int default_fd)
    {
        for (int i = 0; i < num; ++i) fd_array[i] = default_fd;
    }

    int _listen_sock;
    int _port;
};

启动服务器

#include "SelectServer.hpp"
#include <string>

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << "SelectServer" << " port" << std::endl;
        exit(1);
    }
    int port = atoi(argv[1]);

    SelectServer* svr = new SelectServer(port);
    svr->InitSelectServer();
    svr->Run();

    return 0;
}

由于当前服务器调用select函数时直接将timeout设置为了nullptr,因此select函数调用后会进行阻塞等待。

而服务器在第一次调用select函数时只让select函数监视监听套接字的读事件,所以运行服务器之后如果没有客户端发来连接请求,那么读事件就不会就绪,而服务器会一直在第一次调用的select函数中进行阻塞等待。

当我们借助telnet工具向select服务器发起连接请求之后,select函数就会立马检测到监听套接字的读事件就绪,此时select函数便会返回成功,并将我们设置的提示语句进行打印输出,因为当前程序没有对就绪事件进行处理,此后每次select函数一调用就会检测到读事件就绪成功返回,因此屏幕不但打印输出提示语句。

如果服务器在调用select函数时将timeout的值设置为0,那么select函数调用后就会进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。

此时如果select监视的文件描述符上有事件就绪,那么select函数的返回值就是大于0的,如果select函数监视的文件描述符上没有事件就绪,那么select的返回值就是小于0的,这里也就不进行演示了。

事件处理

当select检测到右文件描述符的读事件就绪并成功返回后,接下来就应该对就绪事件进行处理了,这里编写一个HandleEvent函数,当读事件就绪之后就调用该函数进行事件处理。

    void HandleEvent(const fd_set& readfds, int fd_array[], int num)
    {
        for (int i = 0; i < num; ++i)
        {
            // 跳过无效位置
            if (fd_array[i] == DFL_FD) continue;
                
            // 连接事件就绪
            if (fd_array[i] == _listen_sock && FD_ISSET(fd_array[i], &readfds))
            {
                // 获取连接
                struct sockaddr_in peer;
                memset(&peer, 0, sizeof(peer));
                socklen_t len = sizeof(peer);
                int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
                if (sock < 0)
                {
                    std::cerr << "accept error" << std::endl;
                    continue;
                }
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                int peer_port = ntohs(peer.sin_port);
                std::cout << "get a new link[" << peer_ip << ":" << peer_port << "]" << std::endl;
                // 将获取到的文件描述符添加到fd_array中
                if (!SetFdArray(fd_array, num, sock))
                {
                    // 如果添加失败,关闭文件描述符
                    close(sock);
                    std::cout << "select server is full, close fd: " << sock << std::endl;
                }
            }
        }
    }

private:
    bool SetFdArray(int fd_array[], int num, int fd)
    {
        for (int i = 0; i < num; ++i)
        {
            if (fd_array[i] == DFL_FD) 
            {
                fd_array[i] = fd;
                return true;
            }
        }
        return false;
    }

添加文件描述符到fd_array数组中,本质就是遍历fd_array数组,找到一个没有被使用的位置将该文件描述符添加进去即可。

但有可能fd_array数组中全部的位置都已经被占用了,那么文件描述符就会添加失败,此时就只能将刚刚获取上来的连接对应的套接字进行关闭,因为此时服务器是没有能力处理这个连接的。

该select服务器存在的一些问题

6. select的优缺点

select的优点

当然,这也是所有多路转接接口的优点。

select的缺点

select可监控的文件描述符有1024个,除去其中的一个监听套接字,那么它最多只能连接1023个客户端。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文