AzerothCore学习笔记·架构02:网络层与会话管理——一个数据包从网卡到 Handler 的旅程

你在暴风城里按下 W 键,角色往前迈了一步。从键盘到客户端、从客户端到服务器、从服务器再回到你的屏幕上------整个过程不到 100 毫秒。

但这一「步」的数据包到了服务器端,不是直接丢给「移动角色」的处理函数了事的。它经过了 TCP 字节流的拼接、6 字节包头的拆解、加解密的校验、Opcode 的路由分发------最终才交到该处理它的 Handler 手上。

这一篇就追踪这条路径:从网卡到 Handler,一个数据包经历了什么。


两层架构:Socket 和 Session

AzerothCore 的网络层有两套对象:

  • Socket 层WorldSocket):处理底层的 TCP 连接、字节收发、加密解密
  • Session 层WorldSession):处理业务逻辑,一个玩家对应一个 Session

WorldSocket.h 的声明里有一行关键代码:

cpp 复制代码
std::mutex _worldSessionLock;
WorldSession* _worldSession;
bool _authed;

_authed 表示这个 Socket 是否已完成认证。未认证时,_worldSession 为空------客户端连上来了但还没有对应的玩家会话。


完整旅程:六个步骤

第一步:TCP 字节流进入缓冲区

客户端发送的是一个 TCP 字节流,没有边界------没有「一个包开始」「一个包结束」的标记。Socket.h 里用了一个 MessageBuffer 来接收:

cpp 复制代码
void AsyncRead()
{
    _socket.async_read_some(
        boost::asio::buffer(_readBuffer.GetWritePointer(), _readBuffer.GetRemainingSpace()),
        std::bind(&Socket::ReadHandlerInternal, ...)
    );
}

每次从网卡读一批字节,写入 _readBuffer。Buffer 不够用了就扩展,满了就等下一轮。

第二步:读取包头(Header)

TCP 是流协议,但 WoW 客户端发的每个包有固定结构:ClientPktHeader

cpp 复制代码
#pragma pack(push, 1)
struct ClientPktHeader
{
    uint16 size;   // 包体长度(不含 opcode)
    uint32 cmd;    // 操作码(Opcode)
};
#pragma pack(pop)

WorldSocket::ReadHeaderHandler() 负责读取这 6 个字节:

cpp 复制代码
ClientPktHeader* header = reinterpret_cast<ClientPktHeader*>(_headerBuffer.GetReadPointer());
_authCrypt.DecryptRecv(_headerBuffer.GetReadPointer(), sizeof(ClientPktHeader));
header->size -= sizeof(header->cmd);
_packetBuffer.Resize(header->size);

注意第二行:包头被解密了_authCrypt 在登录认证完成后才初始化,初始化之前收发的包不加密,初始化之后才加解密。

第三步:读取包体(Payload)

Header 读取完毕后,_packetBuffer 分配好大小,继续读取剩余的包体字节。完整包收到后,进入 ReadDataHandler()------这里开始分发。

第四步:Opcode 分发

ReadDataHandler() 是个大 switch

cpp 复制代码
switch (opcode)
{
    case CMSG_PING:
        return HandlePing(packet) ? Ok : Error;
    case CMSG_AUTH_SESSION:
        HandleAuthSession(packet);
        return WaitingForQuery;  // 等数据库查询结果
    default:
        // 交给 WorldSession 处理
        std::lock_guard<std::mutex> sessionGuard(_worldSessionLock);
        if (_worldSession)
            _worldSession->HandlePacket(packet);
        return Ok;
}

只有两个 Opcode 在 WorldSocket 这一层直接处理:

  • CMSG_AUTH_SESSION:登录认证,异步查数据库,结果返回后再继续
  • CMSG_PING:保活心跳,最简单的 opcode

其他所有 opcode------走路、施法、交易、说话------全部推给 WorldSession::HandlePacket()

第五步:WorldSession 处理业务逻辑

