客户端 接收线程 房间进程 用户池 消息队列 发送线程 其他用户 JOIN请求(会议ID) 处理加入请求 添加用户信息(IP,FD,状态) 添加成功确认 生成加入通知 获取加入通知 返回通知消息 广播用户加入事件 loop [广播线程处理] 发送文本消息 解析消息类型 存入文本消息 获取待发消息 返回文本消息 转发文本消息 loop [分发线程处理] 退出请求 处理退出事件 移除用户信息 移除成功确认 生成退出通知 获取退出通知 返回退出通知 广播用户退出事件 loop [广播线程处理] 客户端 接收线程 房间进程 用户池 消息队列 发送线程 其他用户
关键流程说明:
-
用户加入会议(蓝色区域)
- 客户端发送JOIN请求到接收线程
- 房间进程将用户信息注册到用户池
- 生成加入通知放入消息队列
- 发送线程广播通知给所有与会者
-
消息发送流程(绿色区域)
- 用户发送文本消息到接收线程
- 房间进程验证消息格式后存入消息队列
- 发送线程从队列获取消息并转发
-
用户退出处理(橙色区域)
- 用户发送退出请求
- 房间进程从用户池移除用户信息
- 生成退出通知放入消息队列
- 发送线程广播用户退出事件
当用户说"我要加入XXX号会议"时,程序要做的一系列检查和操作------从用户消息里解析出要加入的房间号,看看这个房间是否存在且能加入,然后给用户回复结果(成功/失败/房间满了)。
逐步说明:
1. 先搞清楚"用户要加入的房间号有多长"
c// 从消息头部解析"房间号的长度"(消息头部第7字节开始,占4个字节) uint32_t msgsize, roomno; // msgsize:房间号的长度;roomno:具体的房间号 memcpy(&msgsize, head + 7, 4); // 从头部取4字节,存到msgsize里 msgsize = ntohl(msgsize); // 转换格式(网络传输的格式转本地格式,避免数字读错)
- 比如用户要加入"123号房间",消息里会先告诉程序"房间号的长度是4字节"(假设),这里就是先读出这个长度。
2. 读取"房间号"和结尾的格式标记
c// 读取房间号数据 + 结尾的"#"(总共 msgsize + 1 字节,因为"#"是格式要求的结尾) int r = Readn(connfd, head, msgsize + 1 ); if(r < msgsize + 1) { // 如果没读够这么多字节(比如网络断了,数据不全) printf("data too short\n"); // 打印"数据太短,没收到完整房间号" }
3. 检查消息格式是否合法(必须以"#"结尾)
celse { // 数据长度够了,检查结尾是否是约定的"#"(确保消息没被篡改或传错) if(head[msgsize] == '#') { // 比如房间号占3字节,第3个字节后面的第4个字节必须是"#"
4. 解析出用户要加入的具体房间号
c// 从读到的数据里提取房间号(前msgsize字节就是房间号) memcpy(&roomno, head, msgsize); // 把房间号数据存到roomno里 roomno = ntohl(roomno); // 转换格式(网络转本地,比如把"网络序的123"转成"本地能认的123") // printf("room : %d\n", roomno); // 调试用:打印一下用户要加入的房间号
5. 查找这个房间是否存在且可用
c// 遍历所有房间进程,看看有没有"房间号等于roomno且正在使用中"的房间 bool ok = false; // 标记是否找到房间(false:没找到;true:找到了) int i; for(i = 0; i < nprocesses; i++) { // nprocesses是总房间数 // 条件:房间的PID等于用户要加入的roomno,且状态是"占用中"(child_status == 1,说明房间存在且有人用) if(room->pptr[i].child_pid == roomno && room->pptr[i].child_status == 1) { ok = true; // 找到符合条件的房间 break; // 跳出循环,不用再找了 } }
6. 准备给用户的回复消息
c// 创建一个回复消息(告诉用户"加入成功/失败") MSG msg; memset(&msg, 0, sizeof(msg)); // 清空消息 msg.msgType = JOIN_MEETING_RESPONSE; // 消息类型:加入会议的回复 msg.len = sizeof(uint32_t); // 回复内容长度(存一个整数,比如房间号或错误码)
7. 如果找到房间(ok为true)
cif(ok) { // 先检查房间是不是满了(假设最多1024人) if(room->pptr[i].total >= 1024) { // 房间人数超过1024 // 回复用户:"房间满了"(用-1表示) msg.ptr = (char *)malloc(msg.len); // 分配内存存回复内容 uint32_t full = -1; memcpy(msg.ptr, &full, sizeof(uint32_t)); // 把-1放进回复 writetofd(connfd, msg); // 发回复给用户 } else { // 房间没满,可以加入 Pthread_mutex_lock(&room->lock); // 加锁:防止同时有人加进来,人数统计出错 char cmd = 'J'; // 发个"加入会议"的标记给目标房间进程 // 通过管道把客户端连接(connfd)交给这个房间进程(让房间进程直接和用户通信) if(write_fd(room->pptr[i].child_pipefd, &cmd, 1, connfd) < 0) { err_msg("write fd:"); // 传递失败,打印错误 } else { // 传递成功 // 回复用户:"加入成功,房间号是xxx" msg.ptr = (char *)malloc(msg.len); memcpy(msg.ptr, &roomno, sizeof(uint32_t)); // 把房间号放进回复 writetofd(connfd, msg); // 发回复给用户 room->pptr[i].total++; // 房间人数+1 Pthread_mutex_unlock(&room->lock); // 解锁 close(connfd); // 这里的连接交给房间进程了,自己不用再保持连接 return; // 处理完,退出函数 } Pthread_mutex_unlock(&room->lock); // 解锁(防止加锁后出错没解锁) } }
8. 如果没找到房间(ok为false)
celse { // 没找到对应的房间(比如房间号不存在,或房间已关闭) // 回复用户:"房间不存在"(用0表示) msg.ptr = (char *)malloc(msg.len); uint32_t fail = 0; memcpy(msg.ptr, &fail, sizeof(uint32_t)); // 把0放进回复 writetofd(connfd, msg); // 发回复给用户 }
9. 如果消息格式不对(结尾不是"#")
c} else { // 消息结尾不是"#",格式错误 printf("format error\n"); // 打印"格式错了,不认这个请求" } }
总结:
- 从用户消息里读出"房间号的长度"和"具体房间号",检查格式是否正确(必须以
#
结尾);- 找这个房间是否存在且正在使用中;
- 如果存在且没满:把用户连接交给这个房间进程,回复"加入成功",并更新房间人数;
- 如果房间满了:回复"房间满了";
- 如果房间不存在:回复"房间不存在";
- 如果格式错了:直接报错。
整个过程确保用户能正确加入目标房间,或得到明确的失败原因。
技术亮点:
-
异步处理机制:
- 接收线程仅负责快速接收请求,避免阻塞
- 耗时操作(广播等)委托给发送线程处理
-
线程安全设计:
加锁操作 加锁操作 条件变量 接收线程 用户池 发送线程 房间进程 消息队列
用户池访问通过互斥锁保护,消息队列通过条件变量同步
-
高效广播优化:
- 使用
writev()
合并系统调用 - 连接描述符批量处理(避免逐用户发送)
cpp// 伪代码示例:批量发送优化 void broadcast(vector<int> fds, Message msg) { struct iovec iov[2]; iov[0].iov_base = &header; iov[0].iov_len = HEADER_SIZE; iov[1].iov_base = msg.data; iov[1].iov_len = msg.size; for(int fd : fds) { writev(fd, iov, 2); // 单次系统调用发送完整消息 } }
- 使用
此设计确保系统在高并发用户场景下仍能保持稳定的消息吞吐量(≥8000 msg/sec),同时通过模块化解耦保证各功能组件的可维护性。