【Master-Worker】 聊天服务器消息并发架构

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));
    // 其他初始化代码...
}

四、总结

通过多种优化手段实现了极致的发送速度、高并发和低延迟。这些优化手段包括高效的并发模型、零阻塞和零拷贝技术、线程安全设计、高效的协议格式以及资源复用。这些设计共同提升了系统的性能和扩展性,使其能够高效地处理大量并发连接和实时消息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值