找回密码
 register
搜索
查看: 76|回复: 0

洱海月 v0-07-0300 `packet.CLConnect` 详解

[复制链接]
  • 打卡等级:本地老炮
  • 打卡总天数:533
  • 打卡月天数:22
  • 打卡总奖励:530
  • 最近打卡:2026-06-24 01:45:59
Waylee 发表于 2026-4-1 13:00 | 显示全部楼层 |阅读模式 | Google Chrome | Windows 10

马上注册,查看网站隐藏内容!!

您需要 登录 才可以下载或查看,没有账号?register

×

packet.CLConnect 详解

这篇文档是写给第一次接触这个项目的人看的。

目标不是只告诉你 "packet.CLConnect 里有哪几个字段",而是把下面这些问题一次讲清楚:

  • CLConnect 是什么,处在登录流程的哪个位置。
  • 它的包体到底长什么样,按字节怎么排。
  • packet.lua 里的 new / ctor / bis / bos 分别在干什么。
  • 为什么 net_provider 现在被标成了“未使用”。
  • 为什么 account / version / ip 虽然都被读出来了,但真正参与当前 Lua 登录服逻辑的只有一部分。
  • 旧 Login C++ 参考代码和当前项目是什么关系,哪些能直接借,哪些不能照抄。

1. 先记住它在什么位置

CLConnect 是客户端发给登录服的一个“连接阶段包”。

在当前项目里,它对应的包号是:

packet.XYID_CLCONNECT = 355

定义位置:

  • services/login/packet.lua

处理位置:

  • services/login/server.lua
  • services/login/logind.lua

你可以把它理解成:

  1. 客户端 TCP 连接建立。
  2. 客户端先发一个 CLConnect
  3. 登录服收到后,先记录一些连接阶段信息。
  4. 登录服回一个 LCRetConnect
  5. 客户端再继续发 CLAskLogin,进入真正的登录认证阶段。

也就是说:

  • CLConnect 不是“真正校验账号密码”的包。
  • 它更像“登录前握手 / 连接信息上报 / 服务端返回登录入口信息”的第一步。

2. 相关文件分别负责什么

为了不一上来就陷进细节里,先把几个关键文件记住:

services/login/packet.lua

这里定义了所有登录相关包的 Lua 结构。

CLConnect 来说,这里负责两件事:

  • 定义字段长什么样。
  • 负责把原始二进制包解析成 Lua 对象。

services/login/server.lua

这里是通用收发包层。

它负责:

  • xy_id 找到对应的包定义。
  • 调用 p.new() 创建包对象。
  • 调用 p:bis(message) 把二进制包体解析成字段。

services/login/logind.lua

这里是真正的登录业务逻辑。

它会根据 packet.xy_id 判断“收到的是哪一种包”,然后进入不同分支。

CLConnect 就是在这里被消费的。

lualib/iostream.lua

这是底层二进制读写工具。

它负责:

  • readuint() 这种“按数字读”
  • read(0x20) 这种“按固定长度字节串读”
  • writeuint() / write() 这种反向写包

如果你第一次看 packet.lua,一定要知道:
packet.lua 本身并不直接处理字节,它只是调用 iostream.lua 提供的二进制读写接口。


3. CLConnect 在 Lua 里的原始定义

当前代码如下:

packet.CLConnect = {
    xy_id = packet.XYID_CLCONNECT,
    new = function()
        local o = {}
        setmetatable(o, {__index = packet.CLConnect})
        o:ctor()
        return o
    end,
    ctor = function(self)
        self.net_provider = 0 -- 网络线路/运营商线路标识(未使用)。
        self.account = ""
        self.version = ""
        self.ip = ""
    end,
    bis = function(self, buffer)
        local stream    = bistream.new()
        stream:attach(buffer)
        self.net_provider = stream:readuint()
        self.account    = stream:read(0x32)
        self.version    = stream:read(0x20)
        self.ip         = stream:read(0x18)
    end,
    bos = function(self)
        local stream = bostream.new()
        stream:writeuint(self.net_provider)
        stream:write(self.account, 0x32)
        stream:write(self.version, 0x20)
        stream:write(self.ip, 0x18)
    end
}

这段代码虽然不长,但里面每一行都有意义。后面我们逐个拆。


4. 先看最重要的结论:包体布局

CLConnect 的包体在当前项目里可以按下面的方式理解:

