Master-Worker 聊天服务器消息并发架构
一、目标
极致发送速度
高并发
低延迟
并发模型
二、Master
1. 单线程 epoll
- 只负责:accept + 分发
极低 accept 分发延迟:Master 不参与业务,accept → distribute_worker 的延迟被限制在一次系统调用与一个轻量通知(eventfd/写队列)的开销内。
更高并发 accept 吞吐:使用 accept + 非阻塞 + 高 backlog 可在短时间内接收大量新连接而不阻塞业务路径。
极小锁竞争:Master 只把 fd“推送”给 Worker → 几乎零锁争用。
if (listen(listen_sockfd, SOMAXCONN) == -1) {
std::cerr << "Bind error in Master" << std::endl;
}
void Master::accept_client() {
while (true) {
sockaddr_in cli;
socklen_t len = sizeof(cli);
int cli_fd = accept(listen_sockfd, (sockaddr *)&cli, &len);
if (cli_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
std::cerr << "accept error in Master" << std::endl;
break;
}
distribute_worker(cli_fd);
}
}
- 不处理业务 → 零阻塞
void Master::distribute_worker(int cli_fd) {
int num = _worker++ % WORKER;
Worker_fd[cli_fd] = num;
auto work = workers[num];
work->make_queue(cli_fd);
}
2. 非阻塞 listen socket
int Master::make_nonblocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL);
if (flags == -1)
return flags;
return fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
}
void Master::init() {
listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (make_nonblocking(listen_sockfd) == -1) {
std::cerr << "Make_nonblocking error in Master" << std::strerror(errno) << std::endl;
}
// 其他初始化代码...
}
3. 轮询负载均衡
用一个无锁结构分配,Master 只负责把 client_fd 放到目标 worker 的队列,然后 work 通知该 worker
void Master::distribute_worker(int cli_fd) {
int num = _worker++ % WORKER;
Worker_fd[cli_fd] = num;
auto work = workers[num];
work->make_queue(cli_fd);
}
三、Worker
优化结果:每个 Worker 独立处理自己的连接,减少了锁的使用,提高了处理效率
1. 一 Worker 一线程
通过为每个 Worker 分配独立的 epoll_worker 和 conn。这种设计不仅提高了性能,还减少了线程之间的竞争,使得每个 Worker 可以高效地处理自己的任务,充分利用 CPU 缓存,进一步提升了并发性能
每个连接(fd)对应一个 Connection 对象,存储在 Worker 的本地 map 中,减少跨线程访问
class Worker {
public:
Worker(int id)
: epoll_worker(-1),
_id(id) {}
void make_queue(int cli) {
std::lock_guard<std::mutex> lock(cli_mutex); // 仅在修改共享资源时加锁
if (make_nonblocking(cli) == -1) {
perror("Cli_make_nonblocking error");
return;
}
struct epoll_event ev;
ev.data.fd = cli;
ev.events = EPOLLIN | EPOLLET;
conn[cli] = std::make_shared<Connection>(cli, user_msg); // 每个 Worker 有自己的 conn
if (epoll_ctl(epoll_worker, EPOLL_CTL_ADD, cli, &ev) < 0) {
std::cerr << "epoll_ctl_add_error in Worker.cc" << std::strerror(errno) << std::endl;
}
cli_fds.push_back(cli);
}
private:
int epoll_worker; // 每个 Worker 有自己的 epoll 实例
std::unordered_map<int, std::shared_ptr<Connection>> conn; // 每个 Worker 有自己的连接映射
std::vector<int> cli_fds; // 每个 Worker 有自己的客户端文件描述符列表
std::mutex cli_mutex; // 仅在修改共享资源时加锁
};
- 独享 epoll_worker
void Worker::start() {
epoll_worker = epoll_create1(0);
if (epoll_worker == -1) exit(1);
_thread = std::thread([this]() { this->eventloop(); });
}
- 独享 CPU 缓存 → 无锁
在多线程环境中,每个线程都有自己独立的 CPU 缓存。如果多个线程频繁访问和修改共享数据,会导致缓存一致性问题,这会显著降低性能。为了避免这种问题,可以尽量减少线程之间的共享数据访问,让每个线程操作自己的数据,这样可以最大限度地利用 CPU 缓存,减少缓存失效的次数
void Worker::eventloop() {
_running = true;
struct epoll_event events[NUM];
while (_running) {
int n = epoll_wait(epoll_worker, events, NUM, TIME);
if (n < 0) {
if (errno == EINTR) continue;
}
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (events[i].events & (EPOLLERR | EPOLLHUP) || !(events[i].events & EPOLLIN)) {
remove_fd(fd);
continue;
}
auto it = conn.find(fd);
if (it == conn.end()) {
std::cerr << "Invalid fd or nullptr connection: " << fd << std::endl;
remove_fd(fd);
continue;
}
if (!conn[fd]->readMessage()) {
remove_fd(fd);
}
}
}
}
2. 一连接一 fd
- 非阻塞 cli_fd
- EPOLLET + EPOLLIN
void Worker::make_queue(int cli) {
std::lock_guard<std::mutex> lock(cli_mutex);
if (make_nonblocking(cli) == -1) {
perror("Cli_make_nonblocking error");
return;
}
struct epoll_event ev;
ev.data.fd = cli;
ev.events = EPOLLIN | EPOLLET;
conn[cli] = std::make_shared<Connection>(cli, user_msg);
if (epoll_ctl(epoll_worker, EPOLL_CTL_ADD, cli, &ev) < 0) {
std::cerr << "epoll_ctl_add_error in Worker.cc" << std::strerror(errno) << std::endl;
}
cli_fds.push_back(cli);
}
3. 业务逻辑
- Connection 对象
Connection 对象负责管理每个客户端连接的状态和消息处理。每个连接都有一个独立的 Connection 对象,存储在 Worker 的 conn 映射中
class Worker {
public:
void make_queue(int cli) {
std::lock_guard<std::mutex> lock(cli_mutex);
if (make_nonblocking(cli) == -1) {
perror("Cli_make_nonblocking error");
return;
}
struct epoll_event ev;
ev.data.fd = cli;
ev.events = EPOLLIN | EPOLLET;
conn[cli] = std::make_shared<Connection>(cli, user_msg); // 创建 Connection 对象
if (epoll_ctl(epoll_worker, EPOLL_CTL_ADD, cli, &ev) < 0) {
std::cerr << "epoll_ctl_add_error in Worker.cc" << std::strerror(errno) << std::endl;
}
cli_fds.push_back(cli);
}
private:
std::unordered_map<int, std::shared_ptr<Connection>> conn; // 每个 Worker 有自己的连接映射
};
- UserManager 内存哈希
UserManager 是一个内存中的哈希表,用于快速查找和管理用户信息。每个 Worker 线程都有自己的 UserManager 实例,用于管理连接到该 Worker 的用户
UserManager 使用了两个全局的 std::unordered_map 来存储用户信息:
users:映射用户名到文件描述符(fd)。
fds:映射文件描述符到用户名。
这两个哈希表提供了快速的查找能力,使得可以根据用户名快速找到对应的 fd,也可以根据 fd 快速找到用户名
// 这是UserManager类里的全局变量
extern std::unordered_map<std::string, int> users;
extern std::unordered_map<int, std::string> fds;
class Worker {
public:
Worker(int id)
: epoll_worker(-1),
_id(id) {}
void make_queue(int cli) {
std::lock_guard<std::mutex> lock(cli_mutex);
if (make_nonblocking(cli) == -1) {
perror("Cli_make_nonblocking error");
return;
}
struct epoll_event ev;
ev.data.fd = cli;
ev.events = EPOLLIN | EPOLLET;
conn[cli] = std::make_shared<Connection>(cli, user_msg); // 使用 UserManager
if (epoll_ctl(epoll_worker, EPOLL_CTL_ADD, cli, &ev) < 0) {
std::cerr << "epoll_ctl_add_error in Worker.cc" << std::strerror(errno) << std::endl;
}
cli_fds.push_back(cli);
}
private:
UserManager user_msg; // 每个 Worker 有自己的 UserManager
std::unordered_map<int, std::shared_ptr<Connection>> conn;
};
- Redis 异步持久化
UserManager 使用 Redis 来持久化用户信息和好友关系。所有对用户信息的修改(如添加用户、删除用户、添加好友、删除好友)都会同步到 Redis 中,确保数据不会因为程序重启而丢失
RedisClient _redis;
void UserManager::addUser(int fd, const std::string& username) {
_redis.addUserToOnlineLists(username);
users[username] = fd;
fds[fd] = username;
}
void UserManager::removeUser(int cli) {
std::string username = fds[cli];
_redis.removeUserToOnlineLists(username);
users.erase(username);
fds.erase(cli);
}
bool UserManager::addFriend(const std::string& from_name, const std::string& to_name) {
_redis.setUserGroups("Friend", from_name, to_name);
_redis.setUserGroups("Friend", to_name, from_name);
return true;
}
bool UserManager::deleteFriend(const std::string& from_name, const std::string& to_name) {
_redis.removeUserGroups("Friend", from_name, to_name);
_redis.removeUserGroups("Friend", to_name, from_name);
return true;
}
三、零阻塞 / 零拷贝技巧
1. 发送路径
优化结果:减少了发送路径上的延迟,提高了发送速度
- 业务线程 → MessageCenter → 目标 Worker
业务线程将消息发送到 MessageCenter,然后由目标 Worker 直接写入非阻塞的文件描述符,避免了额外的队列拷贝
void Connection::Send_msg(const chat::Chat& chat) {
// ...省略部分代码...
std::string res = Seriamsg(to_name, msg, time);
res = Protocol::pack(res);
MessageCenter::instance().dispatch(_fd, to_fd, res);// 发送到目标 Worker
}
void Worker::eventloop() {
// ...省略部分代码...
if (!conn[fd]->readMessage()) {
remove_fd(fd);
}
}
2. 接收路径
优化结果:减少了接收路径上的延迟,提高了接收效率
- epoll_wait 批量返回
epoll_wait 可以批量返回就绪的事件,减少了系统调用的次数
void Worker::eventloop() {
_running = true;
struct epoll_event events[NUM];
while (_running) {
int n = epoll_wait(epoll_worker, events, NUM, TIME);
if (n < 0) {
if (errno == EINTR) continue;
}
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
// ...省略部分代码...
}
}
}
- 一次 read 读满缓冲区
通过一次 read 读取尽可能多的数据,减少了读取次数
bool Connection::readMessage() {
char buf[MAX_NUM];
ssize_t len = recv(_fd, buf, MAX_NUM, 0);
if (len > 0) {
msg.append(buf, len);
chat::Chat chat_msg;
while (!msg.empty()) {
if (!Protocol::unpack(msg, chat_msg)) break;
// ...省略部分代码...
}
} else if (len == 0) {
// ...省略部分代码...
} else if (len == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// ...省略部分代码...
} else {
std::cerr << "Recive error:" << std::strerror(errno) << std::endl;
break;
}
}
return true;
}
- Protocol::unpack 就地解析 → 零拷贝
使用 Protocol::unpack 直接在原缓冲区解析消息,避免了额外的内存拷贝
bool Protocol::unpack(std::string& buffer, chat::Chat& chat_msg) {
if (buffer.size() >= 4) {
uint32_t len = 0;
memcpy(&len, buffer.data(), 4);
len = ntohl(len);
if (buffer.size() < len + 4) {
return false;
}
std::string msg = buffer.substr(4, len);
Protocol::Parse_msg(msg, chat_msg);
buffer.erase(0, 4 + len);
} else {
return false;
}
return true;
}
3. 线程安全
优化结果:减少了锁的使用,提高了线程安全性和性能
- 每线程本地 map<fd, Connection>
- 仅 Master 与 Worker 交互时加锁(cli_mutex)
每个 Worker 线程都有自己的 conn 映射,存储每个客户端连接的 Connection 对象。这样可以避免多个线程访问同一个 Connection 对象,从而减少锁的使用
class Worker {
public:
Worker(int id)
: epoll_worker(-1),
_id(id) {}
void make_queue(int cli) {
std::lock_guard<std::mutex> lock(cli_mutex); // 仅在修改共享资源时加锁
if (make_nonblocking(cli) == -1) {
perror("Cli_make_nonblocking error");
return;
}
struct epoll_event ev;
ev.data.fd = cli;
ev.events = EPOLLIN | EPOLLET;
conn[cli] = std::make_shared<Connection>(cli, user_msg); // 每个 Worker 有自己的 conn
if (epoll_ctl(epoll_worker, EPOLL_CTL_ADD, cli, &ev) < 0) {
std::cerr << "epoll_ctl_add_error in Worker.cc" << std::strerror(errno) << std::endl;
}
cli_fds.push_back(cli);
}
private:
std::unordered_map<int, std::shared_ptr<Connection>> conn; // 每个 Worker 有自己的连接映射
std::mutex cli_mutex; // 仅在修改共享资源时加锁
};
在 Master 和 Worker 之间交互时,例如分配客户端连接到 Worker,会使用互斥锁 cli_mutex 来保护共享资源
void Master::distribute_worker(int cli_fd) {
std::lock_guard<std::mutex> lock(cli_mutex); // 加锁
int num = _worker++ % WORKER;
Worker_fd[cli_fd] = num;
auto work = workers[num];
work->make_queue(cli_fd);
}
4. 协议与格式
- 4字节长度 + protobuf
- 固定首部长度 → 快速定位
- protobuf 序列化小 → 网络包更小
使用固定长度的头部(4 字节)来表示消息长度,加上 Protobuf 序列化后的消息体。这种设计可以快速定位消息边界,减少解析开销
数据量小:Protobuf 序列化后的数据通常比 JSON 或 XML 更小,这意味着在网络上传输时需要更少的带宽,减少了传输时间。
解析速度快:Protobuf 的解析速度比 JSON 或 XML 快很多,这可以显著提高系统的性能。
std::string Protocol::pack(const std::string& payload) {
uint32_t len = htonl(payload.size());
std::string msg;
msg.resize(4);
memcpy(&msg[0], &len, 4);
msg += payload;
return msg;
}
bool Protocol::unpack(std::string& buffer, chat::Chat& chat_msg) {
if (buffer.size() >= 4) {
uint32_t len = 0;
memcpy(&len, buffer.data(), 4);
len = ntohl(len);
if (buffer.size() < len + 4) {
return false;
}
std::string msg = buffer.substr(4, len);
if (!chat_msg.ParseFromString(msg)) {
std::cerr << "Parse error!" << std::endl;
return false;
}
buffer.erase(0, 4 + len);
} else {
return false;
}
return true;
}
5. 资源复用
- SO_REUSEADDR
允许端口复用,方便服务器重启时快速绑定端口
void Master::init() {
listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 其他初始化代码...
}
四、总结
通过多种优化手段实现了极致的发送速度、高并发和低延迟。这些优化手段包括高效的并发模型、零阻塞和零拷贝技术、线程安全设计、高效的协议格式以及资源复用。这些设计共同提升了系统的性能和扩展性,使其能够高效地处理大量并发连接和实时消息
610

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



