写个函数process_main,它将是会议系统的核心引擎,相当于每个会议房间的"大脑"。它让一个房间进程能够同时处理几十甚至上百个参会者的实时通信需求。
具体的说,启动房间后,创建专门的线程接收新用户、发送消息,同时用 select 监听所有在线用户的消息,解析后转发给房间内其他人,实现多人实时互动(文本、图片、音频等)。
一、核心作用:解决什么问题?🚀
-
实时通信中枢
管理会议房间内所有用户的实时交互(文字/图片/语音) -
高并发处理
用多线程技术同时处理:- 新用户加入 👥
- 消息接收 📥
- 消息广播 📤
- 用户退出 🚪
-
资源隔离
每个房间独立运行,一个房间崩溃不会影响其他房间(进程级隔离) -
协议解析
处理复杂的网络数据格式,解决TCP粘包/拆包问题
二、工作流程详解🔍
步骤1:房间启动准备
printf("room %d starting \n", getpid()); // 打印:"会议室X号机已启动"
Signal(SIGPIPE, SIG_IGN); // 忽略管道破裂错误(防止有人突然退出导致崩溃)
步骤2:雇佣工作团队 👷♂️
// 雇1个前台接待员(专门处理新用户加入)
Pthread_create(&pfd1, NULL, accept_fd, ptr);
// 雇5个快递员(专门负责发送消息)
for(int i=0; i<5; i++) {
Pthread_create(..., send_func, NULL);
}
前台接待员:
accept_fd线程
快递员:send_func线程(5个组成发送线程池)
前二步总结:
void process_main(int i, int fd) { // i:房间编号;fd:与父进程通信的管道
printf("room %d starting \n", getpid()); // 打印房间进程ID(方便调试)
Signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号(避免向已关闭的连接写数据时进程崩溃)
// 声明要用到的函数
void* accept_fd(void *); // 接收新用户连接的线程函数
void* send_func(void *); // 发送消息的线程函数
void fdclose(int, int); // 关闭客户端连接的函数
// 创建“接收新连接”的线程
int *ptr = (int *)malloc(4); // 给线程传递参数(管道fd)
*ptr = fd;
pthread_t pfd1;
Pthread_create(&pfd1, NULL, accept_fd, ptr); // 启动accept_fd线程
// 创建多个“消息发送”线程(数量由SENDTHREADSIZE定义)
for(int i = 0; i < SENDTHREADSIZE; i++) {
Pthread_create(&pfd1, NULL, send_func, NULL); // 启动send_func线程
}
- 关键操作:
- 忽略
SIGPIPE:当客户端断开连接后,服务器若继续写数据会触发SIGPIPE导致进程退出,忽略后可避免。 - 创建两类线程:
accept_fd负责接收新加入房间的用户;send_func(多个)负责将消息转发给房间内的其他用户(多线程提高发送效率)。
- 忽略
在 Unix/Linux 系统中,SIGPIPE 是一个信号(信号编号为 13),当程序尝试向一个“已关闭的管道(pipe)”或“已断开的网络连接”写入数据时,操作系统会向程序发送这个信号。如果程序不处理 SIGPIPE,默认行为是直接终止程序(崩溃退出)。
为什么会产生 SIGPIPE?
最常见的场景是网络通信:
- 假设客户端 A 与服务器建立了连接,然后客户端 A 突然断开连接(比如关闭窗口、网络中断),但服务器还在继续向这个连接写数据。
- 此时服务器的写操作会失败,操作系统会检测到“向无效连接写数据”,于是向服务器进程发送
SIGPIPE信号。
代码中 Signal(SIGPIPE, SIG_IGN) 的作用
SIG_IGN 表示“忽略信号”。这句代码的意思是:告诉操作系统,当程序收到 SIGPIPE 信号时,不要终止程序,直接忽略这个信号。
在你提供的会议系统代码中,这是一个关键的保护措施:
- 房间内的用户可能随时断开连接(比如退出会议),但服务器可能还在尝试向这个已断开的连接发送消息(如其他人的聊天内容、音视频数据)。
- 如果不忽略
SIGPIPE,服务器进程会因为这个信号直接崩溃,导致整个房间的服务中断。 - 忽略后,服务器虽然写操作会失败(返回错误码),但程序不会崩溃,可以通过检查写操作的返回值(如
writen的返回值)来处理“连接已断开”的情况(比如从房间中移除该用户)。
举个例子
比如客户端 B 退出会议后,服务器还在尝试向 B 的套接字写数据:
- 没有
Signal(SIGPIPE, SIG_IGN):服务器收到SIGPIPE,直接崩溃,房间内所有用户都无法通信。 - 有
Signal(SIGPIPE, SIG_IGN):服务器忽略信号,写操作返回-1,程序可以通过这个错误码检测到“B 已断开连接”,然后调用fdclose清理 B 的资源,其他用户不受影响。
总结
SIGPIPE 是“向无效连接写数据”时触发的致命信号,默认会终止程序。代码中用 Signal(SIGPIPE, SIG_IGN) 忽略它,是为了避免服务器因用户意外断开连接而崩溃,保证程序的稳定性。之后可以通过检查 I/O 操作的返回值来处理连接断开的情况。
步骤3:主循环 - 监控所有参会者 📡
for(;;) { // 7x24小时不间断工作
// 1. 复制当前参会者名单
fd_set rset = user_pool->fdset;
// 2. 检查谁在发言(等待0.001秒)
Select(maxfd+1, &rset, NULL, NULL, &time);
// 3. 遍历所有参会者
for(int i=0; i<=maxfd; i++) {
if(检测到用户i在发言) {
// 读取发言的前11个字节(相当于信封)
Readn(i, head, 11);
if(信封格式正确) {
// 拆信封看内容类型
switch(消息类型) {
case 文字消息: // 收信+存快递站
case 图片消息: // 收图+存快递站
case 关闭摄像头: // 处理控制指令
}
} else {
报错("信封格式不对!");
}
}
}
}
// 无限循环,持续监听房间内所有客户端的消息
for(;;) {
fd_set rset = user_pool->fdset; // 复制当前房间内的套接字集合(user_pool是房间的用户管理池)
int nsel; // select返回的就绪套接字数量
struct timeval time;
memset(&time, 0, sizeof(struct timeval)); // 超时时间设为0(非阻塞检查)
// 循环调用select,直到有就绪的套接字(若超时则重新复制fdset再检查)
while((nsel = Select(maxfd + 1, &rset, NULL, NULL, &time)) == 0) {
rset = user_pool->fdset; // 重新复制最新的套接字集合(可能有新用户加入)
}
// 遍历所有套接字,检查哪些有数据到来
for(int i = 0; i <= maxfd; i++) {
if(FD_ISSET(i, &rset)) { // 该套接字有数据可读
char head[15] = {0};
int ret = Readn(i, head, 11); // 读取11字节的消息头(自定义协议的固定长度)
// 情况1:客户端断开连接(读取到0或负数)
if(ret <= 0) {
printf("peer close\n");
fdclose(i, fd); // 处理连接关闭(从房间移除用户)
}
// 情况2:成功读取11字节消息头
else if(ret == 11) {
if(head[0] == '$') { // 消息头必须以'$'开头(验证协议格式)
// 解析消息类型(2字节,网络字节序转主机字节序)
MSG_TYPE msgtype;
memcpy(&msgtype, head + 1, 2);
msgtype = (MSG_TYPE)ntohs(msgtype);
// 构造消息结构体(存储消息详情)
MSG msg;
memset(&msg, 0, sizeof(MSG));
msg.targetfd = i; // 发送者的套接字
memcpy(&msg.ip, head + 3, 4); // 发送者的IP(4字节)
int msglen;
memcpy(&msglen, head + 7, 4); // 消息体长度(4字节,网络字节序转主机)
msg.len = ntohl(msglen);
// 处理:图片、音频、文本消息
if(msgtype == IMG_SEND || msgtype == AUDIO_SEND || msgtype == TEXT_SEND) {
// 转换消息类型(发送方是SEND,接收方显示为RECV)
msg.msgType = (msgtype == IMG_SEND) ? IMG_RECV :
(msgtype == AUDIO_SEND) ? AUDIO_RECV : TEXT_RECV;
msg.ptr = (char *)malloc(msg.len); // 分配内存存消息体
msg.ip = user_pool->fdToIp[i]; // 从映射表获取发送者IP
// 读取消息体(长度为msg.len)
if((ret = Readn(i, msg.ptr, msg.len)) < msg.len) {
err_msg("3 msg format error"); // 消息体不完整
} else {
// 读取消息结尾的'#'(验证协议完整性)
int tail;
Readn(i, &tail, 1);
if(tail != '#') {
err_msg("4 msg format error"); // 结尾符错误
} else {
sendqueue.push_msg(msg); // 消息格式正确,放入发送队列
}
}
}
// 处理:关闭摄像头消息
else if(msgtype == CLOSE_CAMERA) {
char tail;
Readn(i, &tail, 1); // 读取结尾符
if(tail == '#' && msg.len == 0) { // 验证格式(无消息体,仅结尾符)
msg.msgType = CLOSE_CAMERA;
sendqueue.push_msg(msg); // 放入发送队列
} else {
err_msg("camera data error "); // 格式错误
}
}
} else {
err_msg("1 msg format error"); // 消息头不以'$'开头
}
} else {
err_msg("2 msg format error"); // 消息头长度不足11字节
}
// 所有就绪套接字处理完毕,退出循环
if(--nsel <= 0) break;
}
}
}
}
- 核心逻辑:
- 用
select实现“多路复用”:同时监听房间内所有客户端的套接字,高效处理多用户并发消息。 - 严格解析消息格式:按自定义协议(
$ + 类型 + IP + 长度 + 内容 + #)验证消息,确保数据完整。 - 消息转发机制:将合法消息放入
sendqueue队列,由send_func线程负责转发给房间内其他用户(解耦接收和发送,提高效率)。
- 用
三、关键技术解析 ⚙️
1. 消息协议处理(解决TCP粘包问题)
// 消息格式:$_类型_IP_长度_内容_#
char head[15] = {0};
Readn(i, head, 11); // 先读固定11字节头
// 示例:文字消息 "Hello" 的完整格式
// $_TEXT_192.168.1.10_0005_Hello_#
2. 多线程分工协作
| 线程类型 | 相当于 | 工作内容 |
|---|---|---|
| 主循环线程 | 监控中心 | 检测谁在发言,读取消息头 |
| accept_fd线程 | 前台接待 | 处理新用户加入 |
| send_func线程 | 快递员 | 把消息分发给其他参会者 |
3. 发送线程池(解决发送瓶颈)
for(int i=0; i<SENDTHREADSIZE; i++) { // 默认5个发送线程
Pthread_create(..., send_func, NULL);
}
- 为什么需要线程池?
避免单个线程发送大文件(如图片)时阻塞其他消息
4. 异常处理机制
if(ret <= 0) { // 读取失败
printf("peer close\n");
fdclose(i, fd); // 清理该用户资源
}
- 自动检测用户掉线
- 房主退出时关闭整个房间
四、实际工作场景模拟 💬
假设3人会议中用户A发送图片:
- 用户A发送:
$_IMG_192.168.1.10_120000_[图片数据]_# - 主线程检测到A的socket有数据
- 读取11字节头,识别是图片类型
- 根据长度120000分配内存,读取完整图片数据
- 把消息放入发送队列
- 发送线程从队列取出消息
- 转换成接收格式:
$_IMG_RECV_192.168.1.10_120000_[图片数据]_# - 分发给用户B和用户C
五、设计亮点 ✨
-
三重缓冲设计
- 接收与发送解耦
- 避免大文件阻塞小消息
-
零拷贝优化
// 发送时直接写原始数据,避免内存拷贝 writen(i, sendbuf, len); -
精准流量控制
struct timeval time; memset(&time, 0, sizeof(time)); time.tv_usec = 1000; // 精确到1毫秒的超时控制 -
安全内存管理
msg.ptr = (char *)malloc(msg.len); // 动态分配 free(msg.ptr); // 发送完成后立即释放
六、性能优化点 ⚡️
实际使用中可以优化:
// 建议优化:使用epoll替代select
int epfd = epoll_create(100);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = i;
epoll_ctl(epfd, EPOLL_CTL_ADD, i, &ev);
// 主循环改为
int nready = epoll_wait(epfd, events, 100, 1);
当参会者超过1000人时,epoll比select性能高10倍以上
总结:这个函数像什么?🏆
可以把 process_main 想象成会议中心的智能调度系统:
- 前台接待:处理新参会者登记(accept_fd线程)
- 监控大屏:实时显示谁在发言(主循环)
- 快递网络:5个快递员分发消息(发送线程池)
- 安全系统:自动清理退场人员(fdclose)
也可以这样说:
大脑主要做三件事:
- 初始化线程:创建接收新用户和发送消息的线程。
- 监听消息:用
select实时监测所有在线用户的消息。 - 解析转发:验证消息格式,将合法消息放入发送队列,由发送线程转发给其他人。
正是这个精密的系统,支撑着会议室内流畅的实时通信体验,即使同时处理图片、文字、语音等多种数据也能游刃有余!

被折叠的 条评论
为什么被折叠?



