【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析

hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之项目实战——仿QQ聊天程序源码剖析,在这篇文章中,你将会学习到如何利用Linux网络编程技术来实现一个简单的聊天程序,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!

希望这篇文章能对你有所帮助9fe07955741149f3aabeb4f503cab15a.png,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!1a2b6b564fe64bee9090c1ca15a449e3.png(注:这章对于高性能服务器的架构非常重要哟!!!)

03d6d5d7168e4ccb946ff0532d6eb8b9.gif         

目录

一.项目介绍

二.服务器代码剖析

2.1 头文件和相关数据声明

2.2 服务器连接准备代码

2.3 服务器处理逻辑代码

2.3 客户端代码剖析


 

一.项目介绍

      像ssh这样的登录服务通常要同时处理网络连接和用户输入,这也可以使用I/O复用来实现。我们以poll为例实现一个简单的聊天室程序,以阐述如何使用I/O 复用技术来同时处理网络连接和用户输入。该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两个部分。其中客户端程序有两个功能:一是从标准输入终端读入用户数据,并将用户数据发送至服务器;二是往标准输出终端打印服务器发送给它的数据。服务器的功能是接收,客户数据,并把客户数据发送给每一个登录到该服务器上的客户端(数据发送者除外)。下面我们依次给出客户端程序和服务器程序的代码。

二.服务器代码剖析

2.1 头文件和相关数据声明

