Chap13-ChatServer

Chap13-ChatServer

ChatServer

这一节我们使用Asio编写ChatServer.

我们从main入手

#include "global/ConfigManager.h"
#include "server/AsioPool.h"
#include "server/Server.h"

#include <boost/asio.hpp>
#include <boost/asio/signal_set.hpp>
#include <grpc++/grpc++.h>
#include <spdlog/spdlog.h>
#include <thread>

int main()
{
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%s:%#] %v");
    spdlog::set_level(spdlog::level::debug);

    {
        auto& cfg = ConfigManager::GetInstance();
        auto pool = AsioPool::GetInstance();
        boost::asio::io_context ioc;
        boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
        signals.async_wait([&ioc, pool](const boost::system::error_code& /*error*/, int /*signal_number*/) {
            pool->Stop();
            ioc.stop();
        });

        auto port = cfg["ChatServer"]["port"];
        std::make_shared<Server>(ioc, std::stoi(port))->Start();
        ioc.run();
    }
    return 0;
}

很简单,就是启动了一个Server.以及一个AsioPool

我们先来看AsioPool

// .h
#ifndef ASIOPOOL_H
#define ASIOPOOL_H

#include "../global/Singleton.h"
#include <boost/asio.hpp>
#include <boost/asio/executor_work_guard.hpp>
#include <thread>
#include <vector>

class AsioPool : public Singleton<AsioPool> {
    friend Singleton<AsioPool>;

public:
    ~AsioPool();
    AsioPool(const AsioPool&) = delete;
    AsioPool& operator=(const AsioPool&) = delete;

    boost::asio::io_context& GetIOService();
    void Stop();

private:
    AsioPool(std::size_t size = std::thread::hardware_concurrency());

private:
    std::vector<boost::asio::io_context> _services;
    std::vector<std::thread> _threads;
    std::vector<std::unique_ptr<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>> _works;
    std::size_t _next_services;
};

#endif

// .cpp
#include "AsioPool.h"
#include <iostream>
#include <memory>
#include <spdlog/spdlog.h>

AsioPool::AsioPool(std::size_t size)
    : _services(size)
    , _works()
    , _next_services(0)
{
    _works.reserve(size);
    for (std::size_t i = 0; i < size; ++i) {
        _works.emplace_back(std::make_unique<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>(boost::asio::make_work_guard(_services[i])));
    }
    _threads.reserve(size);
    for (std::size_t i = 0; i < size; i++) {
        _threads.emplace_back([this, i] {
            _services[i].run();
        });
    }
}

boost::asio::io_context& AsioPool::GetIOService()
{
    auto& service = _services[_next_services];
    _next_services = (_next_services + 1) % _services.size();
    return service;
}

void AsioPool::Stop()
{
    for (auto& work : _works) {
        work->get_executor().context().stop();
        work.reset();
    }
    for (auto& thread : _threads) {
        thread.join();
    }
}

AsioPool::~AsioPool()
{
    Stop();
    SPDLOG_INFO("AsioPool Destruct");
}

我们知道,asio的socket创建的时候,需要给他一个io_context上下文服务才能进行通信。当socket有信息发送的时候,对应的io_context底层应有一个缓冲区将这些发送请求一一发送给对端。所以我们创建了多个线程,多个io_context,每个线程跑着一个io_context,做到并行。

那么main中也创建了一个io_context,这个io_context不用于通信,有两个用途:

  • 优雅退出:我们注册了信号,接收信号并退出程序
  • 用于接受别的连接:即使是接受别人发来的连接请求也需要io_context处理,我们用这个单独处理连接请求。连接请求处理后,Server内部会获取一个io_context分配给socket通信。

再来看Server

// h
#ifndef GATEWAYSERVER_H
#define GATEWAYSERVER_H

#include "../global/const.h"
#include "../session/Session.h"
#include <memory>
#include <mutex>
#include <unordered_map>

class Server : public std::enable_shared_from_this<Server> {
public:
    Server(net::io_context& ioc, uint16_t port);
    void Start();
    void ClearSession(const std::string& session_id);
    bool CheckValid(const std::string& session_id);

private:
    net::ip::tcp::acceptor _acceptor;
    net::io_context& _ioc;
    uint16_t _port;
    std::unordered_map<std::string, std::shared_ptr<Session>> _sessions;
    std::mutex _mutex;
};

