【TCP/IP】利用I/O复用技术实现并发服务器 - select函数

目录

I/O复用技术

select函数

设置文件描述符

指定监视范围

设置超时

I/O复用服务器端的实现


      由服务器创建多个进程来实现并发的做法有时会带来一些问题,比如:内存上的开销、CPU的大量占用等,这些因素会消耗掉服务器端有限的计算资源、进而影响程序之间的执行效率。那么,有没有方法可以在不创建额外进程的条件下实现并发呢?当然有,那就是I/O复用技术。

I/O复用技术

        I/O复用指的是通过单个线程记录一个或多个I/O流的状态,并对不同状态下的I/O流进行协调,使进程不阻塞于某个特定的I/O调用过程中,从而将有限资源最大化利用

        引用自一段情景材料,方便大家能更好地理解这个技术背后的思想:

假设你是一个机场的空管,你需要管理到你机场的所有的航线,包括进港,出港,有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。

你会怎么做?

最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机,从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。

那么问题就来了:

  • 很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。
  • 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后,基本上就成菜市场了。
  • 空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。

现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?他们用这个东西:

这个东西叫 flight progress strip,每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。这个东西现在还没有淘汰哦,只是变成电子的了而已。是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。 

如果你把每一个航线当成一个 Sock(I/O 流),空管当成你的服务端 Sock 管理代码的话:

  • 第一种方法就是最传统的多进程并发模型:每进来一个新的 I/O 流会分配一个新的进程管理。
  • 第二种方法就是 I/O 多路复用:单个线程,通过记录跟踪每个 I/O 流(sock)的状态,来同时管理多个 I/O 流 。 

                                                                        参考资料:IO 多路复用是什么意思? - 罗志宇

select函数

        select是I/O复用技术中比较经典且常用到的一个函数(还有poll、epoll),在使用上,需要引入<sys/select.h>和<sys/time.h>两个头文件。

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd , fd_set * readset , fd_set * writeset , fd_set * exceptset , const struct timeval * timeout);

//成功时返回大于0的值,失败时返回-1。其余情况为返回发生事件(监视项)的文件描述符数量

/* 监视项 */
// 1.接收数据的套接字
// 2.传输数据(无阻塞)的套接字
// 3.发生异常的套接字

/* 参数含义 */
// maxfd: 监视对象文件描述符数量
// readset: 将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。
// writeset: 将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。
// exceptset: 将所有关注"是否发生异常"的文件描述符注册至fd_set型变量,并传递其地址值。
// timeout: 调用 select 函数后,为防止陷入无限阻塞的状态,传递超时(time-out)消息。


        使用select函数时,一般遵循以下流程步骤:

设置文件描述符

        在调用select函数之前,我们需要声明监视事件,并用数据类型为fd_set的变量来记录监视事件下的每个文件描述符的状态。如图所示,fd_set以数组的形式记录每个文件描述符的状态:

        以图中fd0、fd2为例,值为0,代表着所指向的文件描述符不是被监视对象;反之,fd1和fd3值为1,则是被监视对象。

        在对fd_set数组操作时,有些宏可以帮助我们简化代码的编写工作,下列所示为与对fd_set操作相关的宏:

  • FD_ZERO(fd_set * fdset): 将fd_set变量的所有位初始化为0 。
  • FD_SET(int fd , fd_set * fdset): 在参数fdset指向的变量中注册文件描述符fd的信息。
  • FD_CLR(int fd , fd_set * fdset): 从参数fdset指向的变量中清除文件描述符fd的信息。
  • FD_ISSET(int fd , fd_set * fdset): 若参数fdset指向的变量中包含文件描述符fd的信息,则返回"真",用于对select调用结果的验证。

        宏对应的操作含义如图所示:

指定监视范围

        即设置select函数中的第一个参数 maxfd,其值用来标记限制对监视事件中的文件描述符最大监视数。

设置超时

        当监视的文件描述符未发生变化时,select函数会导致进程发生阻塞。为了避免这种情况发生,我们可以通过设置超时(select函数中的最后一个参数timeout)来防止这种情况的发生。

        timeout的结构体定义如下:

struct timeval
{
    long tv_sec; // 秒
    long tv_usec; // 毫秒
}

//timeval.tv_sec=5,timeval.tv_usec=500; 代表设置超时等待周期为5秒500毫秒

I/O复用服务器端的实现

io_echoserver.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>

#define BUF_SIZE 1024

void Sender_error(char *message);

int main(int argc, char *argv[])
{
    int port, serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;
    socklen_t adr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
    //可以改为直接传参(即使用argv数组),这里主要方便验证和展示
    printf("Please input the port of socket that you want to create:\n");
    scanf("%d", &port);

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        Sender_error((char *)"Sock creation error");
    }
    else
    {
        // 注意:serv_sock初始化成功后值为0
        fd_max = serv_sock;
    }

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(port);

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
    {
        Sender_error((char *)"Bind() error");
    }
    if (listen(serv_sock, 5) == -1)
    {
        Sender_error((char *)"Listen error");
    }
    // 对监测项的文件描述符作初始化赋0操作
    FD_ZERO(&reads);
    // 注册serv_sock套接字信息至reads变量中
    FD_SET(serv_sock, &reads);

    while (1)
    {
        // cpy_reads用来记录文件描述符变化
        cpy_reads = reads;
        // 设置超时等待周期为5s
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        // 只关注接收数据的套接字,不对传输数据、出现异常的套接字进行监视
        // fd_num用来记录发生监视事件的文件描述符数量
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
        {
            break;
        }

        if (fd_num == 0)
        {
            continue;
        }

        for (i = 0; i < fd_max + 1; i++)
        {
            // 判断cpy_reads中是否含有文件描述符i的信息
            if (FD_ISSET(i, &cpy_reads))
            {
                // 若当前只有服务器端套接字,则尝试接收客户端请求
                if (i == serv_sock)
                {
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    // 将客户端的套接字文件描述符信息注册至reads中
                    FD_SET(clnt_sock, &reads);
                    if (fd_max < clnt_sock)
                    {
                        // 增加监视数上限(因为有新的客户端套接字加入)
                        fd_max = clnt_sock;
                    }
                    printf("Connected client: %d \n", clnt_sock);
                }
                else
                {
                    // 接收数据
                    str_len = read(i, buf, BUF_SIZE);
                    // 无数据,则关闭对应套接字
                    if (str_len == 0)
                    {
                        // 清除reads变量中文件描述符i的信息
                        FD_CLR(i, &reads);
                        close(i);
                        printf("Closed client: %d \n", i);
                    }
                    else
                    {
                        write(i, buf, str_len);
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

void Sender_error(char *message)
{
    puts(message);
    exit(1);
}

运行结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

干吃咖啡豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值