#define _GNU_SOURCE 1
#include<t_stdio.h>
#include<t_file.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/poll.h>
#include<fcntl.h>
#include<errno.h>
#define user_limit 5 //最大客户连接数量
#define buffer_size 64
#define fd_limit 65535 //最大文件描述符数量struct client_data{//创建一个客户地址结构体struct sockaddr_in address ; char * write_buf;char buf[buffer_size];
};
int setnonblocking (int fd){//将文件描述符改为非阻塞模式int old_option = fcntl(fd , F_GETFL);int new_option = old_option | O_NONBLOCK;// 添加非阻塞选项fcntl(fd , F_SETFL , new_option);//设置return old_option;}

这部分代码包含了头文件,定义了一些宏,以及一个用于存储客户端数据的结构体,这个结构体是为了服务器更好控制来自客户端的socket套接字,以及灵活控制对socket套接字的读写,然后还定义了setnonblocking()函数来将传入的文件描述符利用fcntl函数改为非阻塞模式,方便服务器进行监听。

2.2 服务器连接准备代码

这段代码是服务器端程序的入口和初始化部分。下面是逐行的解释:

int main(int argc , char *argv[]){if(argc <= 2)//如果参数太少{printf("usage :%s ip_address port_number\n",basename(argv[0]));return 1;}

这段代码检查命令行参数的数量。如果参数少于两个(程序名称和IP地址/端口号),则打印使用说明并退出程序。

    const char * ip = argv[1] ;// 提取ip地址int port = atoi(argv[2]); //提取端口号

从命令行参数中提取服务器的IP地址和端口号。

    struct sockaddr_in address ; //服务器地址bzero(&address ,sizeof(address));//清空address.sin_family = AF_INET;inet_pton(AF_INET , ip ,&address.sin_addr);//设置ipaddress.sin_port = htons(port); //设置端口号

这里创建了一个sockaddr_in结构体来存储服务器的地址信息,并使用bzero函数将其清零。然后设置地址族为AF_INET(IPv4),使用inet_pton函数将点分十进制的IP地址转换为网络字节序的格式,并存储在sin_addr字段中。最后,将端口号从主机字节序转换为网络字节序并存储在sin_port字段中。

    int listenfd = socket(PF_INET ,SOCK_STREAM , 0);//创建监听套接字assert(listenfd >=0);

创建一个TCP套接字(SOCK_STREAM)用于监听客户端连接,并检查套接字是否创建成功。

    int ret = bind(listenfd , (struct sockaddr*)&address , sizeof(address));//绑定assert(ret !=-1);

将套接字绑定到之前设置的服务器地址上,并检查绑定操作是否成功。

    ret = listen(listenfd ,5);//最多同时监听五个assert(ret!=-1);

调用listen函数,使套接字进入监听状态,并设置最大同时连接数为5。然后检查监听操作是否成功。

    //创建user数组,放入多个客户对象,并且使用socket的值可以直接用来索引(作为数组下标)连接对应的client_data对象struct client_data * user = malloc(fd_limit * sizeof(struct client_data));//为了提高poll性能,限制用户数量struct pollfd *fds = malloc(sizeof(struct pollfd) * 6);int user_counter = 0;//计算客户连接数量int i=0;for( i =  1 ; i<=user_limit ; ++i){//对每个fds数据初始化fds[i].fd = -1;fds[i].events =0;}

这段代码分配了两个数组:user数组用于存储客户端数据,fds数组用于poll函数。user数组的大小被设置为fd_limit,这是一个预定义的最大文件描述符数量。fds数组的大小被设置为6,这是因为服务器程序只监听一个套接字(listenfd),而其余的用于客户端连接。user_counter用于跟踪当前连接的客户端数量。fds数组的其余元素被初始化为-1,表示没有对应的文件描述符。

    //初始化怕poll中第一个数据:监听套接字fds[0].fd = listenfd;fds[0].events = POLLIN | POLLERR;fds[0].revents = 0;

最后,将监听套接字listenfd添加到fds数组中,并设置其监听的事件为可读事件(POLLIN)和错误事件(POLLERR)。revents字段用于poll函数返回时存储发生的事件,在这里初始化为0。

这段代码为服务器程序的后续操作设置了基础,包括套接字的创建和绑定,以及用于poll函数的数组的初始化。

2.3 服务器处理逻辑代码

这段代码是服务器程序的主循环,它使用poll系统调用来监控多个文件描述符(fds数组)的事件。这个循环会一直运行,直到遇到错误或者被显式地退出。

while(1){

这是一个无限循环,服务器程序将一直运行直到出现错误或者执行了退出循环的操作。

    ret = poll(fds , user_counter+1 , -1);//开始监听if(ret <0) {printf("poll failed..\n");break;}

在循环的顶部,调用poll函数来等待事件发生。fds数组包含了所有需要监控的文件描述符,user_counter+1表示总共有user_counter个客户端连接加上监听套接字listenfd-1表示poll函数将阻塞直到至少有一个文件描述符上有事件发生。如果poll调用失败(返回值小于0),则打印错误信息并退出循环。

    for ( i =0 ; i <user_counter+1;i++){//每次对整个fds数组进行遍历处理if(fds[i].fd==listenfd && (fds[i].revents & POLLIN)){//如果为第一个监听字符且发生可读事件时struct sockaddr_in client_address;//创建一个新客户套接字socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd ,(struct sockaddr*)&client_address ,&client_addrlength );//获取客户端套接字if(connfd<0){//连接错误printf("erron is:%dd\n");continue;}if(user_counter >=user_limit){//用户太多const char * info ="too many users\n";printf("%s\n",info);send(connfd , info ,strlen(info) , 0);//发送错误给客户端close(connfd);continue;}//对于新连接 ,我们要同时修改fds和users数组,user[connfd]即对应客户端数据user_counter++;//客户数量加一user[connfd].address = client_address;setnonblocking(connfd);//设置为非阻塞模式fds[user_counter].fd = connfd;//最新数据放入数组fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR;fds[user_counter].revents = 0;printf("comes a new user , now have %d user\n",user_counter);}// ... 其他事件处理逻辑 ...}

这个循环遍历fds数组中的每个文件描述符,检查它们是否有事件发生。对于每个事件,服务器程序执行相应的操作:

  1. 如果监听套接字(listenfd)上有新的连接请求(POLLIN事件),服务器接受新连接,并将新的文件描述符(connfd)添加到fds数组中。如果连接数超过限制(user_limit),服务器会发送一个错误消息并关闭新连接。

  2. 如果有任何文件描述符上有POLLERR事件,表示发生了错误,服务器会打印错误信息。

  3. 如果有任何已连接的套接字上有POLLIN事件,表示有数据可读,服务器会读取数据并打印。

  4. 如果有任何套接字上有POLLRDHUP事件,表示对方已经关闭了连接,服务器会关闭对应的连接并更新fds数组。

  5. 如果有任何套接字上有POLLOUT事件,表示可以写数据,服务器会发送数据(如果有数据要发送)。

在循环结束后,服务器程序会继续执行下一次循环,等待更多的连接和事件。

在服务器端代码中,poll函数用于监控多个文件描述符的事件。poll函数的返回值表示有多少个文件描述符发生了事件,而每个文件描述符的事件类型存储在revents字段中。下面是服务器端代码中使用poll函数监控的不同事件类型及其解释:

if(fds[i].fd==listenfd && (fds[i].revents & POLLIN)){// ... 接受连接逻辑 ...
}
else if(fds[i].revents & POLLERR){// ... 错误处理逻辑 ...
}
else if(fds[i].revents & POLLIN){// ... 读取数据逻辑 ...
}
else if(fds[i].revents & POLLRDHUP){// ... 关闭连接逻辑 ...
}
else if(fds[i].revents & POLLOUT){// ... 写数据逻辑 ...
}
  1. POLLIN: 这个事件表示文件描述符上有数据可读。对于服务器来说,这意味着有新的客户端连接请求或者已连接的客户端有数据发送过来。

  2. POLLERR: 这个事件表示文件描述符发生了错误。可能是网络错误,也可能是其他类型的错误。服务器需要检查并处理这些错误。

  3. POLLRDHUP: 这个事件表示文件描述符的读端已经被对方关闭。这通常发生在客户端突然断开连接的情况下。

  4. POLLOUT: 这个事件表示文件描述符的写端准备好了,可以写入数据。对于服务器来说,这意味着它可以向客户端发送数据。

服务器程序通过检查fds数组中每个文件描述符的revents字段,来确定发生了哪种事件,并相应地执行处理逻辑。如果没有任何事件发生,poll函数会阻塞,直到至少有一个文件描述符上有事件发生。服务器程序通过这种方式可以高效地处理多个客户端连接。

2.3 客户端代码剖析

这段代码是一个简单的客户端程序,用于连接到一个服务器,并通过标准输入和输出与服务器进行通信

#define _GNU_SOURCE 1
#include<t_stdio.h>
#include<t_file.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <sys/poll.h>
#include<fcntl.h>
#include<poll.h>
#define buffer_size 64 //缓冲区大小

这段代码包含了必要的头文件和宏定义。_GNU_SOURCE是一个宏,它用于启用一些GNU扩展,如splice系统调用。

int main(int argc , char * argv[])
{if(argc <= 2)//如果参数太少{printf("usage :%s ip_address port_number\n",basename(argv[0]));return 1;}

这段代码检查命令行参数的数量。如果参数少于两个(程序名称和IP地址/端口号),则打印使用说明并退出程序。

    const char * ip = argv[1] ;// 提取ip地址int port = atoi(argv[2]); //提取端口号

从命令行参数中提取服务器的IP地址和端口号。

    struct sockaddr_in server_address ; //服务器地址bzero(&server_address ,sizeof(server_address));//清空server_address.sin_family = AF_INET;inet_pton(AF_INET , ip ,&server_address.sin_addr);//设置ipserver_address.sin_port = htons(port); //设置端口号

创建一个sockaddr_in结构体来存储服务器的地址信息,并使用bzero函数将其清零。然后设置地址族为AF_INET(IPv4),使用inet_pton函数将点分十进制的IP地址转换为网络字节序的格式,并存储在sin_addr字段中。最后,将端口号从主机字节序转换为网络字节序并存储在sin_port字段中。

    int sockfd = socket(PF_INET , SOCK_STREAM , 0 );//创建本地套接字assert(socket >= 0 ); //判错if(connect(sockfd , (struct sockaddr *)&server_address , sizeof(server_address)) < 0){//连接失败的话printf("connection failed...\n");close(sockfd);return 1;}

创建一个TCP套接字(SOCK_STREAM)用于与服务器通信,并检查套接字是否创建成功。然后尝试连接到服务器。如果连接失败,打印错误信息并退出程序。

    struct pollfd fds[2];//创建pollfd结构类型数组,注册标准输入和sockfd文件描述符上的可读事件fds[0].fd = 0;fds[0].events = POLLIN ;//标准输入可读fds[0].revents = 0; //实际发生事件,由内核填充fds[1].fd = sockfd;fds[1].events = POLLIN | POLLRDHUP ;//标准输入可读fds[1].revents = 0; //实际发生事件,由内核填充

创建一个pollfd结构体数组,用于监控标准输入(0)和套接字(sockfd)上的可读事件。

    while (1){ret = poll(fds , 2 , -1); //最大被监听事件只有两个, 返回符合条件文件总数if(ret < 0){//如果监听发生错误printf("poll falied..\n");break;}

在循环的顶部,调用poll函数来等待事件发生。fds数组包含了所有需要监控的文件描述符,2表示总共有两个文件描述符(标准输入和套接字)。-1表示poll函数将阻塞直到至少有一个文件描述符上有事件发生。如果poll调用失败(返回值小于0),则打印错误信息并退出循环。

        if(fds[1].revents & POLLRDHUP){//假如发生了关闭对端连接printf("server close the connection..\n");break;}else if(fds[1].revents & POLLIN){//假如sockfd文件发生可读,则读取服务器传来数据memset(readbuf , '\0' , buffer_size);recv(fds[1].fd , readbuf , buffer_size -1 , 0);//接收数据if(ret <= 0){// 如果接收失败或对方关闭了连接printf("server close the connection..\n");break;}printf("%s\n",readbuf);//打印数据}

 if(fds[0].revents & POLLIN){//标准输入文件描述符可读,说明我们需要写入数据ret = splice(0 , NULL , pipefd[1] , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);//从标准输入写入数据到管道写端ret = splice(pipefd[0] , NULL , sockfd , NULL ,32768 , SPLICE_F_MORE | SPLICEret = splice(0 , NULL , pipefd[1] , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);//从标准输入写入数据到管道写端ret = splice(pipefd[0] , NULL , sockfd , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);//从管道读端将数据传输到sockfdprintf("ok");}}close(sockfd);return 0;
}

这段代码检查套接字(sockfd)上的事件。如果套接字上有POLLRDHUP事件,表示对方已经关闭了连接,服务器会关闭对应的连接并退出循环。如果套接字上有POLLIN事件,表示有数据可读,服务器会读取数据并打印。

这段代码是客户端程序主循环的最后一部分,它处理标准输入(0)上的数据,并通过管道(pipefd)将其传输到套接字(sockfd)上。

  1. ret = splice(0 , NULL , pipefd[1] , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);:

    • splice是一个系统调用,用于直接在内核空间复制数据,避免了用户空间和内核空间之间的数据拷贝。
    • 第一个参数是源文件描述符,这里是从标准输入0
    • 第二个参数是源文件描述符的偏移量,这里为NULL,表示从文件开始读取。
    • 第三个参数是目标文件描述符,这里是对应的管道写端pipefd[1]
    • 第四个参数是目标文件描述符的偏移量,这里为NULL,表示从文件开始写入。
    • 第五个参数是传输的数据量,这里为32768,是一个系统定义的常量,表示最多传输32768字节。
    • 第六个参数是SPLICE_F_MORE,表示这只是一个中间步骤,还有更多的数据要传输。
    • 第七个参数是SPLICE_F_MOVE,表示传输的数据是从内核缓冲区直接移动,而不是复制。
  2. ret = splice(pipefd[0] , NULL , sockfd , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);:

    • 类似地,这段代码使用splice系统调用来从管道读端pipefd[0]传输数据到套接字sockfd
  3. printf("ok");:

    • 打印"ok"表示数据传输成功。

循环继续执行,重复上述操作,直到连接被关闭或出现错误。

  1. close(sockfd);:

    • 关闭套接字sockfd,释放资源。
  2. return 0;:

    • 程序返回0,表示正常退出。

这个客户端程序通过poll系统调用来监控标准输入和套接字的事件,并通过splice系统调用来高效地传输数据。它使用管道作为中间缓冲区,以避免在用户空间和内核空间之间进行数据拷贝。

   好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦4d7d9707063b4d9c90ac2bca034b5705.png!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!2cd0d6ee4ef84605933ed7c04d71cfef.jpeg 

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

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

相关文章

远程控制安卓手机:便捷、高效与安全的方法

在移动设备的领域里&#xff0c;远程控制安卓手机的能力也变得越来越重要。这种技术可以让我们在远程地点方便地操作手机&#xff0c;无论是处理紧急事务、帮助他人解决问题&#xff0c;还是仅仅为了享受科技带来的便利。本文将为你介绍2种便捷、高效且安全的方法&#xff0c;让…

【智能算法】向日葵优化算法(SFO)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2019年&#xff0c;GF Gomes等人受到自然界向日葵运动行为启发&#xff0c;提出了向日葵优化算法&#xff08;Sunflower Optimization, SFO&#xff09;。 2.算法原理 2.1算法思想 SFO模拟向日葵行…

【服务器部署篇】Linux下Ansible安装和配置

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0c;产…

vue3【详解】vue3 比 vue2 升级了哪些重要的功能?

改用 createApp 初始化实例 vue2 使用 new Vue() 初始化实例 vue3 使用 Vue.createApp() 初始化实例 新增 emits 选项 vue3 选项式API中新增了emits 选项&#xff0c;用于显示声明组件中的自定义事件&#xff0c;自定义事件的名称&#xff0c;需用 on 开头。 export default {…

如何在vue3+vite中优雅的使用iconify图标

前言 从Vue2迁移到Vue3&#xff0c;在使用上有着很大的差别。本文的话主要是针对图标的使用差别上进行分析&#xff0c;同时给出基于iconify图标库中unplugin-icons的用法。这里特殊说明一下&#xff1a;其实element-plus中用到的图标也是基于iconify图标库的&#xff0c;在我们…

LT9611UXC双端口 MIPI DSI/CSI 转 HDMI2.0,带音频

1. 说明 LT9611UXC 是一款高性能 MIPI DSI/CSI 至 HDMI2.0 转换器。MIPI DSI/CSI 输入具有可配置的单端口或双端口&#xff0c;具有 1 个高速时钟通道和 1~4 个高速数据通道&#xff0c;工作速率最高为 2Gbps/通道&#xff0c;可支持高达 16Gbps 的总带宽。 LT9611UXC 支持突发…

13 c++版本的五子棋

前言 呵呵 这大概是 大学里面的 c 五子棋了吧 有一些 面向对象的理解, 但是不多 这里 具体的实现 就不赘述, 仅仅是 发一下代码 以及 具体的使用 然后 貌似 放在 win10 上面执行 还有一些问题, 渲染的, 应该很好调整 五子棋 #include<Windows.h> #include<io…

STM32、GD32驱动SHT30温湿度传感器源码分享

一、SHT30介绍 1、简介 SHT30是一种数字湿度和温度传感器&#xff0c;由Sensirion公司生产。它是基于物理蒸发原理的湿度传感器&#xff0c;具有高精度和长期稳定性。SHT30采用I2C数字接口&#xff0c;可以直接与微控制器或其他设备连接。该传感器具有低功耗和快速响应的特点…

树莓派4-通过IIC实现图片循环播放

一、环境 1、树莓派4&#xff1b; 2、串口连接电脑&#xff1b; 3、树莓派由杜邦线连接0.96寸OLED1306协议 4、树莓派能够联网&#xff0c;便于安装环境。离线情况也可以安装&#xff0c;相对麻烦&#xff1b; 二、目标 1、树莓派可以开启IIC并识别已连接的IIC&#xff1b; …

Web3解密:理解去中心化应用的核心原理

引言 在当前数字化时代&#xff0c;去中心化技术和应用正在逐渐引起人们的关注和兴趣。Web3技术作为去中心化应用&#xff08;DApps&#xff09;的基础&#xff0c;为我们提供了一个全新的互联网体验。但是&#xff0c;对于许多人来说&#xff0c;这个复杂的概念仍然充满了神秘…

MongoDB基础操作

文章目录 一、什么是MongoDB二、MongoDB 与关系型数据库对比三、数据类型四、部署MongoDB1、下载二进制包2、下载安装包并解压3、创建用于存放数据和日志的目录&#xff0c;并修改权限4、启动MongoDB4.1前台启动4.2后台启动4.3、配置文件启动服务4.4、配置systemd服务4.5、syst…

RabbitMQ发布确认和消息回退(6)

概念 发布确认原理 生产者将信道设置成 confirm 模式&#xff0c;一旦信道进入 confirm 模式&#xff0c;所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始)&#xff0c;一旦消息被投递到所有匹配的队列之后&#xff0c;broker就会发送一个确认给生产者(包含消…

qt实现方框调整

效果 在四周调整 代码 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QWidget>class MainWindow : public QWidget {Q_OBJECT public:explicit MainWindow(QWidget *parent 0);~MainWindow();void paintEvent(QPaintEvent *event);void updateRect();void re…

Restful API 具体设计规范(概述)

协议 https 域名 https://www.baidu.com/api 版本 https://www.baidu.com/v1 路径 https://www.baidu.com/v1/blogs 方法 数据过滤 状态码返回结果 返回的数据格式 尽量使用 JSON&#xff0c;避免使用 XML。 总结&#xff1a; 看 url 就知道要什么看 http method 就知道干…

Linux进阶篇:CentOS7搭建NFS文件共享服务

CentOS7搭建NFS文件共享服务 一、NFS介绍 NFS(Network File System)意为网络文件系统&#xff0c;它最大的功能就是可以通过网络&#xff0c;让不同的机器不同的操作系统可以共享彼此的文件。简单的讲就是可以挂载远程主机的共享目录到本地&#xff0c;就像操作本地磁盘一样&…

Docker——数据管理和网络通信

目录 一、Docker的数据管理 1.数据卷 2.数据卷容器 3.容器互联 二、Docker镜像的创建 1.基于现有镜像创建 2.基于本地模板创建 3.基于Dockerfile 创建 3.1联合文件系统&#xff08;UnionFS&#xff09; 3.2镜像加载原理 3.3为什么Docker里的Centos大小才200M 4.Dcok…

9【PS作图】像素画Tips

放大缩小 “窗口”-排列-为…画布新建窗口&#xff0c;就可以新建一个窗口&#xff0c;实时看作图效果 如果要保持放大或缩小的像素画仍然保持硬边缘&#xff0c;需要设置两个东西 将 编辑 > 首选项 > 常规 中的 插值方式 改为 “邻近&#xff08;靠近硬边缘&#xff09…

HTML网页自动播放背景音乐和全屏背景图代码

HTML网页自动播放背景音乐的代码 背景音乐代码及分析代码的应用背景图代码及分析下期更新预报 背景音乐代码及分析 能使网站上自动循环的背景音乐代码如下&#xff1a; <audio src"music.mid" autostart"true" loop"true" hidden"true…

【小梦C嘎嘎——启航篇】C++四大类型转换

&#x1f60e; 前言&#x1f64c;C四大类型转换什么是类型转换C语言中的类型转换为什么C要嫌弃C语言的类型转换&#xff1f;自行搞一套呢&#xff1f;C强制类型转换1、static_cast2、reinterpret_cast3、const_cast4、dynamic_cast为什么要支持向下转呢&#xff1f; RTTI 总结撒…

RabbitMQ工作模式(4) - 路由模式

概念 路由模式&#xff08;Routing&#xff09;是 RabbitMQ 中的一种消息传递模式&#xff0c;也称为直连模式。它允许生产者将消息发送到一个交换机&#xff0c;并指定一个或多个路由键&#xff08;routing key&#xff09;&#xff0c;交换机根据路由键将消息路由到与之匹配的…