Linux网络编程系列之服务器编程——多路复用模型

news2024/11/30 3:44:01

Linux网络编程系列  (够吃,管饱)

        1、Linux网络编程系列之网络编程基础

        2、Linux网络编程系列之TCP协议编程

        3、Linux网络编程系列之UDP协议编程

        4、Linux网络编程系列之UDP广播

        5、Linux网络编程系列之UDP组播

        6、Linux网络编程系列之服务器编程——阻塞IO模型

        7、Linux网络编程系列之服务器编程——非阻塞IO模型

        8、Linux网络编程系列之服务器编程——多路复用模型

        9、Linux网络编程系列之服务器编程——信号驱动模型

一、什么是多路复用模型

        服务器的多路复用模型指的是利用操作系统提供的多路复用机制,同时处理多个客户端连接请求的能力。在服务器端,常见的多路复用技术包括select、poll和epoll等。这些技术允许服务器同时监听多个客户端连接请求,当有请求到达时,会通知服务器进行处理。通过使用多路复用技术,可以避免一个线程只处理一个客户端连接的情况,提高服务器的并发性能和响应速度。在实际应用中,多路复用技术被广泛地应用于Web服务器、游戏服务器、消息队列等领域。

        注:下面案例演示采用select结合TCP协议,一般不结合UDP协议使用,案例也演示了select结合UDP协议。

二、特性

        1、支持大量并发连接

        多路复用技术可以同时监听多个客户端连接请求,避免了一个线程只处理一个客户端连接的情况,从而可以支持更多的并发连接。

        2、减少系统开销

        采用多路复用技术可以减少系统开销,因为不需要为每个连接开启一个线程或进程,避免了系统资源浪费。

        3、提高响应速度

        采用多路复用技术可以提高服务器的响应速度,因为多个连接可以同时处理,避免了连接排队的情况。

        4、更好的可扩展性

        多路复用技术可以更好的支持服务器的可扩展性,因为它可以动态地管理和调度连接,方便服务器的扩展和升级。

三、使用场景

        1、高并发的Web服务器

        对于高并发的Web服务器,采用多路复用技术可以同时监听多个客户端连接请求,避免了一个线程只处理一个客户端连接的情况,从而可以支持更多的并发连接。

        2、实时通信服务器

        对于实时通信服务器,采用多路复用技术可以同时监听多个客户端连接请求,可以处理多种类型的通信,包括即时通讯、实时游戏等。

        3、TCP/IP服务器

        对于TCP/IP服务器,采用多路复用技术可以提高服务器的性能和可靠性,因为多个连接可以同时处理,避免了连接排队的情况。

        4、网络监控工具

        对于网络监控工具,采用多路复用技术可以同时处理多个客户端的请求,并对网络数据进行监控和分析。

四、模型框架(通信流程)

        1、建立套接字。使用socket()

        2、设置端口复用。使用setsockopt()

        3、绑定自己的IP地址和端口号。使用bind()

        4、设置监听。使用listen()

        5、多路复用准备工作。使用文件描述符集合操作

        6、循环监听,开始多路复用。使用select()

        7、处理客户端连接或者数据接收。使用accept()或者recv()

        8、关闭套接字。使用close()

五、相关函数API接口

        TCP通信流程常规的API那些在本系列的TCP协议里有大量展示,这里省略,详情可以点击本文开头的链接查看

        1、多路复用select

// 多路复用select
int select(int nfds, 
           fd_set *readfds,
           fd_set *writefds,   
           fd_set *exceptfds, 
           struct timeval *timeout);

// 接口说明
        返回值:成功返回readfds,writefds,exceptfds中状态发生变化的文件描述符数量,失败返回-1
        参数nfds:通常填写三个集合中最大的文件描述符值+1,让内核检测多少个文件描述符的状态
        参数readfds:监控有读数据到达文件描述符集合
        参数writefds:监控有写数据到达文件描述符集合
        参数exceptfds:监控有异常发生到达文件描述符集合
        参数timeout:设置阻塞等待时间,三种情况
            (1)、设置为NULL,一直阻塞等待
            (2)、设置timevl,等待固定的时间
            (3)、设置timeval里时间为0,在检测完描述符后立即返回

        2、集合操作

// 把文件描述符集合里fd清0
void FD_CLR(int fd, fd_set *set);

// 把文件描述符集合里fd位置1
void FD_SET(int fd, fd_set *set);