#endif
// cpp
#include "Server.h"
#include "../session/Session.h"
#include "AsioPool.h"
#include <spdlog/spdlog.h>

Server::Server(net::io_context& ioc, uint16_t port)
    : _ioc(ioc)
    , _acceptor(ioc, net::ip::tcp::endpoint(net::ip::tcp::v4(), port))
    , _port(port)
{
    SPDLOG_INFO("Server Start Success,Listen on port:{}", _port);
}

void Server::Start()
{
    auto& io_context = AsioPool::GetInstance()->GetIOService();
    std::shared_ptr<Session> conn = std::make_shared<Session>(io_context, this);
    _acceptor.async_accept(conn->GetSocket(), [this, conn, self = shared_from_this()](const boost::system::error_code& ec) {
        try {
            if (ec) {
                self->Start();
                return;
            }
            conn->Start();
            SPDLOG_INFO("New connection from {},session:{}", conn->GetSocket().remote_endpoint().address().to_string(),conn->GetSessionId());

            std::unique_lock<std::mutex> lock(_mutex);
            _sessions.insert(std::make_pair(conn->GetSessionId(), conn));
            lock.unlock();

            self->Start();
        } catch (std::exception& e) {
            SPDLOG_ERROR("Exception: {}", e.what());
        }
    });
}

void Server::ClearSession(const std::string& session_id)
{
    std::lock_guard<std::mutex> lock(_mutex);
    _sessions.erase(session_id);
}

bool Server::CheckValid(const std::string& session_id)
{
    auto it = _sessions.find(session_id);
    if (it == _sessions.end()) {
        return false;
    }
    return true;
}

Start中就是AsioPool::GetInstance()->GetIOService();获取一个连接,创建一个Session用于通信。

接下来看Session

// h
#ifndef SESSION_H
#define SESSION_H

#include "../global/const.h"
#include "MsgNode.h"
#include <memory>
#include <queue>

class Server;
class LogicSystem;

class Session : public std::enable_shared_from_this<Session> {
    friend class LogicSystem;

public:
    Session(boost::asio::io_context& ioc, Server* server);
    ~Session();
    std::string GetSessionId() noexcept;
    void Start();
    void Close();
    void Send(const char* msg, int max_length, uint16_t msg_id);
    void Send(std::string msg, uint16_t msg_id);
    void HandleWrite(boost::system::error_code ec, std::shared_ptr<Session> self);
    net::ip::tcp::socket& GetSocket() noexcept;
    std::queue<std::shared_ptr<SendNode>>& GetSendQueue() noexcept;

private:
    void AsyncHead(std::size_t len);
    void AsyncBody(std::size_t len);

private:
    std::shared_ptr<RecvNode> _recv_msg_node;
    std::shared_ptr<MsgNode> _recv_head_node;
    char _data[MAX_LENGTH];

    std::queue<std::shared_ptr<SendNode>> _send_queue;
    std::mutex _send_lock;

    net::ip::tcp::socket _socket;
    std::string _session_id;
    int _uid;

    Server* _server;
    bool _stop;
    bool _head_parse;
};

class LogicNode {
    friend class LogicSystem;

public:
    LogicNode(std::shared_ptr<Session>, std::shared_ptr<RecvNode>);

private:
    std::shared_ptr<Session> _session;
    std::shared_ptr<RecvNode> _recv_node;
};

#endif


// cpp
#include "../session/Session.h"
#include "../server/LogicSystem.h"
#include "../server/Server.h"
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

#include "Session.h"
#include <spdlog/spdlog.h>

Session::Session(boost::asio::io_context& ioc, Server* server)
    : _socket(ioc)
    , _server(server)
    , _stop(false)
    , _head_parse(false)
    , _uid(0)
{
    _session_id = boost::uuids::to_string(boost::uuids::random_generator()());
    _recv_head_node = std::make_shared<MsgNode>(HEAD_TOTAL_LEN);
}

Session::~Session()
{
    Close();
}

void Session::Start()
{
    AsyncHead(HEAD_TOTAL_LEN);
}

