当前位置: 首页 > news >正文

Linux网络编程 多进程UDP聊天室:共享内存与多进程间通信实战解析

知识点1【项目功能介绍】

今天我们写一个 UDP多进程不同进程间通信的综合练习

我这里说一下 这个项目的功能:

1、群发(有设备个数的限制):发送数据,其他所有客户端都要受到数据

2、其他客户端 可以向本机发送消息

3、私发:私发的格式为 /IP:端口号,数据

知识点2【项目实现思路】

1、首先最基本的UDP的步骤

创建套接字→绑定套接字→操作→关闭套接字

2、创建两个子进程,一个子进程负责收数据,另一个子进程负责发数据

3、由于子进程之间 都需要 所连接的设备的 IP 和 端口号,但是子进程之间的空间又是独立的,因此我们需要用共享内存的方式,而共享内存共享的则是一个地址结构体数组

1、共享结构体数组

    char shm_name[32] = "./shm_file";int fd_shm = open(shm_name,O_CREAT | O_RDWR,0666);if(fd_shm < 0){perror("open");_exit(-1);}int shm_size = sizeof(struct sockaddr_in) * NUM_DEVICE;//2、设置共享内存大小ftruncate(fd_shm,shm_size);//3、内存映射struct sockaddr_in * sockaddr_shm = (struct sockaddr_in *)mmap(NULL,shm_size,PROT_READ | PROT_WRITE, MAP_SHARED, fd_shm, 0);if(sockaddr_shm  == MAP_FAILED){perror("mmap");_exit(-1);}bzero(sockaddr_shm,shm_size);

1、首先打开文件(可读可写,创建)

2、由于文件打开,大小为0,我们需要进行扩容

ftruncate();

3、内存映射mmap

NULL(系统自动寻找内存空间),空间大小,文件权限,共享,要映射的文件,偏移量

4、清空内存

bzero();

2、UDP常规流程

创建套接字 和 绑定

		//套接字创建int fd_sock = socket(AF_INET,SOCK_DGRAM,0);if(fd_sock < 0){perror("socket");_exit(-1);}//绑定struct sockaddr_in addr_src;addr_src.sin_family = AF_INET;addr_src.sin_port = htons(8000);addr_src.sin_addr.s_addr = htonl(INADDR_ANY); int ret_bind = bind(fd_sock,(struct sockaddr *)&addr_src,sizeof(addr_src));if(ret_bind < 0){perror("bind");_exit(-1);}

不多作介绍

3、创建子进程的模式

以创建两个为例

    //创建子进程size_t i = 0;for (; i < 2; i++){int pid = fork();if(pid < 0){perror("fork");_exit(-1);}if(pid == 0){break;}}if(i == 0)//子进程1{}else if(i == 1)//子进程2{}

不多作介绍

4、收数据

    if(i == 0)//进程1,负责收数据{while(1){char buf[500] = "";int len = sizeof(struct sockaddr_in);struct sockaddr_in buf_recv;int ret_recv = recvfrom(fd_sock,buf,sizeof(buf),0,(struct sockaddr *)&buf_recv,&len);if(ret_recv < 0){perror("recvfrom");_exit(-1);}//查看该IP和端口是否存在int exists = 0;for (size_t j = 0; j < NUM_DEVICE; j++){if(buf_recv.sin_addr.s_addr == sockaddr_shm[j].sin_addr.s_addr && buf_recv.sin_port == sockaddr_shm[j].sin_port){exists = 1;break;}}if(exists != 1)//不存在,存入第一个空的地址结构体{size_t k = 0;for (; k < NUM_DEVICE; k++){if(ntohs(sockaddr_shm[k].sin_port) == 0){memcpy(&sockaddr_shm[k],&buf_recv,sizeof(buf_recv));//代码书写过程中,这里出现错误 break;}}if(k == NUM_DEVICE){printf("\\r群聊已满,无法加入\\n");continue;}}//遍历收到的信息char buf_IP[16] = "";inet_ntop(AF_INET,&buf_recv.sin_addr.s_addr,buf_IP,sizeof(buf_IP));int port = ntohs(buf_recv.sin_port);printf("\\r收到IP:%s,端口号:%d的信息为:%s\\n",buf_IP,port,buf);printf("\\r请输入数据(提示/起始可指定IP发送):");fflush(stdout);}_exit(-1);}

思路讲解

1、首先接收数据

2、判断数据来源客户端,是否存在,如果不存在,则存在 结构体数组的 最小有效的下标 中,当数组存满后,需要提醒一下,但是不会退出,已经连接的设备仍然可以发送/接收数据,因此需要用continue而不是break

这里我要说我写的过程中的一个错误,希望大家以我为诫,别犯类似错误

我在下面代码中,数组下标忘记写了&sockaddr_shm[k]→sockaddr_shm,导致我永远只能给一台设备发送数据