偏移 长度 字段 类型 说明
0x00 0x04 net_provider uint32 网络线路/运营商线路标识
0x04 0x32 account 固定长度字节串 账号字段,当前 Lua 登录服不使用它做认证
0x36 0x20 version 固定长度字节串 连接阶段版本串,当前 Lua 登录服会记录它
0x56 0x18 ip 固定长度字节串 客户端自报 IP,当前 Lua 登录服不信任它

包体总长度:

0x04 + 0x32 + 0x20 + 0x18 = 0x6E

也就是十进制 110 字节。

这个布局不是只凭 packet.lua 猜出来的。

我们前面的逆向对照里,已经确认当前客户端版本的 Game.exeCLConnect 收发时,包体顺序也是:

4 + 0x32 + 0x20 + 0x18

所以这里可以把它当成“当前 Lua 定义与当前客户端版本一致”的结果来看。

这里一定要注意一个新手最容易混淆的点:

  • xy_id = 355 不是这个 110 字节包体的一部分。
  • xy_id 是外层包号。
  • bis(buffer) 里收到的 buffer,已经是“剥掉包号之后的包体”了。

所以你在 bis() 里看到的 4 + 0x32 + 0x20 + 0x18,就是纯包体布局,不包含包头。


5. xy_id 到底是怎么起作用的

很多人第一次看 packet.lua,会误以为 xy_id 也是 bis() 从字节流里读出来的。

其实不是。

真正流程在 services/login/server.lua

local function unpack_message(id, message)
    local xy_id = id
    local p = packets[xy_id]
    if p then
        p = p.new()
        p:bis(message)
        return p
    end
end

这段逻辑说明:

  • 外层网络层先把 idmessage 分开了。
  • id 用来决定应该实例化哪种包对象。
  • 真正传给 bis() 的,只是包体 message

所以 packet.CLConnect.xy_id = 355 的作用是:

  • 告诉分发器:收到 355 时,应该创建 packet.CLConnect 对象。

它不是 CLConnect 包体内部的一个字段。


6. new()ctor() 到底在做什么

new()

new = function()
    local o = {}
    setmetatable(o, {__index = packet.CLConnect})
    o:ctor()
    return o
end,

这里做了三件事:

  1. 先创建一个空表 o
  2. 给这个表挂上元表,让 o.xxx 找不到时去 packet.CLConnect.xxx 里找。
  3. 调用 ctor() 给字段赋默认值。

也就是说,packet.CLConnect 本质上不是 class 关键字创建的类,而是“用 Lua table + metatable 模拟对象”。

ctor()

ctor = function(self)
    self.net_provider = 0 -- 网络线路/运营商线路标识(未使用)。
    self.account = ""
    self.version = ""
    self.ip = ""
end,

ctor() 的意义主要有两个:

  • 给对象一个稳定、可预期的初始状态。
  • 让调用方即使在 bis() 之前访问字段,也不会遇到 nil

这里的默认值也暗示了字段类型:

  • net_provider 是数字
  • account / version / ip 是字符串

7. bis() 是真正的“收包解析器”

bis 可以理解成 binary input stream。

代码:

bis = function(self, buffer)
    local stream    = bistream.new()
    stream:attach(buffer)
    self.net_provider = stream:readuint()
    self.account    = stream:read(0x32)
    self.version    = stream:read(0x20)
    self.ip         = stream:read(0x18)
end,

这段逻辑按顺序做了四件事:

  1. 创建一个 bistream
  2. 把原始包体 buffer 挂上去
  3. 从当前位置读取一个 uint32
  4. 再顺序读取三个固定长度字节串

最关键的点是:

  • 这是“顺序读取”
  • 没有字段名,没有 tag,没有 JSON key
  • 读的时候必须和客户端写的时候顺序完全一致

一旦顺序错一位,后面所有字段都会整体错位。

这就是二进制协议最典型的特征。


8. readuint()read(0x32) 的区别

这个细节对第一次看项目的人非常重要。

readuint()

lualib/iostream.lua 里:

function bistream:readuint()
    local start = self.pos
    local s = string.sub(self.buffer, start)
    local n = string.unpack("I4", s)
    self.pos = self.pos + 4
    return n
end

这意味着:

  • 从当前偏移读取 4 个字节
  • uint32 解释
  • 读取后游标向后移动 4 个字节

在当前项目实际运行环境里,可以把它理解成“按小端 4 字节无符号整数读”。

