知识点1【项目功能介绍】
今天我们写一个 UDP ,多进程 与不同进程间通信的综合练习
我这里说一下 这个项目的功能:
1、群发(有设备个数的限制):发送数据,其他所有客户端都要受到数据
2、其他客户端 都 可以向本机发送消息
3、私发:私发的格式为 /IP:端口号,数据
知识点2【项目实现思路】
1、首先最基本的UDP的步骤
创建套接字→绑定套接字→操作→关闭套接字
2、创建两个子进程,一个子进程负责收数据,另一个子进程负责发数据
3、由于子进程之间 都需要 所连接的设备的 IP 和 端口号,但是子进程之间的空间又是独立的,因此我们需要用共享内存 的方式,而共享内存共享的则是一个地址结构体数组。
1、共享结构体数组
cpp
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常规流程
创建套接字 和 绑定
cpp
//套接字创建
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、创建子进程的模式
以创建两个为例
cpp
//创建子进程
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、收数据
cpp
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,导致我永远只能给一台设备发送数据
cpp
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、发数据
cpp
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、父进程负责回收空间
cpp
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【整体代码演示】
cpp
//项目介绍 实现多人聊天室
#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 10
int 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;
}
代码运行结果

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