一.消息队列
1.消息队列细节简述
关于消息队列的原理和接口我们之前已经详细介绍过,在这里我们强调一些在操作系统中消息队列的实际细节。
• 消息队列提供了⼀个从⼀个进程向另外⼀个进程发送有类型块数据的⽅法
• 每个数据块都被认为是有⼀个类型,接收者进程接收 的数据块可以有不同的类型值
• 消息队列也有管道⼀样的不⾜,就是每个消息的最⼤⻓度是有上限的(MSGMAX)
• 每个消息队列的总的字节数也是有上限的(MSGMNB),系统上消息队列的总数也有上限(MSGMNI)的
2.消息队列的通信方式
基于上回讲解的消息队列,我们对它的通信方式进行进一步详解。

由之前讲解的操作系统内核组织不同IPC资源,我们知道对于消息队列,也有自己的msqid_ds,并且可以通过其中的ipc_perm拿到标识消息队列的唯一标志key。
cpp
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg msg_first; / first message on queue,unused */
struct msg msg_last; / last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
}
struct ipc_perm {
key_t __key; /* Key supplied to xxxget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
}
消息队列在内核中的组织形式如下:

3.接口简述
我们在这里就不再对接口进行详细讲解,除了收发消息之外其余接口都和其他IPC接口类似。
cpp
NAME
msgrcv, msgsnd - System V message queue operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
• msgid : 由 msgget 函数返回的消息队列标识码
• msgp:是⼀个指针,指针指向准备发送的消息
• msgsz:是msgp指向的消息⻓度,这个⻓度不含 保存消息类型的那个long int⻓整型
• msgflg:控制着当前消息队列满或到达系统上限时将要发⽣⽣的事情, 0即可|---------------------------------------------|
| ( msgflg=IPC_NOWAIT 表⽰⽰队列满不等待,返回EAGAIN错误 )。 |
消息主体:struct msgbuf。其中mtype标识发送消息的主体是谁,mtext为消息正文,可以自定义长度。
cpp
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
}
关于msgflg:
msgtype=0返回队列第⼀条信息
msgtype>0返回队列第⼀条类型等于msgtype的消息
msgtype<0返回队列第⼀条类型⼩于等于msgtype绝对值的消息,并且是满⾜⾜条件的消息类型最⼩的消息
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。msgflg=MSG_NOERROR,消息⼤⼩超过msgsz时被截断
msgtype>0且msgflg=MSG_EXCEPT,接收类型不等于msgtype的第⼀条消息
二.责任链模式
1.什么时责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为设计模式,它允许你将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下一个处理者。
责任链模式的大致框架如下:
-
定义一个处理请求的抽象类或接口(Handler),其中包含一个指向下一个处理者的链接(next handler)和一个处理请求的方法。
-
具体处理者(Concrete Handler)实现处理请求的方法,并决定是否将请求沿着链传递。

责任链的优点在于可以对同一数据进行链式的处理,并且每个责任链结点的高内聚低耦合性较强,只要通过类似链表的操作增删方法结点即可完成操作。

2.基于责任链的消息队列
1、基类Handler
我们可以仿照上面的类图,先创建出一个基类Handler。我们只需要让后续的具体责任链结点继承基类,实现各自的Excute即可。并且为了让各子类实现链式结构,我们还需要定义一个指针。
cpp
class HandlerText
{
public:
virtual void Excute(const std::string &text) = 0;
void SetNext(std::shared_ptr<HandlerText> next)
{
_next = next;
}
void Enable()
{
_enable = true;
}
void Disable()
{
_enable = false;
}
virtual ~HandlerText()
{
}
protected: // protected需要被子类继承
std::shared_ptr<HandlerText> _next; // 下一个责任链节点
bool _enable = true; // 是否启用该节点
};
2、责任链入口类HandlerEntry
接着就是责任链入口类,这个类主要用于将各个责任链结点(每个节点有自己的方法)构建并链接。现在我们有需求:client向server发送信息,其中信息:1.拼接时间和进程pid信息。2.server收到的内容保存到文件中。3.如果文件内容过大,要对文件内容切片保存在指定路径下的目录,打包保存,命令自定义。
因此很显然我们需要创建三个责任链结点继承基类,并实现各自的功能(Excute)。
cpp
class HandlerEntry
{
public:
HandlerEntry()
{
// 构造责任链节点
_format = std::make_shared<HandlerTextFormat>();
_save = std::make_shared<HandlerTextSaveFile>();
_backup = std::make_shared<HandlerTextBackup>();
// 设置责任链节点处理顺序
_format->SetNext(_save);
_save->SetNext(_backup);
}
void EnableHandler(bool isformat, bool issave, bool isbackup)
{
isformat ? _format->Enable() : _format->Disable();
issave ? _save->Enable() : _save->Disable();
isbackup ? _backup->Enable() : _backup->Disable();
}
void Run(const std::string &text)
{
_format->Excute(text);
}
~HandlerEntry()
{
}
private:
std::shared_ptr<HandlerText> _format;
std::shared_ptr<HandlerText> _save;
std::shared_ptr<HandlerText> _backup;
};
3、具体的责任链结点类
cpp
class HandlerTextFormat : public HandlerText
{
public:
void Excute(const std::string &text) override
{
std::string format_result = text + "\n";
if (_enable)
{
// 该节点被开启,对文本进行格式化处理
std::stringstream ss;
ss << time(nullptr) << "-" << getpid() << "-" << text << "\n";
format_result = ss.str();
std::cout << "step 1: 格式化消息: " << text << " 结果: " << format_result << std::endl;
}
if (_next)
{
_next->Excute(format_result); // 将处理结果,表现在text内部,传递给下一个节点
}
else
{
std::cout << "到达责任链处理结尾,完成责任链处理" << std::endl;
}
}
};
// 文件的基本信息: 文件路径,文件名称
std::string defaultfilepath = "./tmp/";
std::string defaultfilename = "test.log";
// 对文本进行文件保存
class HandlerTextSaveFile : public HandlerText
{
public:
HandlerTextSaveFile(const std::string &filepath = defaultfilepath,
const std::string &filename = defaultfilename)
: _filepath(filepath), _filename(filename)
{
// 形成默认的目录名, filesystem
if (std::filesystem::exists(_filepath))
return;
try
{
std::filesystem::create_directories(_filepath);
}
catch (std::filesystem::filesystem_error const &e)
{
std::cerr << e.what() << '\n';
}
}
void Excute(const std::string &text) override
{
if (_enable)
{
// 保存到文件中
std::string file = _filepath + _filename;
std::ofstream ofs(file, std::ios::app);
if (!ofs.is_open())
{
std::cerr << "open file error: " << file << std::endl;
return;
}
ofs << text;
ofs.close();
std::cout << "step 2: 保存消息: " << text << " 到文件: " << file << std::endl;
}
if (_next)
{
_next->Excute(text); // 将处理结果,表现在text内部,传递给下一个节点
}
else
{
std::cout << "到达责任链处理结尾,完成责任链处理" << std::endl;
}
}
private:
std::string _filepath;
std::string _filename;
};
const int defaultmaxline = 5; // 最大行数, 这里比较短,方便测试
// 对文件内容长度进行检查,如果长度过长,对文件内容进行打包备份
class HandlerTextBackup : public HandlerText
{
public:
HandlerTextBackup(const std::string &filepath = defaultfilepath,
const std::string &filename = defaultfilename,
const int &maxline = defaultmaxline)
: _filepath(filepath), _filename(filename), _maxline(maxline)
{
}
void Excute(const std::string &text) override
{
if (_enable)
{
// 该节点被开启,对文件进行检查,如果超范围,我们就要切片,并且进行打包备份
std::string file = _filepath + _filename;
std::cout << "Step 3: 检查文件: " << file << " 大小是否超范围" << std::endl;
if (IsOutOfRange(file))
{
// 如果超了范围,进行切片备份
std::cout << "目标文件超范围,进行切片备份" << file << std::endl;
Backup(file);
}
}
if (_next)
{
_next->Excute(text); // 将处理结果,表现在text内部,传递给下一个节点
}
else
{
std::cout << "到达责任链处理结尾,完成责任链处理" << std::endl;
}
}
private:
bool IsOutOfRange(const std::string &file)
{
std::ifstream ifs(file);
if (!ifs.is_open())
{
std::cerr << "open file eeor: " << file << std::endl;
return false;
}
int lines = 0;
std::string line;
while (std::getline(ifs, line))
{
lines++;
}
ifs.close();
return lines > _maxline;
}
void Backup(const std::string &file)
{
// 1589234234
std::string suffix = std::to_string(time(nullptr));
// "./tmp/test.txt" -> "./tmp/test.txt.1589234234"
std::string backup_file = file + "." + suffix; // 备份文件名
// 只需要文件名, 不需要路径, test.txt.1589234234
std::string src_file = _filename + "." + suffix;
// test.txt.1589234234.tgz
std::string tar_file = src_file + ".tgz";
// 切片备份并打包
pid_t pid = fork();
if (pid == 0)
{
// child
// 1. 先对文件进行重名,Linux上,对文件名进行重命名是原子的.
// "./tmp/test.txt" -> "./tmp/test.txt.1589234234"
// 2. 我们想让子进程进行数据备份
std::filesystem::rename(file, backup_file);
std::cout << "step 4: 备份文件: " << file << " 到文件: " << backup_file << std::endl;
// 3. 对备份文件进行打包,打包成为.tgz, 需要使用exec*系统调用
// 3.1 对备份文件进行打包.tgz
// "./tmp/test.txt" -> "./tmp/test.txt.1589234234" -> "./tmp/test.txt.1589234234.tgz"
// 3.1.1 更改工作路径
std::filesystem::current_path(_filepath);
// 3.1.2 调用tar命令进行打包
execlp("tar", "tar", "-czf", tar_file.c_str(), src_file.c_str(), nullptr);
exit(1); // exec*系统调用失败,返回1
}
// parent
int status;
pid_t rid = waitpid(pid, &status, 0);
if (rid > 0)
{
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
{
// 打包成功,删除源文件
std::filesystem::remove(backup_file);
std::cout << "step 5: 删除备份文件: " << backup_file << std::endl;
}
}
}
private:
std::string _filepath;
std::string _filename;
int _maxline; // 最大行数
};
3.消息队列MsgQueue
消息队列类中,我们只需要定义消息块类型msgbuf和消息队列的创建,初始化,销毁,发送和接收消息方法即可。然后创造客户端类Client和服务器类Server,分别继承MsgQueue并定义创建消息队列端(服务端)和获取消息队列端(客户端)即可。
cpp
#ifndef MSGQUEUE_HPP
#define MSGQUEUE_HPP
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define PATHNAME "/tmp"
#define PROJID 0x123
const int default_fd = -1;
const int defualt_size = 1024;
#define GET_MSGQUEUE (IPC_CREAT)
#define CREATE_MSGQUEUE (IPC_CREAT | IPC_EXCL | 0666)
class MsgQueue
{
// 有类型数据块
struct msgbuf {
long mtype;
char mtext[defualt_size];
};
public:
MsgQueue(): _msgfd(default_fd) {}
// 创建消息队列
void Create(int flag)
{
// 获取唯一的键值
key_t key = ftok(PATHNAME, PROJID);
if(key == -1)
{
std::cerr << "ftok error" << std::endl;
exit(1);
}
// 按照16进制打印key
std::cout << "key: " << std::hex << key << std::endl;
// 创建消息队列 --- 暂定
// IPC_CREAT:创建消息队列,如果不存在,创建之,否则,获取它
// IPC_CREAT | IPC_EXCL(不单独使用): 若消息队列已存在,则返回错误,否则创建之。新的消息队列
_msgfd = msgget(key, flag);
if(_msgfd == -1)
{
std::cerr << "msgget error" << std::endl;
exit(2);
}
std::cout << "msgqueue created: " << _msgfd << std::endl;
}
// 发送消息
void Send(int type, const std::string& text)
{
struct msgbuf msg;
memset(&msg, 0, sizeof(msg));
msg.mtype = type;
memcpy(msg.mtext, text.c_str(), text.size());
// 问题:不能填写成为sizeof(msg)
int n = msgsnd(_msgfd, &msg, sizeof(msg.mtext), 0);
if(n == -1)
{
std::cerr << "msgsnd error" << std::endl;
return;
}
}
// 接受消息,参数设置成输出型参数
void Recv(int type, std::string& text)
{
struct msgbuf msg;
int n = msgrcv(_msgfd, &msg, sizeof(msg.mtext), type, 0);
if(n == -1)
{
std::cerr << "msgrcv error" << std::endl;
return;
}
msg.mtext[n] = '\0';
text = msg.mtext;
}
// 获取消息队列中的属性
void GetAttr()
{
struct msqid_ds outbuffer;
int n = msgctl(_msgfd, IPC_STAT, &outbuffer);
if(n == -1)
{
std::cerr << "msgctl error" << std::endl;
return;
}
std::cout << "outbuffer.msg_perm.__key: " << std::hex << outbuffer.msg_perm.__key << std::endl;
}
// 删除消息队列
void Destroy()
{
int n = msgctl(_msgfd, IPC_RMID, 0);
if(n == -1)
{
std::cerr << "msgctl error" << std::endl;
return;
}
std::cout << "msgqueue destroyed" << std::endl;
}
~MsgQueue() {}
private:
int _msgfd;
};
// 我们需要定义消息类型
#define MSG_TYPE_CLIENT 1
#define MSG_TYPE_SERVER 2
class Server : public MsgQueue
{
public:
Server()
{
MsgQueue::Create(CREATE_MSGQUEUE);
std::cout << "server create msgqueue done" << std::endl;
MsgQueue::GetAttr();
}
~Server()
{
MsgQueue::Destroy();
}
};
class Client : public MsgQueue
{
public:
Client()
{
MsgQueue::Create(GET_MSGQUEUE);
std::cout << "client get msgqueue done" << std::endl;
}
~Client()
{
}
};
#endif // MSGQUEUE_HPP
1、客户端Client
客户端只负责获取消息队列,并发送消息。
cpp
#include "MsgQueue.hpp"
int main()
{
Client client;
while(true)
{
// 只让client发送消息
std::string input;
std::cout << "Please input message: ";
std::getline(std::cin, input);
client.Send(MSG_TYPE_CLIENT, input);
if(input == "exit")
{
break;
}
}
return 0;
}
2、服务端Server
服务端负责创建消息队列,并接收来自客户端的消息,并引入责任链模式对文本进行处理。
cpp
#include "MsgQueue.hpp"
#include "ChainOfResponsibility.hpp"
int main()
{
std::string text;
Server server;
HandlerEntry he;
he.EnableHandler(true, true, true);
while (true)
{
// 如果消息队列为空,阻塞等待
server.Recv(MSG_TYPE_CLIENT, text);
std::cout << "Received: " << text << std::endl;
if(text == "exit")
{
break;
}
// 加工处理数据,采用责任链模式
he.Run(text);
}
return 0;
}
效果如下:
