Lua 中避免低效解析 TCP 网络数据包体的一种方式

TCP 是流式协议,发送方发送出的是字节流,接收方接收到的也是字节流数据。通常,在应用层都会通过 header + body 在字节流中标识出单个协议包。发送方将原始数据打包成 header + body 。header 是固定字节数包头,标识 body 包含了多少字节数据。接收方先读固定字节数 header ,然后根据 header 读出具体的 body 数据。
在游戏中,总会需要编写一些和服务器通信的机器人客户端。我们项目会习惯采用 Lua 来实现,就不可避免的解析 TCP 网络数据。逻辑很简单,通常采用字符串连接的方式几行代码就可以完成。完整代码点击这里 ,下面列出主要的代码片段。

1function mt:init(header_bytes) 2 self.cache = "" 3 self.header_bytes = header_bytes 4end 5 6function mt:input(str) 7 self.cache = self.cache .. str 8end 9 10function mt:output() 11 local hb = self.header_bytes 12 local total = #self.cache 13 if total <= hb then 14 return 15 end 16 17 local body_bytes = string.unpack(">I2", self.cache) 18 if hb + body_bytes > total then 19 return 20 end 21 22 local body = self.cache:sub(hb + 1, hb + body_bytes) 23 self.cache = self.cache:sub(hb + body_bytes + 1) 24 return body 25end

input 函数用于缓存收到的数据,output 函数用于将接收到的字节流解析成单个协议数据包。inputoutput 涉及的字符串操作在调用比较频繁时效率会很低。如果对工具的效率要求提高,便不再满足需求。但是又想这个机器人尽量简单,会先考虑用纯 Lua 来解决这个问题。

上述方案的问题在于字符串连接效率比较低,在接收数据比较频繁时,字符串操作占用大量的 CPU 资源。于是新方案的思想就是尽量避免字符串连接,如下所示。

1function mt:init(header_bytes) 2 self.cache_list = {} 3 self.total_size = 0 4 self.header_bytes = header_bytes 5 self.body_list = {} 6end 7 8function mt:input(str) 9 local cache = self.cache_list 10 local block = cache[#cache] 11 12 if block and #block < self.header_bytes then 13 cache[#cache] = block .. str 14 else 15 cache[#cache + 1] = str 16 end 17 18 self.total_size = self.total_size + #str 19end 20 21function mt:output() 22 local body_list = self.body_list 23 local cache_body = body_list[1] 24 if cache_body then 25 table.remove(body_list, 1) 26 return cache_body 27 end 28 29 local total_str 30 if #self.cache_list == 1 then 31 total_str = self.cache_list[1] 32 else 33 total_str = table.concat(self.cache_list) 34 self.cache_list = {total_str} 35 end 36 37 local hb = self.header_bytes 38 local start_index = 1 39 while true do 40 if not total_str or #total_str < hb then 41 break 42 end 43 44 if self.total_size <= hb then 45 break 46 end 47 48 local header = total_str:sub(start_index, start_index + hb - 1) 49 local body_bytes = string.unpack(">I2", header) 50 if hb + body_bytes > self.total_size then 51 break 52 end 53 54 self.total_size = self.total_size - hb - body_bytes 55 56 local new_index = start_index + hb + body_bytes 57 local body = total_str:sub(start_index + hb, new_index - 1) 58 if cache_body then 59 body_list[#body_list + 1] = body 60 else 61 cache_body = body 62 end 63 64 start_index = new_index 65 end 66 67 if start_index > 1 then 68 self.cache_list = {total_str:sub(start_index)} 69 end 70 71 return cache_body 72end

input 函数中不会进行字符串连接,而是把收到的数据保存到 self.cache_list 中。然后在 output 函数中一次尽最大可能解析协议数据,然后保存在 self.body_list 中,每次调用 output 时若 self.body_list 有数据,则直接返回这里的数据即可。

测试方式见这里。新的方式基本可以瞬间解析完 64M 数据。

最好是过一段时间调用一次 output 函数,这样会更高效。手游客户端的帧率一般是 30 FPS 或 60 FPS 。所以完全可以 1/60 秒调用一次 output 函数,甚至 1/100 秒调用一次也可以。

具体使用时,需要先获取完整的数据(位于 self.body_list )数组中,若没有,则读 socket ,然后添加到缓存中,再解析是否有收到了完整的数据,若没有则 sleep 一小会儿,则尝试。具体代码如下。

1function mt:read_packet() 2 local packet 3 while true do 4 -- 尝试获取完整的数据 5 packet = self.pack_obj:output(true) 6 if packet then 7 return packet 8 end 9 10 -- 读 socket 11 local buf, err = self.sock:read() 12 if not buf or #buf == 0 then 13 return nil, err 14 end 15 16 self.pack_obj:input(buf) 17 -- 解析是否收到了完整的数据 18 packet = self.pack_obj:output() 19 if packet then 20 break 21 end 22 Levent.sleep(0.01) 23 end 24end

一开始使用这段代码时,没有先尝试获取完整的数据,每次调用 read_packet 都会读 socket ,当一次收到的数据量很大时,可能包含了多个完整的数据包,而此时还 read_packet ,若服务器没有返回数据,则客户端会一直等待 read_packet 返回,就会卡住。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Lua 中避免低效解析 TCP 网络数据包体的一种方式 - HelloWorld