// 把文件描述符集合里所有位清0
void FD_ZERO(fd_set *set);

// 测试文件描述符集合里fd是否置1
int FD_ISSET(int fd, fd_set *set);

六、案例

       1、 采用select函数,完成多路复用TCP服务器的通信演示,用nc命令来模拟客户端

// 多路复用TCP服务器的案例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>       
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>


#define MAX_LISTEN  FD_SETSIZE // 最大能处理的连接数, 1024
#define SERVER_IP   "192.168.64.128"    // 记得改为自己IP
#define SERVER_PORT 20000   // 不能超过65535,也不要低于1000,防止端口误用

// 定义客服端管理类
struct ClientManager
{
    int client[MAX_LISTEN];     // 存储客户端的套接字
    char ip[MAX_LISTEN][20];    // 客户端套接字IP
    uint16_t port[MAX_LISTEN];  // 客户端套接字端口号
};

// 初始化客户端管理类
void client_manager_init(struct ClientManager *manager)
{
    for(int i = 0; i < MAX_LISTEN; i++)
    {
        manager->client[i] = -1;
        manager->port[i] = 0;
        memset(manager->ip, 0, sizeof(manager->ip));
    }
}


int main(int argc, char *argv[])
{
    // 1、建立套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1)
    {
        perror("socket fail");
        return -1;
    }
    // 2、设置端口复用(推荐)
    int optval = 1; // 这里设置为端口复用,所以随便写一个值
    int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    if(ret == -1)
    {
        perror("setsockopt fail");
        close(sockfd);
        return -1;
    }

    // 3、绑定自己的IP地址和端口号
    struct sockaddr_in server_addr = {0};
    socklen_t addr_len = sizeof(struct sockaddr);
    server_addr.sin_family = AF_INET;   // 指定协议为IPV4地址协议
    server_addr.sin_port = htons(SERVER_PORT);  // 端口号
    // server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址

    ret = bind(sockfd, (struct sockaddr *)&server_addr, addr_len);
    if(ret == -1)
    {
        perror("bind fail");
        close(sockfd);
        return -1;
    }

    // 4、设置监听
    ret = listen(sockfd, MAX_LISTEN);
    if(ret == -1)
    {
        perror("listen fail");
        close(sockfd);
        return -1;
    }

    // 5、多路复用的准备工作
    fd_set client_set, active_set;

    // (1)、清空活跃的文件描述符集合
    FD_ZERO(&active_set);
    // (2)、把服务器的套接字文件描述符加入到活跃的文件描述符集合中
    FD_SET(sockfd, &active_set);
    // (3)、初始化活跃集合中最大的文件描述符
    int maxfd = sockfd;

    // (4)、初始化能接受的活跃客户端管理类
    struct ClientManager manager;
    client_manager_init(&manager);
    

    uint16_t port = 0;  // 新的客户端端口号
    char ip[20] = {0};  // 新的客户端IP
    struct sockaddr_in client_addr; // 新的客户端地址
    char recv_msg[128] = {0};   // 用来接收客户端的数据

  

    printf("wait client...\n");

    while(1)
    {
        client_set = active_set;    // 先备份活跃的集合

        // 6、多路复用,同时监听多个文件描述符状态,阻塞等待
        int num = select(maxfd+1, &client_set, NULL, NULL, NULL);
        if(num == -1)
        {
            perror("select fail");
            close(sockfd);
            return -1;
        }

        // 如果监听文件描述符发生变化,说明一定有新的客户端连接上来
        if(FD_ISSET(sockfd, &client_set))
        {
            int new_client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
            if(new_client_fd == -1)
            {
                perror("accept fail");
                continue;
            }
            else
            {
                // 打印连接的客服端IP和端口号
                memset(ip, 0, sizeof(ip));
                strcpy(ip, inet_ntoa(client_addr.sin_addr));
                port = ntohs(client_addr.sin_port);
                printf("[%s:%d] connect\n", ip, port);
                
                // 把新的客户端套接字加入到活跃的集合中
                FD_SET(new_client_fd, &active_set);

                // 更新最大活跃文件描述符
                if(maxfd < new_client_fd)
                {
                    maxfd = new_client_fd;
                }

                // 把新的套接字加入到空的活跃客户端套接字数组
                for(int i = 0; i < MAX_LISTEN; i++)
                {
                    if(manager.client[i] == -1)
                    {
                        manager.client[i] = new_client_fd;
                        manager.port[i] = port;
                        strcpy(manager.ip[i], ip);
                        break;
                    }
                }
                
                // 如果只有服务器的套接字发生变化,新的套接字没有发送数据
                // 那就继续监听,否则需要打印套接字的信息
                if(--num == 0)
                {
                    continue;
                }
            }
        }
        // 如果客服端发送数据过来
        for(int i = 0; i < MAX_LISTEN; i++)
        {
            if(manager.client[i] == -1)
            {
                continue;
            }
            
            // 如果活跃的客户端有发送数据,注意这里要采用client_set,而不是active_set,否则会读取不了数据
            if(FD_ISSET(manager.client[i], &client_set))
            {
                // 接收数据
                memset(recv_msg, 0, sizeof(recv_msg));
                ret = recv(manager.client[i], recv_msg, sizeof(recv_msg), 0);

                memset(ip, 0, sizeof(ip));
                strcpy(ip, inet_ntoa(client_addr.sin_addr));
                port = ntohs(client_addr.sin_port);

                if(ret == 0)
                {
                    printf("[%s:%d] disconnect\n", manager.ip[i], manager.port[i]);

                    FD_CLR(manager.client[i], &active_set); // 清空对应活跃集合的套接字
                    manager.client[i] = -1;
                    manager.port[i] = 0;
                    memset(manager.ip[i], 0, sizeof(ip));
                   
                    // 需要重新更新活跃集合中最大的文件描述符
                    maxfd = sockfd;
                    for(int j = 0; j < MAX_LISTEN; j++)
                    {
                        if(manager.client[j] != -1 && maxfd < manager.client[j])
                        {
                            maxfd = manager.client[j];
                        }
                    }
                }
                else if(ret > 0)
                {
                    printf("[%s:%d] send data: %s\n", manager.ip[i], manager.port[i], recv_msg);
                }

                // 如果所有发生变化的套接字都已经处理完成
                if(--num == 0)
                {
                    break;
                }
            }
        }
    }
    
    close(sockfd);

    return 0;
}

           2、 采用select函数,完成多路复用UDP服务器的通信演示,用nc命令来模拟客户端

