消息队列
- 消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。
- 每个数据块都被认为是一个类型,接收进程可以独立地接收含有不同类型的数据结构。
- 我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度
特点:
- 全双工,双向通信
- 面向数据报
- 生命周期随内核,直到显示删除或者系统重启
消息队列函数
1、msgget函数
功能:该函数用来创建和访问一个消息队列。
原型:int msgget(key_t key, int msgflg);
参数:
key:某个消息队列的名字
msgflg:权限标志,表示消息队列的访问权限。由9个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。
返回值:成功返回一个非负整数,即该消息队列的标识码;失败返回-1
2、msgctl函数
功能:该函数用来控制消息队列
原型:int msgctl(int msgid, int cmd, struct msgid_ds *buf);
参数:
msgid:由msgget函数返回的消息队列标识码
cmd:是将要采取的动作,它可以取以下3个值:
IPC_ STAT:把msgid_ ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_ SET:在进程有足够权限的前提下,把消息队列的当前关联值设置为msgid_ds数据结构中给出的值
IPC_RMID:删除消息队列
buf:是指向msgid_ds的结构体指针,它指向消息队列模式和访问权限的结构。
msgid_ds结构至少包括以下成员:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
返回值:成功返回0;失败返回-1
3、msgsnd函数
功能:该函数用来把一条消息添加到消息队列中
原型:int msgsend(int msgid, const void *msgp, size_t msgsz, int msgflg);
参数:
msgid:是由msgget函数返回的消息队列标识码
msgp:是一个指向准备发送消息的结构体指针
msgsz:是msgp指向的消息长度,这个长度不含保存消息类型的那个long int长整型
msgflg:控制着当前消息队列满或到达系统上限时将要发生的事
msgflg=IPC_NOWAIT表示队列满不等待,返回EAGAIN错误
返回值:成功返回0;失败返回-1
说明:
1.消息结构体在两方面受到约束:
首先,它必须小于系统规定的上限值
其次,它必须以一个long int长整型成员变量开始,接收者函数将用这个成员变量来确定消息的类型2.消息结构参考形式如下:
struct msgbuf{
long mtype;
char mtext[1024];
};
4、msgrcv函数
功能:该函数用来从一个消息队列获取消息
原型:int msgrcv(int msgid, void *msgp, size_t msgsz, long msgtype, int msgflg);
参数:
msgid:是由msgget函数返回的消息队列标识码
msgp:是一个指向准备发送消息的结构体指针
msgsz:是msgp指向的消息长度,这个长度不含保存消息类型的那个long int长整型
msgtype:可以实现一种简单的接收优先级
msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
返回值:成功返回实际放到接收缓冲区里去的字符个数;失败返回-1
- 说明:
如果msgtype = 0,返回队列中的第一条消息
如果msgtype > 0,返回队列第一条类型等于msgtype的消息
如果msgtype < 0,返回队列第一条类型小于等于msgtype的绝对值的消息,并且是满足条件的消息类型最小的消息
msgflg = IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误
msgflg = MSG_NOERROR,消息大小超过msgsz时被截断
msgtype > 0且msgflg = MSG_EXCEPT,接收类型不等于msgtype的第一条消息
C/S模型
C/S结构即Client/Server (客户端/服务器) 结构,是软件系统体系结构,通过将任务合理分配到Client端和Server端,降低了系统的通讯开销,需要安装客户端才可进行管理操作。
客户端和服务器端的程序不同,用户的程序主要在客户端,服务器端主要提供数据管理、数据共享、数据及系统维护和并发控制等,客户端程序主要完成用户的具体的业务。
开发比较容易,操作简便,但应用程序的升级和客户端程序的维护较为困难。
消息队列实现进程间通信
common.c
#include "common.h"
static int MsgCommon(int flags){
//若两个文件的ftok参数相同,则key一定相同
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0){
perror("ftok error");
return -1;
}
int msgid = msgget(key, flags);
if(msgid < 0){
perror("msgget error");
return -1;
}
return msgid;
}
int MsgCreate(){
//IP_CREAT:不存在就创建,存在就打开
//IPC_EXCL:搭配IP_CREAT使用,若消息队列存在,就打开失败
//0666:指定权限
return MsgCommon(IPC_CREAT | IPC_EXCL | 0666);
}
int MsgOpen(){
return MsgCommon(IPC_CREAT);
}
int MsgDestory(int msgid){
int ret = msgctl(msgid, IPC_RMID, NULL);
if(ret < 0){
perror("msgctl error");
return -1;
}
return 0;
}
int MsgSend(int msgid, int type, char* buf, size_t size){
Msgbuf msgbuf;
msgbuf.mtype = type;
//按照这种方式,mtext一定留有空间保存\0
if(size >= sizeof(msgbuf.mtext)/sizeof(msgbuf.mtext[0])){
printf("input buf size is too long\n");
return -1;
}
strcpy(msgbuf.mtext, buf);
int ret = msgsnd(msgid, &msgbuf, sizeof(msgbuf.mtext), 0);
if(ret < 0){
perror("msgsnd error");
return -1;
}
return 0;
}
int MsgRecv(int msgid, int type, char* buf, size_t max_size){
Msgbuf msgbuf;
int ret = msgrcv(msgid, &msgbuf, sizeof(msgbuf.mtext), type, 0);
if(ret < 0){
printf("msgrcv error");
return -1;
}
//此处我们的判定依据是根据 MsgSend 函数类的
//由于 MsgSend 函数能够保证一定在 mtext 中存在 \0
//所以此处我们不必给输出缓冲区再预留一个 \0 的空间
if(max_size < sizeof(msgbuf.mtext)){
printf("output buf size too small");
return -1;
}
strcpy(buf, msgbuf.mtext);
return 0;
}
/////////////////////////////////////////////////////////////////////////////
//以下是测试代码
/////////////////////////////////////////////////////////////////////////////
#if 0
void TestCreate(){
int msgid = MsgCreate();
printf("msgid = %d\n", msgid);
}
void TestOpen(){
int msgid = MsgOpen();
printf("msgid = %d\n", msgid);
}
void TestDestory(){
int msgid = MsgOpen();
int ret = MsgDestory(msgid);
printf("ret = %d\n", ret);
}
void TestSend(){
int msgid = MsgCreate();
char* msg = "hehe\n";
int ret = MsgSend(msgid, 1, msg, strlen(msg)); //此处用strlen的好处是只取内容,而不用将整个数组取出
printf("ret = %d\n", ret);
}
void TestRecv(){
int msgid = MsgOpen();
char buf[1024] = {0};
int ret = MsgRecv(msgid, 1, buf, sizeof(buf));
printf("ret = %d\n", ret);
printf("buf = %s\n", buf);
}
int main(){
//TestCreate();
//TestOpen();
//TestDestory();
//TestSend();
TestRecv();
return 0;
}
#endif
创建之后:
销毁之后:
发送数据之后:
接收数据之后:
client.c
#include <stdio.h>
#include <stddef.h>
#include "common.h"
int main(){
int msgid = MsgOpen();
while(1){
char buf[1024] = {0};
printf("Please Enter:");
fflush(stdout);
//从标准输入读数据
ssize_t read_size = read(0, buf, sizeof(buf)-1);
if(read_size < 0){
perror("read error");
return 1;
}
if(read_size == 0){
printf("read done!\n");
return 0;
}
buf[read_size] = '\0';
//2.把数据发送给服务器
MsgSend(msgid, CLIENT_TYPE, buf, strlen(buf));
printf("send done, wait recv...\n");
//3.读取服务器返回结果
char output_buf[1024] = {0};
MsgRecv(msgid, SERVER_TYPE, output_buf, sizeof(output_buf));
//4.将结果打印在标准输出上
printf("server# %s\n", output_buf);
}
return 0;
}
server.c
#include <stdio.h>
#include <stddef.h>
#include "common.h"
int main(){
int msgid = MsgCreate();
while(1){
char buf[1024] = {0};
//1.从客户端接收数据
MsgRecv(msgid, CLIENT_TYPE, buf, sizeof(buf));
//2.读取接收的数据
printf("client# %s\n", buf);
//3.服务器发送数据给客户端
char input_buf[1024] = {0};
printf("Please Enter:");
fflush(stdout);
ssize_t read_size = read(0, input_buf, sizeof(input_buf)-1);
if(read_size < 0){
perror("read error");
return 1;
}
if(read_size == 0){
printf("read done!\n");
return 0;
}
input_buf[read_size] = '\0';
MsgSend(msgid, SERVER_TYPE, input_buf, strlen(input_buf));
printf("send done, wait recv...\n");
}
}
具体代码请参考Git
消息队列与命名管道比较
消息队列跟命名管道有不少的相同之处,与命名管道一样,消息队列进行通信的进程可以是任何进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read;而在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
与命名管道相比,消息队列的优势在于:
- 消息队列可以独立于发送和接收进程而存在,从而解决单个命名管道只能单向通信的缺点
- 通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
- 接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。