上一篇避免通過拼接字符串作爲接收數據的緩衝區,解決辦法是通過一個 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 層面解決問題。目前看來,應該是滿足需求了。