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

知识点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;
}

代码运行结果

结束

代码重在练习!

代码重在练习!

代码重在练习!

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

相关推荐
꧁༺朝花夕逝༻꧂几秒前
随机面试--<二>
linux·运维·数据库·nginx·面试
开***能9 分钟前
EthernetiP转modbusTCP网关在加氢催化中的应用
linux·服务器·网络
UFIT15 分钟前
系统安全及应用
linux·运维
又过一个秋30 分钟前
【sylar-webserver】重构日志系统
linux·c++·算法·重构
JhonKI1 小时前
【Linux网络】构建UDP服务器与字典翻译系统
linux·服务器·网络·tcp/ip·udp
伤不起bb1 小时前
系统安全及应用
linux·运维·网络·安全·系统安全
啊吧怪不啊吧2 小时前
Linux常见指令介绍中(入门级)
linux·运维·服务器
鹏大师运维2 小时前
用银河麒麟 LiveCD 快速查看原系统 IP 和打印机配置
linux·ip地址·国产化·国产操作系统·打印机·统信uos·livecd
楠奕2 小时前
neo4j-community-3.5.5-unix.tar.gz安装
linux·服务器·数据库
Rudon滨海渔村2 小时前
Linux通用一键换源脚本.sh - ubuntu、centos全自动更换国内源 - LinuxMirrors神器
linux·运维·ubuntu·centos·换源