// 多路复用TCP服务器的案例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>       
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <errno.h>

#define MAX_LISTEN  FD_SETSIZE // 最大能处理的连接数, 1024
#define SERVER_IP   "192.168.64.128"    // 记得改为自己IP
#define SERVER_PORT 20000   // 不能超过65535,也不要低于1000,防止端口误用


int main(int argc, char *argv[])
{
    // 1、建立套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd == -1)
    {
        perror("socket fail");
        return -1;
    }

    // 2、设置端口复用(推荐)
    int optval = 1; // 这里设置为端口复用,所以随便写一个值
    int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    if(ret == -1)
    {
        perror("setsockopt fail");
        close(sockfd);
        return -1;
    }

    // 3、绑定自己的IP地址和端口号
    struct sockaddr_in server_addr = {0};
    socklen_t addr_len = sizeof(struct sockaddr);
    server_addr.sin_family = AF_INET;   // 指定协议为IPV4地址协议
    server_addr.sin_port = htons(SERVER_PORT);  // 端口号
    // server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址

    ret = bind(sockfd, (struct sockaddr *)&server_addr, addr_len);
    if(ret == -1)
    {
        perror("bind fail");
        close(sockfd);
        return -1;
    }

    // 4、多路复用的准备工作
    fd_set client_set, active_set;

    // (1)、清空活跃的文件描述符集合
    FD_ZERO(&active_set);
    // (2)、把服务器的套接字文件描述符加入到活跃的文件描述符集合中
    FD_SET(sockfd, &active_set);
    // (3)、初始化活跃集合中最大的文件描述符
    int maxfd = MAX_LISTEN;

    // (4)、初始化能接受的活跃客户端套接字数组
    int client[MAX_LISTEN];
    for(int i = 0; i < MAX_LISTEN; i++)
    {
        client[i] = -1;     // 空的置为-1,活跃的置为对应的文件描述符
    }

    uint16_t port = 0;  // 新的客户端端口号
    char ip[20] = {0};  // 新的客户端IP
    struct sockaddr_in client_addr; // 新的客户端地址
    char recv_msg[128] = {0};   // 用来接收客户端的数据

    printf("wait client...\n");

    while(1)
    {
        client_set = active_set;    // 先备份活跃的集合

        // 5、多路复用,同时监听多个文件描述符状态,阻塞等待
        int num = select(maxfd+1, &client_set, NULL, NULL, NULL);
        if(num == -1)
        {
            perror("select fail");
            close(sockfd);
            return -1;
        }
        else
        {
            // 接收数据
            memset(recv_msg, 0, sizeof(recv_msg));
            ret = recvfrom(sockfd, recv_msg, sizeof(recv_msg), 0, (struct sockaddr*)&client_addr, &addr_len);

            memset(ip, 0, sizeof(ip));
            strcpy(ip, inet_ntoa(client_addr.sin_addr));
            port = ntohs(client_addr.sin_port);
            printf("[%s:%d] send data: %s\n", ip, port, recv_msg);
        }
    }

    close(sockfd);

    return 0;
}

        注:TCP和UDP的代码有所不同,多路复用监听方式有所不同。