所以:

  • net_provider 是数值字段
  • 它不是原始字符串
  • 也不是 4 字节原样 hex 文本

read(0x32)

lualib/iostream.lua 里:

function bistream:read(len)
    local start = self.pos
    local endd = self.pos + len - 1
    local s = string.sub(self.buffer, start, endd)
    self.pos = self.pos + len
    return s
end

这意味着:

  • 它只是把这 len 个字节原样切出来
  • 不做数值解释
  • 不自动去掉末尾 \0

所以 account / version / ip 在 Lua 里其实都是“固定长度原始字节串”。

你后续如果要把它们转成更自然的字符串,经常还要自己处理:

  • 结尾补零
  • 截断
  • string.match
  • 编码问题

9. 每个字段分别应该怎么理解

下面按字段逐个讲。

9.1 net_provider

当前字段名:

self.net_provider = 0 -- 网络线路/运营商线路标识(未使用)。

这个字段之前叫 unknow,后来改成了 net_provider

改名原因不是拍脑袋,而是有逆向依据:

  • 旧 Login C++ 参考里,CLConnect 有一个明确命名的字段 mNetProvider
  • 老的 CLConnectHandler::Execute 会把这个字段存起来
  • 后面再走按线路选择代理入口的逻辑

所以这个字段的语义不是“随机值”,也不是“验证码”,而是:

  • 客户端上报自己想走哪条线
  • 或者客户端声明自己当前归属哪类网络线路

可以把它理解成:

  • 电信线
  • 网通/联通线
  • 教育网
  • 默认线路

这里要特别强调:

当前 Lua 登录服虽然已经能把它读出来,但没有真正使用它。

也就是说,现阶段它只是“解析出来了”,并没有进入业务分支。

这也是为什么注释里写了 (未使用)

9.2 account

self.account = stream:read(0x32)

这个字段长度固定是 0x32,也就是 50 字节。

新手很容易下意识以为:

  • 这里就是登录账号
  • 服务端后面应该会拿它去查库

但当前项目并不是这样走的。

当前 Lua 登录服真正做账号登录,是在后续 CLAskLogin 分支里。

也就是说:

  • CLConnect.account 会被解析
  • 但当前 logind.lua 处理 CLConnect 时没有使用它

如果你问“那它为什么还在包里”:

  • 从当前代码能确定的是“它存在且被客户端发来了”
  • 但从当前 Lua 逻辑看,它并不是认证主路径需要的字段

更稳妥的说法是:

  • 它是协议里的连接阶段冗余/预备字段
  • 当前服务端没有消费它

这里我特意不用更强的结论,是因为“它为什么存在”属于推测问题;我们目前能确定的是“当前 Lua 登录服没有用它”。

9.3 version

self.version = stream:read(0x20)

这是 CLConnect 里当前 Lua 登录服唯一明确保存下来的字段。

logind.lua 里:

if packet.xy_id == packet_def.XYID_CLCONNECT then
    connect_versions[fd] = packet.version
    ...
end

这说明:

  • 它是连接阶段版本串
  • 服务端把它按连接 fd 记录起来
  • 到后面真正处理 CLAskLogin 时,再拿出来一起写日志

注意它和后面 CLAskLogin.uVersion 不是同一种东西:

  • CLConnect.version 是固定 0x20 字节字符串
  • CLAskLogin.uVersion 是一个数值版本号

两者不要混为一谈。

9.4 ip

self.ip = stream:read(0x18)

它是客户端自己上报的一个固定长度 IP 字段,长度 24 字节。

但当前 Lua 登录服并不信这个字段。

实际业务里用的是:

local ip, port = string.match(handshake[fd], "(.+):(.+)")

也就是服务端从 socket 层记录的真实对端地址。

这点非常重要。

如果你第一次接项目,很容易误以为:

  • packet.ip 就是服务端最终拿来判定来源 IP 的值

其实不是。

当前代码里真正被信任的是:

  • handshake[fd]
  • 也就是连接建立时服务端看到的对端地址

而不是客户端自己在包里写的 ip

这是比较合理的,因为客户端自报 IP 本来就不可信。


10. CLConnect 在当前 Lua 登录服里的真实处理链

这一段是最值得新手反复看一遍的。

第一步:收到外层包号和包体

server.lua 里,网络层把消息拆成:

  • id
  • message

然后:

local p = packets[xy_id]
p = p.new()
p:bis(message)

到这里为止,packet.CLConnect 只是一个被正确解析出来的 Lua 对象。