WorldSession 是一个玩家的全部上下文:

cpp 复制代码
class WorldSession
{
    Player* _player;           // 当前操控的角色(选角后才有值)
    uint32 _accountId;          // 账号 ID(整个 Session 生命周期不变)
    WorldSocket* _socket;       // 回指 Socket
    // ... 还有背包、任务、buff、冷却时间等所有玩家状态
};

HandlePacket() 是 Message Route 的入口:根据 Opcode 找到对应的处理函数指针并调用。

第六步:响应包返回

Server → Client 的包走 WorldSocket::SendPacket()

cpp 复制代码
void WorldSocket::SendPacket(WorldPacket const& packet)
{
    // 压缩
    if (packet.NeedsCompression())
        CompressIfNeeded();

    // 加密
    if (_authCrypt.IsInitialized())
        _authCrypt.EncryptSend(...);

    // 发出去
    QueuePacket(std::move(buffer));
}

为什么设计成两层

1. 认证阶段没有 Session

登录过程中,客户端连上了,但账号还没验证完,_worldSession 还不存在。如果包分发逻辑需要「必须有个 Session 才能处理」,那么 CMSG_AUTH_SESSION 本身就没法路由了。

所以把认证相关的两个 Opcode(CMSG_AUTH_SESSIONCMSG_PING)单独在 Socket 层处理,等认证完成后再创建 WorldSession 并关联到 Socket。

2. 一个 TCP 连接 ≠ 一个玩家

一个玩家可能中途掉线、又重连------Socket 变了,但 _accountId 不变,WorldSession 可以通过 Account ID 被找回。同一个账号在 SessionMap 里最多只有一个在线 Session。

3. Session 可以离线保留

WorldSessionMgr 里有两个 Map:

cpp 复制代码
SessionMap _sessions;              // 在线 Session
SessionMap _offlineSessions;       // 离线 Session(掉线后短暂保留)

玩家掉线后,_offlineSessions 会保留 Session 几秒钟。这段时间如果玩家重连,直接把离线 Session 激活,省去重新加载角色的数据库查询。


登录流程的 Socket → Session 转换

CMSG_AUTH_SESSION 认证通过后,WorldSocket 里做了一件关键的事:

cpp 复制代码
// 创建 WorldSession
WorldSession* newSession = new WorldSession(accountId, ...);
sWorldSessionMgr->AddSession(newSession);

// 把 Session 绑定到 Socket
_worldSession = newSession;
_authed = true;

从这一刻起,这个 TCP 连接就和玩家的 Session 绑定了。所有后续的客户端包都会进入 WorldSession::HandlePacket()


SessionMgr 的全局管理

WorldSessionMgr 是所有 Session 的管理器:

cpp 复制代码
class WorldSessionMgr
{
    SessionMap _sessions;           // accountId → WorldSession*
    Queue _queuedPlayer;            // 等待队列
    uint32 _playerLimit;            // 服务器最大人数上限

    void AddSession(WorldSession* session);
    void KickSession(uint32 accountId);
    void SendGlobalMessage(WorldPacket const* packet);  // 全服广播
};

玩家登录时,如果服务器满了(_sessions.size() >= _playerLimit),会被放入 _queuedPlayer 等待队列,有空位了再依次放进来。


小结:三层责任分离

层次 职责
网络传输 Socket<T>(模板基类) TCP 字节流收发、Buffer 管理
世界连接 WorldSocket 包头读取、OpCode 分发、加密解密、压缩
玩家会话 WorldSession 业务逻辑处理、角色状态、Handler 路由

三层各司其职:最底层只管字节,中层负责「这是什么包」,最上层负责「这个包要干什么」。

从 TCP 字节流到玩家移动指令,中间经过 Socket 拆包、Opcode 路由、Session 分发三层。而 Auth Session 一旦验证通过,Socket 和 Session 就绑死了------后续的所有操作,都只走 WorldSession 这一条通道。