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,防止端口误用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)、初始化能接受的活跃客户端套接字数组int client[MAX_LISTEN];for(int i = 0; i < MAX_LISTEN; i++){client[i] = -1; // 空的置为-1,活跃的置为对应的文件描述符}uint16_t port = 0; // 新的客户端端口号char ip[20] = {0}; // 新的客户端IPstruct 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(client[i] == -1){client[i] = new_client_fd;break;}}// 如果只有服务器的套接字发生变化,新的套接字没有发送数据// 那就继续监听,否则需要打印套接字的信息if(--num == 0){continue;}}}// 如果客服端发送数据过来for(int i = 0; i < MAX_LISTEN; i++){if(client[i] == -1){continue;}// 如果活跃的客户端有发送数据,注意这里要采用client_set,而不是active_set,否则会读取不了数据if(FD_ISSET(client[i], &client_set)){// 接收数据memset(recv_msg, 0, sizeof(recv_msg));ret = recv(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", ip, port);FD_CLR(client[i], &active_set); // 清空对应活跃集合的套接字client[i] = -1; // 清空客户端套接字数组// 需要重新更新活跃集合中最大的文件描述符maxfd = sockfd;for(int i = 0; i < MAX_LISTEN; i++){if(client[i] != -1 && maxfd < client[i]){maxfd = client[i];}}}else{printf("[%s:%d] send data: %s\n", ip, port, 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}; // 新的客户端IPstruct 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协议使用,但是不代表不行,案例给出了演示。
相关文章:

Linux网络编程系列之服务器编程——多路复用模型
一、什么是多路复用模型 服务器的多路复用模型指的是利用操作系统提供的多路复用机制,同时处理多个客户端连接请求的能力。在服务器端,常见的多路复用技术包括select、poll和epoll等。这些技术允许服务器同时监听多个客户端连接请求,当有请求…...
在SQL语句里使用正则表达式,因该怎么使用
在SQL中使用正则表达式通常需要使用特定的函数或运算符,具体的语法可能因不同的数据库系统而有所不同。以下是使用正则表达式的一般方法,但请注意,具体语法可能会因您使用的数据库而有所不同。 一般情况下,您可以使用以下方法在S…...

扫码登录-测试用例设计
扫码登录测试用例...

PyTorch CUDA GPU高占用测试
0x00 问题描述 安装完成PyTorch、CUDA后,验证PyTorch是否能够通过CUDA高占用GPU(占用>95%),特地使用以下代码测试。 0x01 代码设计 这个代码会持续执行神经网络的训练任务,每次循环都进行前向传播、反向传播和参数…...
Java|学习|abstract ,接口 Interface , Object
1.abstract 1.1 abstract abstract 是修饰符,表示抽象的,用来修饰抽象类和抽象方法。 abstract 修饰的类是抽象类,抽象类不能创建对象,主要用于被子类继承。 abstract 修饰的方法是抽象方法,该方法没有方法体&…...

安全的Sui Move是Web3大规模采用之路的基石
没有信任,就没有Web3的大规模采用。还有其他重要障碍阻碍了首个十亿用户的到来,包括令人困惑的用户体验、复杂的身份验证模式以及不确定的监管体系,但所有障碍中,要数大多数人对区块链技术持怀疑和不信任态度最严重。 对于许多人…...

Python中图像相似性度量方法汇总
1. 引言 在当前到处充满着图像的世界里,测量和量化图像之间的相似性已经成为一项关键的任务。无论是图像检索、内容推荐还是视觉搜索,图像相似性方法在现代计算机视觉的应用中都发挥着关键的作用。 幸运的是,Python提供了大量的工具和库&am…...

pycharm中快速对比两个.py文件
在学习一个算法的时候,就想着自己再敲一遍代码,结果最后出现了一个莫名其妙的错误,想跟源文件对比一下到底是在哪除了错,之前我都是大致定位一个一个对比,想起来matlab可以快速查找出两个脚本文件(.m文件)的区别&#…...
C++程序结束
在C程序任意位置结束程序需要return 0,如果只return的话会发生生成错误...
嵌入式学习-核心板、开发板和单片机
目录 核心板开发板单片机三者关系 核心板 核心板是一种电路板,它集成了微处理器、存储器和一些必要的接口电路。它通常用于嵌入式系统或物联网设备中,作为整个系统的核心组件。它的主要功能是将微处理器的指令和数据总线转换为各种外设的接口࿰…...

【pycharm】控制台报错:终端无法加载文件\venv\Scripts\activate.ps1
目录 一、在pycharm控制台输入 二、在windows的power shell (以管理员方式打开) 三、 在pycharm控制台输入 四、重新打开pycharm即可 前言:安装pycharm2022-03版本出现的终端打开报错 一、在pycharm控制台输入 get-executionpolicy …...

Python算术运算符:加减乘除 整除 取余 幂指数 小括号
运算案例 需求:用户手工输入梯形的上底、下底以及高,能直接通过Python打印出梯形的面积为多少。 做这个需求前,首先要知道Python的算数运算符有哪些。 2、算术运算符 所谓的算数运算符就是我们日常生活中的加减乘除等待。 运算符描述实例…...
访问者模式:对象结构的元素处理
欢迎来到设计模式系列的第十九篇文章,本篇将介绍访问者模式。访问者模式是一种行为型设计模式,它用于处理对象结构中不同类型的元素,而不需要修改这些元素的类。 什么是访问者模式? 访问者模式是一种将数据结构与数据操作分离的…...

ChatGPT快速入门
ChatGPT快速入门 一、什么是ChatGPT二、ChatGPT底层逻辑2.1 实现原理2.2 IO流程 三、ChatGPT应用场景3.1 知心好友3.2 文案助理3.3 创意助理3.4 角色扮演 一、什么是ChatGPT ChatGPT指的是基于GPT(Generative Pre-trained Transformer)模型的对话生成系…...
链表的实现(c语言)
链表分为单链表、双链表和循环链表,这些理论知识在笔记中自然写了,这里我只写出其中的实现: 单链表的实现 #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct Book {char bookname[20];char a…...

【Redis】渐进式遍历
scan命令渐进式遍历 Redis使⽤scan命令进⾏渐进式遍历键,进⽽解决直接使⽤keys获取键时可能出现的阻塞问题。每次scan命令的时间复杂度是O(1),但是要完整地完成所有键的遍历,需要执⾏多次scan。 SCAN 以渐进式的⽅式进⾏键的遍历。 SCAN…...

uni-app开发微信小程序的报错[渲染层错误]排查及解决
一、报错信息 [渲染层错误] Framework nner error (expect FLOW INITIALCREATION end but get FLOW CREATE-NODE) 二、原因分析及解决方案 第一种 原因:基础库版本的原因导致的。 解决: 1.修改调试基础库版本 2.详情—>本地设置—>调试基础库…...
三、C语言常用运算符
1、算术运算符 符号说明加号-减号*乘号/除号%取余符号,相除以后余数是几自增运算符,整数值增加 1--自减运算符,整数值减少1 2、关系运算符 符号说明检查两个操作数的值是否相等,如果相等则条件为真。!检查两个操作数的值是否相…...

ubuntu联网图标消失
sudo service NetworkManager stopsudo rm /var/lib/NetworkManager/NetworkManager.statesudo service NetworkManager start执行 sudo rm /var/lib/NetworkManager/NetworkManager.state 命令将删除位于 /var/lib/NetworkManager 目录下的 NetworkManager.state 文件。...
中华人民共和国网络安全法
中华人民共和国网络安全法 《中华人民共和国网络安全法》已由中华人民共和国第十二届全国人民代表大会常务委员会第二十四次会议于2016年11月7日通过,现予公布,自2017年6月1日起施行。2022年9月12日,国家互联网信息办公室发布关于公开征求《…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...