第二步:进入 logind.luaCLConnect 分支

逻辑大致如下:

if packet.xy_id == packet_def.XYID_CLCONNECT then
    connect_versions[fd] = packet.version
    ret = packet_def.LCRetConnect.new()
    ...
    ret.login_challenge = 3474034634
    loginservice.send(fd, idx, ret)
end

这一段里,你能看到当前服务端实际上只做了这些事:

  1. 记录 packet.version
  2. 构造一个 LCRetConnect
  3. 回给客户端登录 IP / 端口 / challenge

没有做的事包括:

  • 没有使用 packet.net_provider
  • 没有使用 packet.account
  • 没有使用 packet.ip

也就是说,CLConnect 在当前 Lua 登录服里的作用更接近:

  • 作为连接阶段握手包存在
  • 提供一个版本串给后面日志使用
  • 然后触发服务端回 LCRetConnect

第三步:等客户端发 CLAskLogin

真正开始账号密码处理,是后面的 CLAskLogin 分支。

这也是为什么你会看到:

  • CLConnect 里虽然也有 account
  • 但当前登录服实际认证并不是用它完成的

11. 为什么 net_provider 明明有语义,却还标“未使用”

这是一个很典型、也很值得新手习惯的项目现象:

  • 协议字段有明确语义
  • 但当前 Lua 服务端并没有把它接入业务逻辑

这两件事并不矛盾。

CLConnect.net_provider 来说,现在的状态就是:

已经明确的部分

  • 它是一个 4 字节数值字段
  • 它不是随机数
  • 它对应线路/运营商线路选择语义

当前没接上的部分

  • Lua 登录服没有根据它切换 login_ip
  • Lua 登录服没有根据它切换 login_port
  • Lua 登录服没有把它存到 fd 关联状态里
  • Lua 登录服没有做基于线路的策略判断

所以注释写“未使用”是为了提醒后来的人:

  • 字段不是没意义
  • 只是当前这套 Lua 实现里还没有消费它

12. 旧 Login C++ 参考代码应该怎么用

这一块非常容易踩坑,所以单独讲。

旧 Login 参考里,CLConnect::Read 大致是:

Read(&this->mMiBao, 1);
Read(&this->mNetProvider, 4);

如果你第一次看到这个,很容易得出一个错误结论:

那当前 Lua 里的 CLConnect 也应该只有 5 个字节。

这是不对的。

正确理解是:

旧 Login 参考能提供什么

  • 它能提供字段语义锚点
  • 尤其是 mNetProvider 这个名字非常有价值

旧 Login 参考不能直接照抄什么

  • 不能把它的整包长度原样套到当前 Lua
  • 不能假设当前客户端的 CLConnect 还和旧 Login 一模一样

我们现在的判断是:

  • 当前 Lua 里的 CLConnect 包体更长
  • 现客户端确实会发送 4 + 0x32 + 0x20 + 0x18 这样的布局
  • 旧 Login 参考主要用来帮助确认“前面那个 4 字节字段的语义”

所以你在做项目接手时,最好形成这个习惯:

  • 用旧代码确认“这个字段大概是干什么的”
  • 用当前客户端 / 当前 Lua 代码确认“现在这个版本具体长什么样”

两边都要看,不能只看一边。


13. 为什么 CLConnect.bos() 里没有 return,现在却没出问题

这是一个非常值得新手注意的小细节。

当前 CLConnect.bos() 是这样写的:

bos = function(self)
    local stream = bostream.new()
    stream:writeuint(self.net_provider)
    stream:write(self.account, 0x32)
    stream:write(self.version, 0x20)
    stream:write(self.ip, 0x18)
end

注意:

  • 它没有像很多别的包一样 return stream:get()

如果你去看 loginserver.send()

local message, size = packet:bos()

那你会发现:

  • 如果真拿 CLConnect 去调用 loginservice.send()
  • 那这里理论上会拿不到 message

为什么当前项目没炸?

因为当前服务端并不会主动给客户端发 CLConnect

当前实际收发方向是:

  • CLConnect:客户端 -> 服务端
  • LCRetConnect:服务端 -> 客户端

所以 CLConnect.bos() 现在更像一个“没被用到的反向构包函数”。

这也是你以后维护时要记住的一点:

  • 当前没出问题,不等于这段代码天然没问题
  • 只是因为当前运行路径没走到它