七、总结

        多路复用适用于处理连接的客户端的数量小于1024的场景,当然你可以改,让其超过1024限制,这里不做讨论。多路复用模型TCP服务器跟简单的TCP服务器通信流程很像,就是在接收客户端时要采用select要进行操作。一般情况下,不采用多路复用select结合UDP协议使用,但是不代表不行,案例给出了演示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1095874.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ROS-6.参数的使用

参数的使用 参数服务结构命令行的使用方式运行小海龟命令介绍查看参数获取参数值设置参数保存参数到文件从文件导入参数 通过程序操作创建节点修改cmake编译运行 参数服务结构 ros中存在参数服务管理服务&#xff0c;管理这所有参数&#xff0c;所有节点剋订阅和发布这些节点 …

第三章 内存管理 三、覆盖与交换

目录 一、覆盖技术 二、交换技术 三、总结 一、覆盖技术 1、在覆盖技术中&#xff0c;我们要找到程序的调用结构。 2、因为这些程序不可能同时被调用&#xff08;互斥调用&#xff09;&#xff0c;所以我们只需要选出需要空间最大的程序。 3、在物理内存中开拓一片与最大程…

ABB机器人关于重定位移动讲解

关于机器人如何重定位移动&#xff0c;首先来看一下示教器上的重定位移动是在哪。 从图中所示的坐标位置和操纵杆方向得知&#xff0c;重定位的本质是绕X、Y、Z轴的旋转。那么实现跟摇杆一样的操作&#xff0c;就可以通过改变当前位置的欧拉角来实现&#xff0c;参考Rapid指令…

小米笔记本Pro 15.6“频繁蓝屏解决办法

一、事情的缘起 2020年3月&#xff0c;我在小米官网购买这个笔记本&#xff0c;型号为&#xff1a;小米笔记本Pro 15.6" 2019款 四核i5 8G MX250 深灰。当时买这款笔记本&#xff0c;也是考虑到它屏幕比较大&#xff0c;而且配置也不错&#xff0c;四核8G的内存也足够我办…

测试需要写测试用例吗?

如何理解软件的质量 我们都知道&#xff0c;一个软件从无到有要经过需求设计、编码实现、测试验证、部署发布这四个主要环节。 需求来源于用户反馈、市场调研或者商业判断。意指在市场行为中&#xff0c;部分人群存在某些诉求或痛点&#xff0c;只要想办法满足这些人群的诉求…

并行Stream的性能测试

final long count 200_000_000;Random random new Random();//创建2亿条的listList<Integer> list Stream.generate(() -> random.nextInt(20)).limit(count).collect(Collectors.toList());// 顺序处理long startTime System.currentTimeMillis();list.stream().…

C语言联合体和枚举

C语言联合体和枚举 文章目录 C语言联合体和枚举一、联合体①联合体简介②联合体大小的计算 二、枚举 一、联合体 ①联合体简介 union Un {char c;int i; };像结构体一样&#xff0c;联合体也是由⼀个或者多个成员构成&#xff0c;这些成员可以不同的类型。但是编译器只为最大…

【Python数据分析工具】

文章目录 概要整体架构流程技术名词解释 概要 数据分析是一种通过收集、处理、分析和解释大量数据&#xff0c;以发现有价值信息、洞察趋势、制定决策并解决问题的过程。在现代科技和互联网的推动下&#xff0c;数据分析变得日益重要。它不仅仅是对数字和图表的简单解释&#…