void Session::Close()
{
    if (_stop) {
        return;
    }
    _stop = true;
    {
        std::lock_guard<std::mutex> lock(_send_lock);
        while (!_send_queue.empty()) {
            _send_queue.pop();
        }
    }

    // 安全关闭socket
    boost::system::error_code ec;
    _socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
    if (ec) {
        SPDLOG_WARN("Socket shutdown error: {}", ec.message());
    }

    _socket.close(ec);
    if (ec) {
        SPDLOG_WARN("Socket close error: {}", ec.message());
    }
    SPDLOG_INFO("Session {} disconnected!", _session_id);
}

void Session::Send(const char* msg, int max_length, uint16_t msg_id)
{
    std::lock_guard<std::mutex> lock(_send_lock);
    int size = _send_queue.size();
    if (size > MAX_SEND_SIZE) {
        SPDLOG_WARN("Session:{} Send Queue Fulled,This Failed", _session_id);
        return;
    }
    _send_queue.push(std::make_shared<SendNode>(msg, max_length, msg_id));
    if (size > 0) {
        return;
    }
    auto msgnode = _send_queue.front();
    boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len), std::bind(&Session::HandleWrite, this, std::placeholders::_1, shared_from_this()));
}

void Session::Send(std::string msg, uint16_t msg_id)
{
    Send(msg.data(), msg.size(), msg_id);
}

void Session::HandleWrite(boost::system::error_code ec, std::shared_ptr<Session> self)
{
    try {
        if (ec) {
            SPDLOG_WARN("Handle Write Filed,Errir is {}", ec.what());
            Close();
            _server->ClearSession(_session_id);
        }
        std::lock_guard<std::mutex> lock(_send_lock);
        _send_queue.pop();
        if (!_send_queue.empty()) {
            auto msgnode = _send_queue.front();
            boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len),
                std::bind(&Session::HandleWrite, this, std::placeholders::_1, shared_from_this()));
        }

    } catch (std::exception& e) {
        SPDLOG_WARN("Exception code:{}", e.what());
        Close();
        _server->ClearSession(_session_id);
    }
}

net::ip::tcp::socket& Session::GetSocket() noexcept
{
    return _socket;
}

std::queue<std::shared_ptr<SendNode>>& Session::GetSendQueue() noexcept
{
    return _send_queue;
}

void Session::AsyncHead(std::size_t len)
{
    boost::asio::async_read(_socket, boost::asio::buffer(_recv_head_node->_data, len), [self = shared_from_this(), this](boost::system::error_code error, size_t bytes_transferred) {
        if (error) {
            if (error == boost::asio::error::eof) {
                SPDLOG_WARN("Connection closed by perr");
            } else {
                SPDLOG_WARN("Error Reading header:{},bytes transferred:{}", error.message(), bytes_transferred);
            }
            _server->ClearSession(_session_id);
            Close();
            return;
        }
        if (bytes_transferred < HEAD_TOTAL_LEN) {
            SPDLOG_WARN("Read Length not matched");
            _server->ClearSession(_session_id);
            Close();
            return;
        }
        if (!_server->CheckValid(_session_id)) {
            SPDLOG_WARN("Invalid SessionId");
            _server->ClearSession(_session_id);
            Close();
            return;
        }

        uint16_t msg_id = 0;
        memcpy(&msg_id, _recv_head_node->_data, HEAD_ID_LEN);
        msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);
        uint16_t data_len = 0;
        memcpy(&data_len, _recv_head_node->_data + HEAD_ID_LEN, HEAD_DATA_LEN);
        data_len = boost::asio::detail::socket_ops::network_to_host_short(data_len);

        _recv_msg_node = std::make_shared<RecvNode>(data_len, msg_id);
        AsyncBody(data_len);
    });
}

void Session::AsyncBody(std::size_t len)
{
    boost::asio::async_read(_socket, boost::asio::buffer(_data, len), [self = shared_from_this(), this](boost::system::error_code error, size_t bytes_transferred) {
        if (error) {
            if (error == boost::asio::error::eof) {
                SPDLOG_WARN("Connection closed by perr");
            } else {
                SPDLOG_WARN("Error Reading Body:{},bytes transferred:{}", error.message(), bytes_transferred);
            }
            _server->ClearSession(_session_id);
            Close();
            return;
        }
        if (bytes_transferred < HEAD_TOTAL_LEN) {
            SPDLOG_WARN("Read Length not matched");
            _server->ClearSession(_session_id);
            Close();
            return;
        }
        if (!_server->CheckValid(_session_id)) {
            SPDLOG_WARN("Invalid SessionId");
            _server->ClearSession(_session_id);
            Close();
            return;
        }

        memcpy(_recv_msg_node->_data, _data, bytes_transferred);
        _recv_msg_node->_cur_len += bytes_transferred;

        auto logic_node = std::make_shared<LogicNode>(shared_from_this(), _recv_msg_node);
        LogicSystem::GetInstance()->PostMsgToQueue(logic_node);

        /* 继续接受head->body->head->body... */
        AsyncHead(HEAD_TOTAL_LEN);
    });
}