如果以后你真要让服务端主动构造 CLConnect,那这里应该补上:

return stream:get()

14. 为什么 version 被保存,而 account / ip / net_provider 没有

从当前实现看,登录服对 CLConnect 的取舍标准很明显:

  • 能直接服务当前流程的,就留
  • 当前流程用不到的,就先不接

version

被保存到:

connect_versions[fd]

用途是后面记录登录日志时带上连接阶段版本信息。

account

当前没有保存,因为真正登录认证使用的是后续 CLAskLogin 里的账号相关字段。

ip

当前没有保存,因为服务端更信任 socket 层看到的真实地址,而不是客户端自报。

net_provider

当前没有保存,因为这套 Lua 登录服还没有做“按线路回不同登录入口”的逻辑。


15. 如果以后要启用 net_provider,应该从哪里开始改

如果未来你要把这个字段真正用起来,建议从下面这个方向接:

第一步:在 CLConnect 分支里先保存下来

例如:

connect_net_provider[fd] = packet.net_provider

第二步:回 LCRetConnect 时按线路决定返回哪个地址

就是在构造:

  • ret.login_ip
  • ret.login_port

之前,根据 packet.net_provider 选线路。

第三步:断开或登录完成后清理状态

connect_versions[fd] 一样,在合适时机清掉。

第四步:增加日志

建议至少打出:

  • fd
  • net_provider
  • 最终下发的 login_ip
  • 最终下发的 login_port

这样后面排线问题时会轻松很多。


16. 新手最容易犯的几个误区

误区 1:xy_idbis() 里读

不是。

xy_id 是外层包号,先由网络层拆出来,再决定用哪个包对象去解析 message

误区 2:account 一定是当前登录服真正拿来查库的账号

不是当前这套逻辑。

真正认证是后面的 CLAskLogin

误区 3:ip 一定可信

不可信。

当前服务端使用的是 socket 记录的真实对端地址。

误区 4:net_provider 标了“未使用”就等于“没有语义”

不是。

它有语义,只是当前 Lua 登录服没有消费。

误区 5:旧 Login 参考代码里的结构可以直接照搬

不能直接照搬。

旧代码主要拿来确认字段语义,不是拿来替代当前版本包布局的。


17. 如果你想自己验证这篇文档,可以从哪里下手

最适合新手的验证顺序如下:

验证 1:确认 CLConnect 的 Lua 包定义

看:

  • services/login/packet.lua

确认:

  • xy_id = 355
  • net_provider / account / version / ip

验证 2:确认收包分发流程

看:

  • services/login/server.lua

确认:

  • xy_id 用来选包类型
  • bis(message) 只吃包体

验证 3:确认 CLConnect 的业务分支

看:

  • services/login/logind.lua

确认:

  • 只保存了 packet.version
  • 没用 packet.net_provider
  • 没用 packet.account
  • 没用 packet.ip

验证 4:确认底层读写函数

看:

  • lualib/iostream.lua

确认:

  • readuint() 是数值读取
  • read(len) 是原样截取固定长度字节串

18. 当前可以给出的最稳结论

如果你只想记住最核心的结论,可以记下面这几条:

  1. packet.CLConnect 是登录流程里的连接阶段包,包号 355
  2. 当前 Lua 定义的包体布局是:net_provider + account + version + ip
  3. net_provider 的语义是“网络线路/运营商线路标识”,但当前 Lua 登录服未使用。
  4. 当前 Lua 登录服在 CLConnect 分支里真正用到的只有 version
  5. accountip 虽然会被解析,但当前不参与核心登录逻辑。
  6. 旧 Login C++ 参考代码可以帮助确认 net_provider 的语义,但不能原样照抄整包结构。
  7. CLConnect.bos() 目前没有 return stream:get(),只是因为当前服务端不主动发送 CLConnect,所以暂时没暴露问题。

19. 一句话总结

CLConnect 在当前项目里最准确的理解是:

一个登录前的连接阶段包。
当前 Lua 登录服主要拿它做“记录连接版本并进入下一步握手”,其中 net_provider 字段已经确认是线路标识,但还没有真正接入业务逻辑。

您需要登录后才可以回帖 登录 | register

本版积分规则

QQ|雪舞知识库 ( 浙ICP备15015590号-1 | 萌ICP备20232229号|浙公网安备33048102000118号 )|天天打卡

GMT+8, 2026-6-24 05:30 , Processed in 0.062905 second(s), 24 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表