你在暴风城里按下 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_SESSION、CMSG_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 这一条通道。