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

这篇具有很好参考价值的文章主要介绍了【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个人的时候还好,几十号人以后,基本上就成菜市场了。
  • 空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。

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

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

这个东西叫 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函数时,一般遵循以下流程步骤:

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

设置文件描述符

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

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

        以图中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调用结果的验证。

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

【TCP/IP】利用I/O复用技术实现并发服务器 - 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);
}

运行结果:

【TCP/IP】利用I/O复用技术实现并发服务器 - select函数文章来源地址https://www.toymoban.com/news/detail-483139.html

到了这里,关于【TCP/IP】利用I/O复用技术实现并发服务器 - select函数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • TCP服务器的演变过程:IO多路复用机制select实现TCP服务器

    手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。 为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。 本节,在上一章节的基础上,将并发的实现改为IO多路复用机制,使用select管理每个新接入的客户端连

    2024年02月03日
    浏览(58)
  • 【TCP服务器的演变过程】使用IO多路复用器epoll实现TCP服务器

    手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。 为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。 本节,在上一章节的基础上,将IO多路复用机制select改为更高效的IO多路复用机制epoll,使用epoll管理每

    2024年01月17日
    浏览(70)
  • FPGA实现 TCP/IP 协议栈 服务器 纯VHDL代码编写 提供4套vivado工程源码和技术支持

    FPGA实现 TCP/IP 协议栈 服务器 纯VHDL代码编写 提供4套vivado工程源码和技术支持 没玩过TCP网络通信都不好意思说自己玩儿过FPGA,这是CSDN某大佬说过的一句话,鄙人深信不疑。。。目前网上fpga实现udp协议的源码满天飞,我这里也有不少,但用FPGA纯源码实现TCP的项目却很少,能上

    2024年02月04日
    浏览(50)
  • epoll多路复用_并发服务器

    应用程序: 驱动程序:

    2024年02月15日
    浏览(57)
  • 使用select实现TCP并发服务器模型

    本期主要分享的是对于select的使用,使用select实现TCP并发服务器模型,由于之前所用到的技术知识只能够支撑我们进行单个访问,但是有了select之后呢,我们就能够实现多用户进行访问;这也是非常符合客观需求的; 这次呢我们重点来使用一下select; 用到的头文件如下: 我

    2024年02月08日
    浏览(47)
  • FPGA实现10G万兆网TCP/IP 协议栈,纯VHDL代码编写,提供服务器和客户端2套工程源码和技术支持

    目前网上fpga实现udp协议的源码满天飞,我这里也有不少,但用FPGA纯源码实现TCP的项目却很少,能上板调试跑通的项目更是少之又少,甚至可以说是凤毛菱角,但很不巧,本人这儿就有一个; 本设采用纯VHDL实现了10G万兆网TCP/IP协议栈,该协议栈分为TCP服务器核客户端,没有使

    2024年02月09日
    浏览(66)
  • Linux多路IO复用技术——epoll详解与一对多服务器实现

    本文详细介绍了Linux中epoll模型的优化原理和使用方法,以及如何利用epoll模型实现简易的一对多服务器。通过对epoll模型的优化和相关接口的解释,帮助读者理解epoll模型的工作原理和优缺点,同时附带代码实现和图解说明。

    2024年02月05日
    浏览(43)
  • linux并发服务器 —— IO多路复用(八)

    半关闭只能实现数据单方向的传输;当TCP 接中A向 B 发送 FIN 请求关闭,另一端 B 回应ACK 之后 (A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态 (半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据 close不会影响到其他进程,shutdown会

    2024年02月09日
    浏览(51)
  • 【高并发服务器 02】——线程池与IO多路复用

    线程池的好处 :所有的池都是为了事先把资源准备好,在后续用的时候可以更加方便的拿到这个资源—— 不用去申请、释放资源 什么时候用线程池 ? IO事务并发较高 :人在杭州,但是数据库在北京,想要查询数据库,需要通过互联网建立TCP三次握手,频繁地创建和销毁线

    2024年03月23日
    浏览(50)
  • C/S架构学习之多线程实现TCP并发服务器

    并发概念: 并发是指两个或多个事件在 同一时间间隔 发生; 多线程实现TCP并发服务器的实现流程: 一、创建套接字(socket函数): 通信域选择IPV4网络协议、套接字类型选择流式; 二、填充服务器的网络信息结构体: 1.定义网络信息结构体变量; 2.求出结构体变量的内存

    2024年02月06日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包