for (; k < NUM_DEVICE; k++)
{if(ntohs(sockaddr_shm[k].sin_port) == 0){memcpy(&sockaddr_shm[k],&buf_recv,sizeof(buf_recv));//代码书写过程中,这里出现错误 break;}
}

3、遍历收到的数据

5、发数据

else if(i == 1){   while(1){printf("\\r请输入数据(提示/起始可指定IP发送):");fflush(stdout);char buf[256] = "";fgets(buf,sizeof(buf),stdin);buf[strlen(buf) - 1] = 0;if(buf[0] == '/'){char buf_ip[16] = "";int int_port = 0;char data[256] = "";sscanf(buf,"/%[^:]:%4d,%s",buf_ip,&int_port,data);int int_ip = 0;//转为网络字节序inet_pton(AF_INET,buf_ip,&int_ip);int_port = htons(int_port);//定义一个标志位,判断输入的端口是不是存在int flag = 0;size_t k = 0;for (; k < NUM_DEVICE; k++){if(sockaddr_shm[k].sin_addr.s_addr == int_ip && sockaddr_shm[k].sin_port == int_port){flag = 1;break;}}if(flag == 1){//私发sendto(fd_sock,data,sizeof(data),0,(struct sockaddr *)&sockaddr_shm[k],sizeof(struct sockaddr_in));}else{//不存在该端口,打印提示内容,输出现有的所有IP和端口printf("指令错误,请按照下面的model输入\\n");printf("mode:/192.168.6.3:9000,data\\n");if(sockaddr_shm[0].sin_addr.s_addr != 0);{printf("以下是已经连接的端口\\n");for (size_t i = 0; i < NUM_DEVICE; i++){if(sockaddr_shm[i].sin_port != 0){inet_ntop(AF_INET,&sockaddr_shm[i].sin_addr.s_addr,buf_ip,sizeof(buf_ip));int_ip = ntohs(sockaddr_shm->sin_port);printf("%s:%d\\n",buf_ip,int_ip);}}}}}else//群发{for (size_t j = 0; j < NUM_DEVICE ;j++){if(sockaddr_shm[j].sin_port != 0){sendto(fd_sock,buf,sizeof(buf),0,(struct sockaddr *)&sockaddr_shm[j],sizeof(struct sockaddr_in));}}}}}

思路讲解

1、观察格式,我们发现私发格式 第一个字母必须要求是/开头,我们用这个作为进行判断

2、首先需要判断输入的端口 和 IP地址是否合法

3、如果合法,进行发送,不合法则需要 遍历提示内容,如果已经有设备的连接,需要遍历出可以通信的IP

注意

这一步比较复杂的是数据类型(网络字节序与主机字节序的转换,与点分法十进制串与 网络整形IP 的转换)

6、父进程负责回收空间

    else{while(1){int ret_wait = waitpid(-1,NULL,WNOHANG);if(ret_wait < 0){break;}}close(fd_shm);close(fd_sock);munmap(shm_name,shm_size);remove(shm_name);}

需要回收的空间介绍

1、共享内存时 打开的共享内存文件描述符

2、套接字

3、映射关系

4、映射文件删除

知识点2【整体代码演示】

//项目介绍 实现多人聊天室
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */
#include <strings.h>
#include <string.h>
#include <stdlib.h>#define NUM_DEVICE 10int main(int argc, char const *argv[])
{//父进程负责管理子进程的内存,子进程1负责收,子进程2 负责发//收的流程,创建sock,绑定端口,收,关闭端口//发的流程,创建sock,绑定端口,发,变比端口//由于需要子进程之间需要共享 结构体数组数据,这里需要利用到共享内存//1、创建共享内存,并计算大小char shm_name[32] = "./shm_file";int fd_shm = open(shm_name,O_CREAT | O_RDWR,0666);if(fd_shm < 0){perror("open");_exit(-1);}int shm_size = sizeof(struct sockaddr_in) * NUM_DEVICE;//2、设置共享内存大小ftruncate(fd_shm,shm_size);//3、内存映射struct sockaddr_in * sockaddr_shm = (struct sockaddr_in *)mmap(NULL,shm_size,PROT_READ | PROT_WRITE, MAP_SHARED, fd_shm, 0);if(sockaddr_shm  == MAP_FAILED){perror("mmap");_exit(-1);}bzero(sockaddr_shm,shm_size);//套接字创建int fd_sock = socket(AF_INET,SOCK_DGRAM,0);if(fd_sock < 0){perror("socket");_exit(-1);}//绑定struct sockaddr_in addr_src;addr_src.sin_family = AF_INET;addr_src.sin_port = htons(8000);addr_src.sin_addr.s_addr = htonl(INADDR_ANY); int ret_bind = bind(fd_sock,(struct sockaddr *)&addr_src,sizeof(addr_src));if(ret_bind < 0){perror("bind");_exit(-1);}//创建子进程size_t i = 0;for (; i < 2; i++){int pid = fork();if(pid < 0){perror("fork");_exit(-1);}if(pid == 0){break;}}//子进程1 负责收if(i == 0){while(1){char buf[500] = "";int len = sizeof(struct sockaddr_in);struct sockaddr_in buf_recv;int ret_recv = recvfrom(fd_sock,buf,sizeof(buf),0,(struct sockaddr *)&buf_recv,&len);if(ret_recv < 0){perror("recvfrom");_exit(-1);}//查看该IP和端口是否存在int exists = 0;for (size_t j = 0; j < NUM_DEVICE; j++){if(buf_recv.sin_addr.s_addr == sockaddr_shm[j].sin_addr.s_addr && buf_recv.sin_port == sockaddr_shm[j].sin_port){exists = 1;break;}}if(exists != 1)//不存在,存入第一个空的地址结构体{size_t k = 0;for (; k < NUM_DEVICE; k++){if(ntohs(sockaddr_shm[k].sin_port) == 0){memcpy(&sockaddr_shm[k],&buf_recv,sizeof(buf_recv));//代码书写过程中,这里出现错误 break;}}if(k == NUM_DEVICE){printf("\\r群聊已满,无法加入\\n");continue;}}//遍历收到的信息char buf_IP[16] = "";inet_ntop(AF_INET,&buf_recv.sin_addr.s_addr,buf_IP,sizeof(buf_IP));int port = ntohs(buf_recv.sin_port);printf("\\r收到IP:%s,端口号:%d的信息为:%s\\n",buf_IP,port,buf);printf("\\r请输入数据(提示/起始可指定IP发送):");fflush(stdout);}_exit(-1);}//子进程2 负责发,这里设置,如果发送收到bye,Bye退出//实现发送消息,实际上是给多人发送,使用 结构体数组,存储多人的信息else if(i == 1){   while(1){printf("\\r请输入数据(提示/起始可指定IP发送):");fflush(stdout);char buf[256] = "";fgets(buf,sizeof(buf),stdin);buf[strlen(buf) - 1] = 0;if(buf[0] == '/'){char buf_ip[16] = "";int int_port = 0;char data[256] = "";sscanf(buf,"/%[^:]:%4d,%s",buf_ip,&int_port,data);int int_ip = 0;//转为网络字节序inet_pton(AF_INET,buf_ip,&int_ip);int_port = htons(int_port);//定义一个标志位,判断输入的端口是不是存在int flag = 0;size_t k = 0;for (; k < NUM_DEVICE; k++){if(sockaddr_shm[k].sin_addr.s_addr == int_ip && sockaddr_shm[k].sin_port == int_port){flag = 1;break;}}if(flag == 1){//私发sendto(fd_sock,data,sizeof(data),0,(struct sockaddr *)&sockaddr_shm[k],sizeof(struct sockaddr_in));}else{//不存在该端口,打印提示内容,输出现有的所有IP和端口printf("指令错误,请按照下面的model输入\\n");printf("mode:/192.168.6.3:9000,data\\n");if(sockaddr_shm[0].sin_addr.s_addr != 0);{printf("以下是已经连接的端口\\n");for (size_t i = 0; i < NUM_DEVICE; i++){if(sockaddr_shm[i].sin_port != 0){inet_ntop(AF_INET,&sockaddr_shm[i].sin_addr.s_addr,buf_ip,sizeof(buf_ip));int_ip = ntohs(sockaddr_shm->sin_port);printf("%s:%d\\n",buf_ip,int_ip);}}}}}else{for (size_t j = 0; j < NUM_DEVICE ;j++){if(sockaddr_shm[j].sin_port != 0){sendto(fd_sock,buf,sizeof(buf),0,(struct sockaddr *)&sockaddr_shm[j],sizeof(struct sockaddr_in));}}}}}//父进程回收子进程else{while(1){int ret_wait = waitpid(-1,NULL,WNOHANG);if(ret_wait < 0){break;}}close(fd_shm);close(fd_sock);munmap(shm_name,shm_size);remove(shm_name);}return 0;
}

代码运行结果

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!

相关文章:

  • 反转字符串
  • 【数据结构入门训练DAY-19】总结数据结构中的栈
  • OkHttp入门
  • python——函数
  • EMQX学习笔记
  • CCF-GESP认证的学习资源与知识点详细指南
  • (mamba_ssm)安装踩坑指南
  • 远程桌面-文件传输
  • ECMAScript
  • STM32——相关软件安装
  • GPU虚拟化实现(一)
  • 矩阵-螺旋矩阵
  • Kafka 如何理解Kafka的高可用
  • 从本地存档到协作开发的Git简单使用
  • 解决go项目构建后不能夸Linux平台的问题
  • 使用IntersectionObserver实现目标元素可见度的交互
  • Franka机器人ROS 2来袭:解锁机器人多元应用新可能
  • [密码学基础]商用密码应用安全性评估(密评):网络安全新风口,高薪紧缺人才必备技能
  • 本地搭建一个简易版本的 Web3 服务
  • 2025年世界职业院校技能大赛实施方案(意见稿)
  • 中越海警开展2025年第一次北部湾联合巡逻
  • 大气科学家、北京大学副教授李成才逝世,终年56岁
  • 从 “负分” 到世界杯亚军,蒯曼专打“逆风局”
  • 海拔四百公里的救赎
  • 观察丨微短剧盛行“拿来主义”,版权保护迫在眉睫
  • 上海推出平台算法治理合规指引:不得“静默推荐”,算法应用向上向善