std::string Session::GetSessionId() noexcept
{
    return _session_id;
}

LogicNode::LogicNode(std::shared_ptr<Session> session, std::shared_ptr<RecvNode> recv_node)
    : _session(session)
    , _recv_node(recv_node)
{
}

关于读写,后面再说。先看这里的send函数。

_send_queue.push(std::make_shared<SendNode>(msg, max_length, msg_id));

我们将其放入了一个队列之中,为什么呢?因为多个数据,我们需要保证异步发送数据的有序性。比如hello world如果没有顺序保证,可能就会发送成为world hello.

仔细观察,在AsyncBody最后有一个步骤,我们将构造一个逻辑节点,发送给LogicSystem的队列。

LogicSystem::GetInstance()->PostMsgToQueue(logic_node);

我们为了解耦,这个Session负责接受原始数据,真正的逻辑处理交给逻辑类进行处理。逻辑类处理完之后,再次发送个这个Session的队列进行字节流的发送。

那么AsyncHead和AsyncBody操作呢,简单来说,为了防止粘包(读取部分或读取过多),我们使用async_read可以读取指定字节的数据。这样就可以配合一定的规则,解决粘包。我们的方法是,首先接受头部,一共4个字节,前两个字节是发送请求的id,后两个字节是接下来要接受的数据长度len.接下来再次调用async_read读取指定的len长度的字节就可以了。AsyncHead就是读取前四个字节的头部信息,AsyncBody就是读取数据内容的。

为了配合读取头部,和读取实际数据我们创建了MsgNode结构方便处理:

// h
#ifndef MSGNODE_H
#define MSGNODE_H

#include "../global/const.h"

#include <boost/asio.hpp>
#include <string>

class LogicSystem;
class MsgNode {
    friend class Session;
    friend class LogicSystem;

public:
    MsgNode(uint16_t max_len);
    virtual ~MsgNode();
    void Clear();

protected:
    uint16_t _cur_len;
    uint16_t _total_len;
    char* _data;
};

class RecvNode : public MsgNode {
    friend class Session;
    friend class LogicSystem;

public:
    RecvNode(uint16_t max_len, uint16_t msg_id);

private:
    uint16_t _msg_id;
};

class SendNode : public MsgNode {
    friend class Session;
    friend class LogicSystem;

public:
    SendNode(const char* msg, uint16_t max_len, uint16_t msg_id);

private:
    uint16_t _msg_id;
};

#endif

// cpp
#include "MsgNode.h"

MsgNode::MsgNode(uint16_t max_len)
    : _total_len(max_len)
    , _cur_len(0)
    , _data(new char[_total_len + 1])
{
    _data[_total_len] = '\0';
}

MsgNode::~MsgNode()
{
    delete[] _data;
}

void MsgNode::Clear()
{
    ::memset(_data, 0, _total_len);
    _cur_len = 0;
}

RecvNode::RecvNode(uint16_t max_len, uint16_t msg_id)
    : MsgNode(max_len)
    , _msg_id(msg_id)
{
}

SendNode::SendNode(const char* msg, uint16_t max_len, uint16_t msg_id)
    : MsgNode(max_len)
    , _msg_id(msg_id)
{
    uint16_t msg_id_host = boost::asio::detail::socket_ops::host_to_network_long(msg_id);
    memcpy(_data, &msg_id_host, HEAD_ID_LEN);

    uint16_t max_len_host = boost::asio::detail::socket_ops::host_to_network_long(max_len);
    memcpy(_data + HEAD_ID_LEN, &max_len_host, HEAD_DATA_LEN);
    memcpy(_data + HEAD_TOTAL_LEN, msg, max_len);
}

好的我们再来看LogicSystem类的实现

