Lua 中避免低效解析 TCP 網絡數據包體的一種方式(續)

上一篇避免通過拼接字符串作爲接收數據的緩衝區,解決辦法是通過一個 Lua 模塊來獲取接收後的完整數據,若沒有完整數據則讀取 socket ,若還沒有完整數據則 sleep 一小會兒,然後再嘗試。 瞭解過 Lua 或用過 skynet 可知,使用 coroutine 可以實現 sock:read(1000) 這種同步的寫法但實際是異步的方式讀取 1000 字節數據,當網絡連接正常時,函數 read 只有在接收了指定字節數後的數據後纔會返回。這裏不討論如何在等待網絡數據到達時使當前的 coroutine 讓出執行,而在 socket 讀取到數據後再將此 coroutine 喚醒。這裏討論如何在 Lua 中實現一個相對高效的數據緩衝區,通過 local bytes = sock:read(2); local data = sock:read(bytes) 這種方式解包,獲取接收到的完整數據。這種方式和前面一種方式的不同點在於不需要調用 sleep ,接收到完整的數據後就返回。完整的代碼在這裏

先列出通過字符串拼接的方式實現的接收數據緩衝區。每次收到數據後則拼接,產生新的字符串。然後再根據字節數返回相應的子串。

local setmetatable = setmetatable

local mt = {}
mt.__index = mt

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

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

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

    local data = cache:sub(1, num_bytes)
    self.cache = cache:sub(num_bytes + 1)

    return data
end

local function _new(...)
    local obj = setmetatable({}, mt)
    obj:init(...)
    return obj
end

return _new

下面是不拼接字符串實現的接收數據緩衝區。由於高頻的拼接字符串是很耗時的操作,這裏的核心想法就是避免這種情況。每次接收到數據後將數據緩存在 Lua 數組中,然後根據字節數拼接產生字符串。

local setmetatable = setmetatable
local table = table

local mt = {}
mt.__index = mt

function mt:init()
    self.str_blocks = {}
    self.total_bytes = 0
end

function mt:input(str)
    table.insert(self.str_blocks, str)
    self.total_bytes = self.total_bytes + #str
end

function mt:output(num_bytes)
    if self.total_bytes < num_bytes then
        return
    end

    local blocks = self.str_blocks
    local num = #blocks

    local index
    local stat_bytes = 0
    for i, block in ipairs(blocks) do
        index = i
        stat_bytes = stat_bytes + #block
        if stat_bytes >= num_bytes then
            break
        end
    end

    local str = table.concat(blocks, "", 1, index)
    local data = str:sub(1, num_bytes)
    local left_num = num - index

    local new_blocks = {}
    if stat_bytes > num_bytes then
        new_blocks[#new_blocks + 1] = str:sub(num_bytes + 1)
    end
    if left_num > 0 then
        table.move(blocks, index + 1, num, #new_blocks + 1, new_blocks)
    end

    self.str_blocks = new_blocks
    self.total_bytes = self.total_bytes - num_bytes
    return data
end

local function _new(...)
    local obj = setmetatable({}, mt)
    obj:init(...)
    return obj
end

return _new

下面是測試的代碼。在我的機器上,優化前需要花幾十秒的時間,優化後不到 200 毫秒運行完畢。

local ipairs = ipairs
local assert = assert
local os = os
local string = string

local p1_func = require "string1"
local p2_func = require "string2"

local p1 = p1_func(2)
local p2 = p2_func(2)

local function test(obj)
    local raw = {}
    local list = {}
    local total = 0
    local max = 64 * 1024
    for i = 1, max, 32 do
        total = total + i
        local s = string.rep("A", i)
        raw[#raw + 1] = s
        list[#list + 1] = string.pack(">s2", s)
    end

    for _, str in ipairs(list) do
        obj:input(str)
    end

    local start = os.clock()
    local ret = {}
    for _, str in ipairs(raw) do
        local data = obj:output(2)
        local n = string.unpack(">I2", data)
        assert(n == #str)
        ret[#ret + 1] = obj:output(n)
    end
    print(os.clock() - start)

    assert(#raw == #ret, #raw .. " vs " .. #ret)
    for i = 1, #raw do
        assert(raw[i] == ret[i])
    end
end

local new = ...
test(new and p2 or p1)

由於項目中用到的工具對性能有些要求,但又沒有那麼高的要求,所以就還是想在 Lua 層面解決問題。目前看來,應該是滿足需求了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章