1、class MsgQueue
1.1 接收数据
- 通过 msgrcv() 函数将指定的消息队列的数据读取到数据缓冲 DataBuffer 中;
- 将 DataBuffer 指向的数据解析成消息结构体指针(MsgStruct *),;
- 取出消息结构体的消息对象(message).
std::int32_t length = msgrcv(m_msg_queue_id, reinterpret_cast<void *>(dataBuffer.pos()), MAX_BUFFER_LENGTH, m_process_id, IPC_NOWAIT);
MsgStruct *msgStructPtr = reinterpret_cast<MsgStruct *>(dataBuffer.pos());
std::string str(msgStructPtr->message, msgStructPtr->message_length);
1.2 发送数据
- 将字符串类型的参数转成数据缓冲 DataBuffer 类型;
- 调用 msgsnd() 将数据缓冲发送到消息队列;
2、class message
消息类型共 9 种,分为请求测试用例、发送消息、开启、停止、结束测试、测试用例结果、故障、其他、永久开启等。
类的继承关系如下:MessageBase -> MessageQueryTestItem / MessageOther -> MessageSendMsg/MessageStart -> MessageStartForever/MessageStop
2.1 class MessageBase
常用的 3 种消息类型:1)发送消息类型 SendMessage;2)启动测试用例类型 start;3)停止测试用例类型 stop。
- 记录消息所挂载的 epoll;
- 获取类型函数
- 响应函数:记录源地址、目标地址、请求 id、包类型
子类重载的响应函数逻辑是调用基类的响应函数后,将传入的 json 对象转成测试用例对象,并存于容器,将容器传给处理函数后,将测试用例转成响应信息的 message 字段。
2.2 class MessageQueryTestItem 请求测试用例类型
在继承基础上,新增 toMessage()。
- 1)toMessage():用于将测试用例对象转成 json 格式,并序列化到 jsonObject:
jsonMessage.push_back(TestItemConfig::testItem2Json(it));
nlohmann::json jsonObject;
jsonObject["message_type"] = getType();
jsonObject["message"] = jsonMessage;
- 2)响应函数:从测试用例配置文件读取测试用例对象,并将测试用例对象转成 Json 类型,作为响应的字段值。
2.3 class MessageSendMsg
在继承基础上,新增处理函数。
- 1) 处理函数 deal():具体做法是将测试用例对象传给所挂载的 epoll,由 epoll 将测试用例对象所要接
收的 msg 发送给测试用例实例。
2.4 class MessageStart
在继承基础上,新增处理函数 deal() 和开启函数 startAll()/startSpecify()。
-
1)**处理函数 deal() ** 具体操作是赋值测试用例的 result 字段,并将处理后的测试用例对象添加于容器;
-
2)开启函数是将测试用例的 json 描述信息转成测试用例对象,并置放在容器里;
2.4.1 MessageStart::responce
Epoller 一运行,上位机发送一个 MessageStart 消息报文,消息管理器调用对应的 responce 报文进行响应。该响应报文是将 jsonMessage 包含的测试用例对象进行处理(deal()),最后执行这些测试用例,即将这些测试用例放入测试用例待执行队列,设置测试状态为 Running,并修改测试用例通知 fd m_notify_test_item_fd
的 EPOLLOUT 事件 EpollUtil::epollCtrl(m_epfd, EPOLL_CTL_MOD, m_notify_test_item_fd, EPOLLOUT | EPOLLET)
。
2.5 class MessageStop
-
在继承基础上,新增停止函数。
-
1)停止函数:将测试用例的 json 描述信息转成测试用例对象,并将其放入容器。将容器传给所挂载的 epoll,并由 epoll 停止测试用例(stopTestItem());
3、class MessageManager 消息管理器
-
1)消息类型对象容器:消息管理器维护一个哈希映射表,按名字存储每种消息类型对象。
-
2)响应函数:解析请求报文,判断消息类型,调用相应的响应函数。
4、class Package 发送给 PC 的数据报文的打包与解包
4.1 PC 与下位机数据交互的通信过程如下:
- PC –> 下位机:请求启动的测试用例 json 信息({“message”: testItemJson, “message_type”:”send_message”});
- 下位机 –> PC:若下位机启动测试用例成功,发送响应报文,消息内容在转述请求包的 message 字段基础上,
添加处理结果 result 字段(“result”: “ok”); - 下位机 –> PC:若接收到测试用例返回的结果({“name”:”xxx”, “value”:”xxx”}),发送响应报文,消息内容在
转述请求包的 message 字段基础上,添加处理结果 result 字段(“result”: {“name”:”xxx”, “value”:”xxx”})
日志实例:
2024-03-05 14:23:10.600 [info] 给pc发送报文:
{"message":
{ "bin":"test_soc_soft_ver","chip":"SOC1","key":11,"name":"start_soc_soft_ver","parameter":"eol",
**"result":"{\"name\":\"rtc\",\"value\":[\"MPQ79700FS's RTC Read Test starts:\"]}"**,
"test":true,"type":"parallel_test"},
"message_type":"test_item_result"
}
4.2 打包基操作流程
打包基操作分为 2 个阶段。阶段 1 私有打包方法将文本类型转为数据包 PackageData 类型:将字符串整合成数据包的字段(源头src、去向des、包id、包类型、消息内容);阶段 2 公有打包方法将 PackageData 类型转为 DataBuffer 类型:申请一段字符串空间,将 PackageData 的字段逐个填入,返回时将字符串转成 DataBuffer 类型。
// 阶段 1:文本类型->PackageData
packageDataPtr->src = m_local_chip_name;
packageDataPtr->des = ADDRESS_PC;
packageDataPtr->id = generateMessageId();
packageDataPtr->package_type = PackageType::Report;
packageDataPtr->message = jsonObject.dump();
// 阶段 2:
std::memcpy(bufPtr, packageDataPtr->src.c_str(), packageDataPtr->src.size());
bufPtr += SRC_LENGTH;
xxx
return std::make_shared<DataBuffer>(buf);
4.3 打包封装操作
- 下位机与 PC 的交互数据包类型有 4 种,分别为心跳包、请求包、响应包、报告包等。打包封裝操作均是将文本/对象类型打包成 PackageData 类型,分别有 4 种封装操作:心跳包打包、故障请求打包、结束测试打包、测试用例结果打包。
4.4 解包操作
按结构体逐部分解析字符串。
class Channel
将 fd 封装成 Channel,对于 TCP fd,封装成 TcpChannel;对于 pipefd,封装成 TestItemChannel。
5.1 ChannelBase 通道基
含有读 fd、读缓冲、读操作、写 fd、写缓冲队列、写操作、epoll。
5.1.1 放置待写数据操作
将数据封装成数据结构体,添加进写缓冲队列,并让 epoll 监听通道基的写 fd 的写事件 m_epollEx_ptr->notifyWrite(shared_from_this());
。
5.1.2 读操作
从读 fd 读取数据到读缓冲 read(m_read_fd, m_receive_buf_ptr->pos(), m_receive_buf_ptr->readOnceSize())
。
5.1.3 写操作
从写缓冲队列弹出数据缓冲,将数据缓冲的数据写入写 fd,write(m_write_fd, dataBufferPtr->pos(), dataBufferPtr->writeOnceSize())
。
5.2 TcpChannel
- 含有名称描述符
m_name
,其值为 client 的名字(SOC2/PC); - 指向测试用例对象的指针、指向消息队列的指针、指向测试用例的指针
m_file_ptr
- 读写 fd 都是连接描述符 connfd(
connfd = accept(m_listenfd, (sockaddr *)&clientSocketAddress, &socketAddressLength))
);
5.3 TestItemChannel
- 含有名称描述符
m_name
,其值为测试用例的名称; - 测试用例通道是只提供读 fd(其值为 popen 返回的文件指针),epoll 只能读取测试用例输出到标准输出设备的数据。
- 无写fd,只有读 fd(其值为 popen 返回的指向测试用例实例标准输入流的文件指针的文件描述符
fileno(fileptr)
);
备注: poepn() 调用 fork() 产生子进程,建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或写入到子进程的标准输入设备中。
5.4 ChannelFactory
5.4.1 创建测试用例通道
通过 popen() 启动就绪队列里的测试用例,并用通道对返回的文件指针 fp(作为读 fd)。
5.4.2 创建 TCP 通道
将 fd 作为 readfd 与 writefd。
6、测试用例管理器
测试用例队列、挂载的 epoll、设置测试用例、清除测试用例、开启下一测试用例。
6.1 设置就绪测试用例
将需要运行的测试用例容器逐个弹出,添加进就绪测试用例缓冲队列 m_test_item_queue;让 epoll 监听 m_notify_test_item_fd 的可写事件 notifyNextTestItem EpollUtil::epollCtrl(m_epfd, EPOLL_CTL_MOD, m_notify_test_item_fd, EPOLLOUT | EPOLLET);
备注: 思考 m_notify_test_item_fd 在哪里接收写
6.2 清除就绪测试用例
将就绪测试用例队列 m_test_item_queue 清空。
6.3 启动测试用例
通过 popen() 启动就绪队列里的测试用例,并用通道对返回的文件指针 fp(作为读 fd) 进行封装。将读 fd 与 通道作为键值对保存在 epoll 上。
7、上行线程
- 上行线程都要挂靠在 Epoll,Epoll 记录上行 fd,若上行 fd 不存在,则创建上行 TCP 通道。
8、class epoller
8.1 初始化
-
创建消息管理器、测试用例管理器、上行线程、epollfd 实例(epollfd 指示该实例)。
-
对于部署在 SOC2(isSlave) 的 epoller 既承担 TestItem 的 server 角色,又承担 SOC1 epoller 的 client 角色。它需要接收来自 SOC1 的转发请求。所以需要创建上行 TCP 连接通道(m_uplevel_fd = 连接描述符 connFd)。
参考链接:https://blog.csdn.net/dearQiHao/article/details/102877786 # socket编程之 connect()函数
// connect() 连接请求后,服务器端只是把连接请求信息记录到等待队列,只有 accept 操作后,才能进行数据交换(read/write)
m_uplevel_fd = EpollUtil::connectTo(ip, port); // m_uplevel_fd = 连接描述符 connFd
- 对于部署在 SOC1(isMaster) 的 epoller 只承担 PC 端和 SOC2 epoller 的server 角色。它创建监听描述符以监听 PC 和 SOC2 的消息。
// 创建监听 fd,等待可读事件
m_listenfd = EpollUtil::createListenFd(m_epfd, m_config_ptr->getListenPort());
->
// 把监听套接字加入 epoll
epollCtrl(epfd, EPOLL_CTL_ADD, listenfd, EPOLLIN | EPOLLET);
8.2 运行
1)消息管理器对 StartForever 请求报文进行响应;
2)创建 epoll_event
结构体(记录 fd 就绪时,内核保存和返回的数据);
3)等待 epoll 实例(epollfd)所挂载的 fd 所监听的事件发生,并将就绪事件保存在 epoll_event
结构体;
4)处理事件:
- 若就绪事件是新连接到来事件(监听描述符
m_listenfd
可读事件),则接受 TCP 连接; - 若就绪事件是通知执行测试用例(通知描述符
m_notify_test_item_fd
可读事件),则启动测试用例; - 若就绪事件是其他可读可写事件,则执行处理读写操作。
8.3 发送数据包
- 通过数据包记录的目标对象名称,找到对应通道;
- 将参数数据包封装成数据缓冲对象,再将数据缓冲对象封装成数据结构体({srcAddr,dataBuffer});
8.4 发送给测试用例
Epoller 维护一个哈希表({key:Channel})m_map_channel
,根据测试用例的 key 找到该通道(对 fd 的封装),并将测试用例的 msg 作为待放置数据,封装成数据结构体 DataStruct,再放入写缓冲队列。
8.5 开启测试用例
由测试用例管理器调度。
8.6 停止测试用例
epoller 分别调用测试用例通道,让通道给实例发送 kill 信号,实例收到信号后会执行回调函数,用于将测试用例执行的信息封装在 json 对象,并通过管道传给 epoller(cout << jsonObject.dump() <<endl;
)。
8.7 处理事件
1)对于 m_notify_test_item_fd
的可写事件,由测试用例管理器开启测试用例实例;
2)针对 SOC1,对于监听 fd 接收到 TCP 的请求连接(与 SOC2 或 上位机的连接),则创建一个 TCP 通道;
3)对于测试用例通道的读写,调用处理读写函数。