netpack 提供的是网络基础数据包的打包和解包功能,是网络输出传输绕不开的一个点。本篇会尝试分析一下 netpack 的实现。
概述
netpack 中一共提供了 5 个 Lua 接口。pack 用来打包数据,tostring 用来将一个二进制数据转化为字符串,filter 用来将数据按照类型整理,clear 用来清空待处理数据的队列,pop 用来从待处理队列中弹出一个网络包。
pack
pack 可以把一个二进制数据或者是一个字符串打包成一个二进制数据包。
包头占两个字节,是数据的长度,因为只有两个字节,所以单个数据包的长度需要小于 65536 个字节。值得注意的是,数据的字节序固定采用了大端方式,第一个字节是长度的高 8 位,第二个字节是长度的低 8 位。之所以这样写而不是直接保存长度,主要是担心夸字节序主机之间的交流。包头后面紧跟了数据内容。
tostring
tostring 负责将一个二进制数据转化为字符串格式。接受一个 userdata 和一个长度参数,返回转化后的字符串。实现上全都是通过 Lua 的 api 实现的,比较简单。
有一点需要注意的是,netpack.tostring 会释放掉本来的 userdata 的内存。
filter
lfilter 在处理大部分类型的命令时都只是简单的传递了一下参数而已,此处不再赘述。需要重点关注的点是,当处理 SKYNET_SOCKET_TYPE_DATA 类型的网络数据的时候,会执行的对数据进行分包的操作。
结构
每个服务有一个网络数据队列,它包含了两部分,一部分是每个套接字的未完成部分 hash,它是一个 uncomplete 结构体的哈希桶,存放着每一个连接的一份未完成数据。另一个部分是已经完成分包的数据包队列,它是一个动态长度的环形数组,可以动态扩容,每次扩容 QUEUESIZE 个数据长度,存放着每一个连接的全部待处理网络包,等待消息分发函数调用 pop 来拿取网络包。
|
|
分包
分包的最终处理函数是 filter_data_,这里就会用上上面提到的结构体,根据不同的情况,分别填充不同的数据,最后返回给 Lua 层的调用。
函数会首先尝试通过 find_uncomplete 拿到 socket id 对应的 uncomplate 结构体。这里因为保存的数据结构是个哈希桶,所以找到对应哈希节点以后要遍历链表进行查找。
如果存在对应的 uncomplate 结构体,则说明上次还有未完全读取的数据包。首先判断已读取长度 read 的大小,如果 read 小于 0,说明上次只读取到了本数据包的一个字节数据,这时候会读第二个字节的数据来得到完成的数据包长度。通过 uncompalte 结构体中的数据包总长度 pack.size 和已读取长度 read 可以计算出还需要的数据长度 need 来。
通过比较还需要的长度和本次处理的数据长度,可以知道本次处理的情况。如果本次的长度还是不够拼出全部的数据,则把本次的数据加到 uncomplate 中即可。如果本次的数据正好可以完成当前的数据包,则直接把数据加到 uncomplate 中并且返回它即可。如果本次的数据会超出当前需要的数据,则首先把本数据包的内容加入到 queue.queue 中去,然后递归调用 push_more 处理剩下的内容,直到数据长度不再够组成一个完整数据包。
一开始不存在 uncomplate 结构体的处理方法基本一样,只是要额外处理一下本次读取的数据长度为 1 的情况,这种情况不仅要保存 uncompalte 结构体,还要把 read 置为 -1 来作为这种特殊情况的标识。
pop
用来从已完成组装的队列中拿取最早的那一个数据包。上面说了分包的时候如果数据一次读取的比较多,超出了一条消息的范围,则会依次处理并且把每条消息都 push 到 queue 的 queue 数组中去。要处理消息的时候,就调用 netpack.pop 从队列中拿取一条消息出来。
clear
用来清空整个队列的方法,在 gateserver 中被放在了 CMD 的 __gc 元方法中。会遍历清空 queue 中的 hash 和 queue 这两个结构,释放每一个数据的内存。