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

给你一囗甜甜゛ 提交于 2020-08-16 19:35:16

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

function mt:init(header_bytes)
    self.cache = ""
    self.header_bytes = header_bytes
end

function mt:input(str)
    self.cache = self.cache .. str
end

function mt:output()
    local hb = self.header_bytes
    local total = #self.cache
    if total <= hb then
        return
    end

    local body_bytes = string.unpack(">I2", self.cache)
    if hb + body_bytes > total then
        return
    end

    local body = self.cache:sub(hb + 1, hb + body_bytes)
    self.cache = self.cache:sub(hb + body_bytes + 1)
    return body
end

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

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

function mt:init(header_bytes)
    self.cache_list = {}
    self.total_size = 0
    self.header_bytes = header_bytes
    self.body_list = {}
end

function mt:input(str)
    local cache = self.cache_list
    local block = cache[#cache]

    if block and #block < self.header_bytes then
        cache[#cache] = block .. str
    else
        cache[#cache + 1] = str
    end

    self.total_size = self.total_size + #str
end

function mt:output()
    local body_list = self.body_list
    local cache_body = body_list[1]
    if cache_body then
        table.remove(body_list, 1)
        return cache_body
    end

    local total_str
    if #self.cache_list == 1 then
        total_str = self.cache_list[1]
    else
        total_str = table.concat(self.cache_list)
        self.cache_list = {total_str}
    end

    local hb = self.header_bytes
    local start_index = 1
    while true do
        if not total_str or #total_str < hb then
            break
        end

        if self.total_size <= hb then
            break
        end

        local header = total_str:sub(start_index, start_index + hb - 1)
        local body_bytes = string.unpack(">I2", header)
        if hb + body_bytes > self.total_size then
            break
        end

        self.total_size = self.total_size - hb - body_bytes

        local new_index = start_index + hb + body_bytes
        local body = total_str:sub(start_index + hb, new_index - 1)
        if cache_body then
            body_list[#body_list + 1] = body
        else
            cache_body = body
        end

        start_index = new_index
    end

    if start_index > 1 then
        self.cache_list = {total_str:sub(start_index)}
    end

    return cache_body
end

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

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

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!