一、引言
先来聊聊数据结构中的队列(如下图),队列具有先进先出的特点,其实就很像我们现实生活中做核酸、打疫苗排队一样。你去做核酸会先在队尾排,然后队列里面的人一直往前进,对头的人先做核酸,做完接着下一个,依次进行。那么IPC通信中的消息队列也类似这样。
二、消息队列概述
- 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
- 消息队列也有管道一样的不足,就是每个数据块的最大长度是有上限的,系统上全体队列的最大总长度也有一个上限
- 消息队列是消息的链表,之前我们学的STL容器本身的根本原理都是用链表,然后以此结合c++模板来实现STL容器。链表就包括下一个节点的指针和当前节点的数据。只不过这个消息队列的链表比较特殊,尾部入队,头部出队,有秩序的进行。
三、消息队列函数
1、msgget函数——创建或者访问
作用:用来创建或访问一个消息队列
原型:int msgget(key_t key, int msgflg);
参数说明:
- key: 某个消息队列的名字
- msgflg:由九个权限标志构成(读、写、执行——0777),它们的用法和创建文件时使用的mode模式标志是一样的
返回值:如果操作成功,msgget将返回一个非负整数,即该消息队列的标识码;如果失败,则返回“-1”
2、msgsnd函数——发送
作用:把一条消息添加到消息队列里去
原型:int msgsnd(int msgid,const void *msg_ptr, size_t msg_sz,int msgflg);
参数说明:
- msgid: 由msgget函数返回的消息队列标识码
- msg_ptr:是一个指针,指针指向准备发送的消息(实际上是结构体指针,如下图,但要自定义结构体名称,不能与其一样,mtype必须是大于0、long类型,mtext长度可以改)
- msg_sz:是msg_ptr指向的消息长度,这个长度不能保存消息类型的那个“long int”长整型计算在内
- msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情,一般不用,设置为0
返回值:操作成功,返回“0”,如果失败,则返回“-1”
3、msgrcv函数(阻塞式 )——接收
作用:是从一个消息队列里检索消息
原型:int msgrcv(int msgid, void *msg_ptr, size_t msgsz, long int msgtype,int msgflg);
参数说明:
- msgid: 由msgget函数返回的消息队列标识码
- msg_ptr:是一个指针,指针指向准备发送的消息
- msg_sz:是msg_ptr指向的消息长度,这个长度不能保存消息类型的那个“long int”长整型计算在内
- msgtype:它可以实现接收优先级的简单形式
- msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
返回值:操作成功,返回“0”,如果失败,则返回“-1”
注意:msg_ptr的返回类型都是void *,因为在设计函数的时候不知道程序员会使用什么样的数据类型,即不知道发送或接收什么样的数据类型。(以后自己设计函数如果不知道需要什么类型的数据,都可以设计成通用的void* )
四、示例
创建两个应用程序,利用消息队列实现回合制伪聊天——死循环结合两种消息类型
1、代码
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include<sys/msg.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<string.h>
typedef struct sendMsgBuf
{
long mtype;//必须long
char mtext[50];//长度可以改
}MSGBUF;
using namespace std;
int main()
{
int msgid = 0;
MSGBUF sendbuf;
msgid = msgget((key_t)1001,IPC_CREAT | 0777);//与共享内存类似
if (msgid == -1)
{
perror("msgget error");
}
//sendbuf.mtype = 1;//要在循环内,要有类型和输入数据
while (true)
{
sendbuf.mtype = 1;
cin >> sendbuf.mtext;
if ((msgsnd(msgid, &sendbuf, sizeof(sendbuf), 0)) == -1)
{
perror("msgsnd error");
}
else
{
cout << "发送成功" << endl;
bzero(&sendbuf, sizeof(sendbuf));
if ((msgrcv(msgid, &sendbuf, sizeof(sendbuf), 2, 0)) == -1)
{
perror("msgrcv error");
}
else
{
cout << "写端接收成功" << endl;
cout << "sendbuf.mtext = " << sendbuf.mtext << endl;
bzero(&sendbuf, sizeof(sendbuf));//要记得清空
}
}
}
return 0;
}
typedef struct sendMsgBuf
{
long mtype;//必须long
char mtext[50];//长度可以改
}MSGBUF;
using namespace std;
int main()
{
int msgid = 0;
MSGBUF recvbuf;
msgid = msgget((key_t)1001, IPC_CREAT | 0777);//与共享内存类似
if (msgid == -1)
{
perror("msgget error");
}
while (true)
{
if ((msgrcv(msgid, &recvbuf, sizeof(recvbuf), 1, 0)) == -1)
{
perror("msgrcv error");
}
else
{
cout << "接收成功" << endl;
cout << "recvbuf.mtext = " << recvbuf.mtext << endl;
//
bzero(&recvbuf, sizeof(recvbuf));
recvbuf.mtype = 2;
cin >> recvbuf.mtext;
if ((msgsnd(msgid, &recvbuf, sizeof(recvbuf), 0)) == -1)
{
perror("msgsnd error");
}
else
{
cout << "读端发送成功" << endl;
bzero(&recvbuf, sizeof(recvbuf));
}
}
}
return 0;
}
2、运行结果
3、注意事项
1、运行时可以先运行读端,因为msgget函数是如果存在队列就访问,不存在就创建。但是就算先运行读端,如果没有消息发送进来,也收不到消息(不能打印接收成功),所以可以判断msgrcv也是个阻塞函数。
2、消息队列有一个好处就是当你读完一条消息,系统会自动把消息清除一条,知道全部读完。(接收消息的函数中可能已经封装了队列元素清除操作)。所以通过ipcs查看消息队列一直都是0字节0消息,如下图
3、要在死循环里面给类型和输入数据,如果消息类型不对或者没在循环中给到,则会出现msgsnd error: Invalid argument报错,如下图所示
补充:1、管道和消息队列的区别
管道不能保存数据,消息队列能暂时保存 ,管道需要两端同时打开(一端关闭则导致管道破裂),消息队列中即使对方不存在,我也可以一直顺序写数据进行保存,
跟管道一样的不足是不能传递大量数据(小说、视频文件等),因为这样你就要开非常大的数组,就会把最大长度占满,导致不能开第二个消息队列,但可以传送简单数据 ,实现消息提醒功能2、共享内存和消息队列的区别
- 结构上,共享内存是一个普通内存区域,没有数据结构,需要用memcpy,会覆盖之前的值;消息队列就是一个链表容器,遵循先进先出原则,不会覆盖之前数据
- 消息队列存的( 链表有数据域 )结构体,不能随意设置,必须是固定的结构(long type) ,共享内存可以任意结构
- 消息队列大小有限制(根据所有当前系统消息队列最大长度计算),共享内存没有大小限制
- 所以把共享内存和消息队列结合才是完美
3、文件传输业务——上传下载
文件上传一般用在客户端把文件以网络的方式传输给服务器(下载相反),涉及socket通信,跟进程没关系,不能用消息队列,因为消息队列只能进行本地操作, 还不如IO流实现拷贝,没必要开两进程再用消息队列传输文件(杀鸡用牛刀)