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;
}
代码运行结果
结束
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!