GCOV覆盖率分析

安全之安全(security)博客目录导读 覆盖率分析汇总 目录 一、GCOV简介 二、GCOV使用示例 三、GCOV编译命令 四、运行并生成覆盖率报告 五、覆盖率报告分析 一、GCOV简介 因为动态代码分析可能只覆盖部分代码&#xff0c;所以我们需要一个代码覆盖工具&#xff0c;以了解…

APP备案避坑指南,值得收藏

目录 什么时间节点前需完成备案&#xff1f; APP/小程序一定要做备案吗&#xff1f; 涉及前置审批的APP有哪些&#xff1f; APP 支持安卓、IOS 多个运行平台&#xff0c;应该备案多少次&#xff1f; 企业是自有服务器&#xff0c;该如何进行APP备案&#xff1f; APP备案可…

探索服务器的无限潜能:创意项目、在线社区与更多可能

文章目录 1. 创意项目的孵化器1.1 托管你的应用1.2 测试和开发1.3 制定和实施你的计划 2. 构建在线社区2.1 自定义社交网络2.2 数据控制2.3 扩展和改进 3. 其他创意可能性3.1 博客和媒体网站3.2游戏服务器3.3 云存储3.4 数据分析3.5 远程办公 结论 &#x1f389;欢迎来到Java学…

你不一定知道的七种进程间通信方式

一、前言 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到&#xff0c;所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷贝到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信…

Easysearch压缩模式深度比较:ZSTD+source_reuse的优势分析

引言 在使用 Easysearch 时&#xff0c;如何在存储和查询性能之间找到平衡是一个常见的挑战。Easysearch 具备多种压缩模式&#xff0c;各有千秋。本文将重点探讨一种特别的压缩模式&#xff1a;zstd source_reuse&#xff0c;我们最近重新优化了 source_reuse,使得它在吞吐量…

【ROS2RUN源码解析:解决ROS2 run命令找不到问题的详细流程】

文章目录 概要整体架构流程技术名词解释小结 概要 当你在使用ROS2时遇到找不到可执行文件的错误时&#xff0c;首先需要执行以下步骤来诊断问题。首先&#xff0c;使用命令printenv AMENT_PREFIX_PATH&#xff08;或者ros2 pkg prefix加上包的名称&#xff09;来检查你的功能包…

AI :微软推出 AutoGen 框架,帮开发者创建基于大语言模型的复杂应用

本心、输入输出、结果 文章目录 AI :微软推出 AutoGen 框架,帮开发者创建基于大语言模型的复杂应用前言AutoGen 简介快速入门AutoGen 安装相关支持相关代码相关架构图弘扬爱国精神AI :微软推出 AutoGen 框架,帮开发者创建基于大语言模型的复杂应用 编辑:简简单单 Online z…

Chrome 浏览器关闭后再打开,需要重新登录账号,解决办法

最近&#xff08;2023-10-15&#xff09;每次打开 Chrome 浏览器&#xff0c;Chrome 自身账号以及各个网站账号都需要重新登录&#xff0c;电脑本身为家用&#xff0c;使用时间不多的情况下&#xff0c;频繁登录账号很痛苦&#xff0c;也很迷惑。现找到解决办法&#xff0c;记录…

基于SSM大学生竞赛活动平台

基于SSM大学生竞赛活动平台的设计与实现 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringSpringMVCMyBatisVue 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 【主要功能】 前台系统主要功能实现&#xff1a;首页列表展示、自愿者招聘、竞赛新闻查…

汇编语言基础

引言 汇编语言是直接在硬件之上工作的编程语言&#xff0c;首先要了解硬件系统的结构&#xff0c;才能有效的应用汇编语言对其编程。汇编课程的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作。 基础知识 1.1机器语言 机器语言是机器指令的集合…

Stm32_标准库_13_串口蓝牙模块_手机与蓝牙模块通信

代码&#xff1a; #include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Serial.h"char News[100] "";uint8_t flag 1;void Get_Hc05News(char *a){uint32_t i 0…

设计模式之是简单工厂模式

分类 设计模式一般分为三大类&#xff1a;创建型模式、结构型模式、行为型模式。 创建型模式&#xff1a;用于创建对象&#xff0c;共五种&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。结构型模式&#xff1a;用于处理类或对…