// h
#include "../global/Singleton.h"
#include "../global/const.h"
#include "../session/Session.h"
#include <condition_variable>
#include <functional>
#include <mutex>
#include <nlohmann/json.hpp>
#include <queue>
#include <string>
#include <thread>
#include <unordered_map>

typedef std::function<void(std::shared_ptr<Session>, uint16_t msg_id, const std::string& msg)> FuncBack;

class LogicSystem : public Singleton<LogicSystem> {
    friend class Singleton<LogicSystem>;

public:
    void PostMsgToQueue(std::shared_ptr<LogicNode> msg);
    void RegisterCallBacks();
    void DealMsg();

public:
    LogicSystem(std::size_t size = std::thread::hardware_concurrency());
    ~LogicSystem();

private:
    std::queue<std::shared_ptr<LogicNode>> _queue;
    std::mutex _mutex;
    std::condition_variable _cv;
    std::vector<std::thread> _work_threads;
    std::size_t _size;
    bool _stop;
    std::unordered_map<MsgId, FuncBack> _function_callbacks;
};

// cpp
#include "LogicSystem.h"
#include <spdlog/spdlog.h>
void LogicSystem::PostMsgToQueue(std::shared_ptr<LogicNode> msg)
{
    std::unique_lock<std::mutex> lock(_mutex);
    _queue.push(msg);
    _cv.notify_one();
}

void LogicSystem::RegisterCallBacks()
{
    // 登陆请求
    _function_callbacks[MsgId::ID_CHAT_LOGIN] = [this](std::shared_ptr<Session> session, uint16_t msg_id, const std::string& msg) {
        json j(msg);
        auto uid = j["uid"].get<int>();
        auto token = j["token"].get<std::string>();
        SPDLOG_INFO("User {} Login with token {}", uid, token);

        std::string return_str = j.dump(4);
        session->Send(return_str, msg_id);
    };
}

void LogicSystem::DealMsg()
{
    while (true) {
        std::unique_lock<std::mutex> lock(_mutex);
        _cv.wait(lock, [this]() {
            return _stop || !_queue.empty();
        });
        if (_stop && _queue.empty()) {
            break;
        }

        if (!_queue.empty()) {
            std::queue<std::shared_ptr<LogicNode>> local_queue;
            local_queue.swap(_queue);
            lock.unlock();
            while (!local_queue.empty()) {
                auto msg = local_queue.front();
                local_queue.pop();

                auto it = _function_callbacks.find(static_cast<MsgId>(msg->_recv_node->_msg_id));
                if (it != _function_callbacks.end()) {
                    it->second(msg->_session, msg->_recv_node->_msg_id, std::string(msg->_recv_node->_data, msg->_recv_node->_total_len));
                }
            }
        }
    }
}

LogicSystem::LogicSystem(std::size_t size)
    : _stop(false)
    , _size(size)
{
    RegisterCallBacks();
    _work_threads.reserve(size);
    for (std::size_t i = 0; i < size; ++i) {
        _work_threads.emplace_back(&LogicSystem::DealMsg, this);
    }
}

LogicSystem::~LogicSystem()
{
    _stop = true;
    _cv.notify_all();
    for (auto& p : _work_threads) {
        p.join();
    }

    SPDLOG_INFO("LogicSystem Stopped");
}

我们在逻辑类中创建了多个线程,每个线程都在等待获取队列中的锁,得到锁之后,去除一个任务,然后释放锁,前去处理任务。我们创建了一个存放处理类型和对应回调函数的容器,线程只需要根据这个类型id,就能找到回调进行处理。

处理完成之后,还要Send回原来的Session,进行字节流的发送。

session->Send(return_str, msg_id);

在DealMsg中处理任务的时候,我们这里为了减少锁的静态,我们直接创建了一个local_queue,将待办任务队列的数据直接swap给这个本地队列,然后直接释放锁,剩下就是线程自己处理的问题了。有效的减少了对锁的竞争。

联调

接下来我们进行测试。打开GateWayServer/StatusServer/ChatServer,然后前端进行登陆测试:

image-20251026203101020

image-20251026203147663

image-20251026203206057

image-20251026203216179

image-20251026203244195

可以看到正常运行了。

posted @ 2025-12-24 23:16  大胖熊哈  阅读(12)  评论(0)    收藏  举报