数据打包 · Packing
当连接建立成功,在 net.Conn 实例上能调用的方法有三个:
- 发送数据
WritePacket(p pk.Packet) error - 接收数据
ReadPacket(p *pk.Packet) error - 关闭连接
Close() error
在 Login 阶段还需要按协议调用
SetCipher和SetThreshold,这里不做讨论。
无论是发送数据还是接收数据,Minecraft 协议都是以数据包(pk.Packet)为单位处理的。
包长度、加密、压缩等传输细节1 Go-MC 都已经实现并封装到 pk.Packet 结构体内了,用户直接使用即可。
结构体 pk.Packet 定义如下:
type Packet struct {
ID int32
Data []byte
}
ID 是一个枚举值,表示数据包的功能,Data 的格式也需要根据它判断。可用的值在 data/packetid 中找到。
Data 逻辑上包含了一个或多个数据项,例如客户端在玩家移动时可能会发送的 Move 包可能格式如下:
| Field Name | Field Type | Notes |
|---|---|---|
| X | Double | Absolute position. |
| Feet Y | Double | Absolute feet position, normally Head Y - 1.62. |
| Z | Double | Absolute position. |
| On Ground | Boolean | True if the client is on the ground, false otherwise. |
即 Data 包含4个数据字段:三个 Double 和一个 Boolean。
所有的字段类型都定义在 net/packet 包,例如 pk.Double、pk.Boolean。
这些类型都实现了 pk.Field 接口,你可以调用它们的 ReadFrom 和 WriteTo 方法。
以下代码可以用来生成一个这样的数据包。
var buffer bytes.Buffer
_, _ = pk.Double(x).WriteTo(&buffer)
_, _ = pk.Double(y).WriteTo(&buffer)
_, _ = pk.Double(z).WriteTo(&buffer)
_, _ = pk.Boolean(onGround).WriteTo(&buffer)
p := pk.Packet {
ID: packetid.ServerboundMovePlayerPos,
Data: buffer.Bytes(),
}
操作起来有点繁琐,好在 Go-MC 为这样的操作提供了一个帮助函数:pk.Marshal()。经过改进的代码如下:
p := pk.Marshal(
packetid.ServerboundMovePlayerPos,
pk.Double(x),
pk.Double(y),
pk.Double(z),
pk.Boolean(onGround),
)
反过来,如果服务器想接收一个这样的数据包,那么需要用到 ReadFrom 方法,用笨办法就是如下这样:
var (
x pk.Double
y pk.Double
z pk.Double
onGround pk.Boolean
)
r := bytes.NewReader(p.Data)
_, _ = x.ReadFrom(r)
_, _ = y.ReadFrom(r)
_, _ = z.ReadFrom(r)
_, _ = onGround.ReadFrom(r)
加上错误处理更加繁琐了!好在 Go-MC 也提供了一个帮助函数:p.Scan()。经过改进的代码如下:
var (
x pk.Double
y pk.Double
z pk.Double
onGround pk.Boolean
)
_ = p.Scan(&x, &y, &z, &onGround)
在实际使用中,一般需要根据情况选择是否使用帮助函数,请自行判断。
使用 pk.Array
在一些数据包格式中,会存在 Array of X 类型的数组字段。
这些数组的前一个字段通常是一个 VarInt 类型的长度字段, Go-MC 提供了一个帮助类型来处理这种常见情况。
以 Commands 包为例,目前该数据包格式如下:
| Field Name | Field Type | Notes |
|---|---|---|
| Count | VarInt | Number of elements in the following array. |
| Nodes | Array of Node | An array of nodes. |
| Root index | VarInt | Index of the root node in the previous array. |
我们假设你已经定义好 Node 类型并为其实现了 pk.Field 接口,为了接收这个数据包中的 Nodes 数组,
我们可以采用以下繁琐的代码:
var (
count pk.VarInt
nodes []Node
root pk.VarInt
)
r := bytes.NewReader(p.Data)
_, _ = count.ReadFrom(r)
nodes = make([]Node, count)
for i := range nodes {
_, _ = nodes[i].ReadFrom(r)
}
root.ReadFrom(r)
为了使用 p.Scan() 简化工作量,我们需要用到 pk.Array() 函数:
var (
nodes []Node
root pk.VarInt
)
_ = p.Scan(
pk.Array(&nodes),
&root,
)
使用 pk.Array() ,会自动处理数组长度的 VarInt。返回值实现 pk.Field 接口,可用于 pk.Marshal() 与 pk.Scan()。
注意,当切片的元素类型不支持相应调用的 WriteTo 和 ReadFrom 方法时,会 panic。
请确保将 pk.Array() 的返回值用作 FieldEncoder 时,切片元素类型也实现 FieldEncoder,
将 pk.Array() 的返回值用作 FieldDecoder 时,切片元素类型也实现 FieldDecoder。
使用 pk.Option
一些数据包格式中,会存在 Optional X 类型的可选字段。顾名思义,这种字段在数据包中是可选的。
可选字段是否存在需要根据上下文进行判断,上下文通常指其前面的 Boolean 值,例如:
| Field Name | Field Type | Notes |
|---|---|---|
| Is Signed | Boolean | |
| Signature | Optional String | Exist only if Is Signed is true |
读取该数据包的伪代码如下:
IsSigned = ReadBoolean()
if IsSigned {
Signature = ReadString()
}
为了能在 p.Scan() 函数调用中提供这样的判断逻辑,Go-MC 提供了四个帮助类型:pk.Option 、 pk.OptionDecoder 、 pk.OptionEncoder 和 pk.Opt。
pk.Opt 是一个一般不会用到的原始类型,如需使用请自行阅读注释及源码,在此不做说明。
pk.Option 是一个泛型类型,有两个泛型参数:T 和 P,其中后者是前者的指针类型。
T 必须满足 pk.FieldEncoder 接口,P 必须满足 FieldDecoder 接口。
例如:pk.Option[pk.String, *pk.String],pk.Option[pk.VarInt, *pk.VarInt]等,都是合法的类型。
需要手动指定 P 是因为当前 Go 编译器泛型实现不够完善2,在 Go 支持推导结构体泛型参数类型之后,
上面的两个例子就可以简单写成 pk.Option[pk.String] 和 pk.Option[pk.VarInt] 了。
pk.OptionDecoder 和 pk.OptionEncoder 是两个变体,分别去掉了 对T的约束 和 对P的约束,很好理解。
以下给出读写上面例子数据包的完整代码
var Signature pk.Option[pk.String, *pk.String]
_ = p.Scan(&Signature)