上一篇避免通过拼接字符串作为接收数据的缓冲区,解决办法是通过一个 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 ,接收到完整的数据后就返回。完整的代码在这里。
先列出通过字符串拼接的方式实现的接收数据缓冲区。每次收到数据后则拼接,产生新的字符串。然后再根据字节数返回相应的子串。
1local setmetatable = setmetatable 2 3local mt = {} 4mt.__index = mt 5 6function mt:init() 7 self.cache = "" 8end 9 10function mt:input(str) 11 self.cache = self.cache .. str 12end 13 14function mt:output(num_bytes) 15 local cache = self.cache 16 if #cache < num_bytes then 17 return 18 end 19 20 local data = cache:sub(1, num_bytes) 21 self.cache = cache:sub(num_bytes + 1) 22 23 return data 24end 25 26local function _new(...) 27 local obj = setmetatable({}, mt) 28 obj:init(...) 29 return obj 30end 31 32return _new
下面是不拼接字符串实现的接收数据缓冲区。由于高频的拼接字符串是很耗时的操作,这里的核心想法就是避免这种情况。每次接收到数据后将数据缓存在 Lua 数组中,然后根据字节数拼接产生字符串。
1local setmetatable = setmetatable 2local table = table 3 4local mt = {} 5mt.__index = mt 6 7function mt:init() 8 self.str_blocks = {} 9 self.total_bytes = 0 10end 11 12function mt:input(str) 13 table.insert(self.str_blocks, str) 14 self.total_bytes = self.total_bytes + #str 15end 16 17function mt:output(num_bytes) 18 if self.total_bytes < num_bytes then 19 return 20 end 21 22 local blocks = self.str_blocks 23 local num = #blocks 24 25 local index 26 local stat_bytes = 0 27 for i, block in ipairs(blocks) do 28 index = i 29 stat_bytes = stat_bytes + #block 30 if stat_bytes >= num_bytes then 31 break 32 end 33 end 34 35 local str = table.concat(blocks, "", 1, index) 36 local data = str:sub(1, num_bytes) 37 local left_num = num - index 38 39 local new_blocks = {} 40 if stat_bytes > num_bytes then 41 new_blocks[#new_blocks + 1] = str:sub(num_bytes + 1) 42 end 43 if left_num > 0 then 44 table.move(blocks, index + 1, num, #new_blocks + 1, new_blocks) 45 end 46 47 self.str_blocks = new_blocks 48 self.total_bytes = self.total_bytes - num_bytes 49 return data 50end 51 52local function _new(...) 53 local obj = setmetatable({}, mt) 54 obj:init(...) 55 return obj 56end 57 58return _new
下面是测试的代码。在我的机器上,优化前需要花几十秒的时间,优化后不到 200 毫秒运行完毕。
1local ipairs = ipairs 2local assert = assert 3local os = os 4local string = string 5 6local p1_func = require "string1" 7local p2_func = require "string2" 8 9local p1 = p1_func(2) 10local p2 = p2_func(2) 11 12local function test(obj) 13 local raw = {} 14 local list = {} 15 local total = 0 16 local max = 64 * 1024 17 for i = 1, max, 32 do 18 total = total + i 19 local s = string.rep("A", i) 20 raw[#raw + 1] = s 21 list[#list + 1] = string.pack(">s2", s) 22 end 23 24 for _, str in ipairs(list) do 25 obj:input(str) 26 end 27 28 local start = os.clock() 29 local ret = {} 30 for _, str in ipairs(raw) do 31 local data = obj:output(2) 32 local n = string.unpack(">I2", data) 33 assert(n == #str) 34 ret[#ret + 1] = obj:output(n) 35 end 36 print(os.clock() - start) 37 38 assert(#raw == #ret, #raw .. " vs " .. #ret) 39 for i = 1, #raw do 40 assert(raw[i] == ret[i]) 41 end 42end 43 44local new = ... 45test(new and p2 or p1)
由于项目中用到的工具对性能有些要求,但又没有那么高的要求,所以就还是想在 Lua 层面解决问题。目前看来,应该是满足需求了。