上一篇避免通过拼接字符串作为接收数据的缓冲区,解决办法是通过一个 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 层面解决问题。目前看来,应该是满足需求了。