A way to avoid inefficient parsing of TCP network packets in Lua

TCP is a streaming protocol. The sender sends a byte stream and the receiver receives byte stream data. Usually, in the application layer, a single protocol package will be identified in the byte stream through header + body. The sender packages the original data into header + body. The header is a fixed byte packet header, which identifies how many bytes of data the body contains. The receiver reads the fixed byte header first, and then reads the specific body data according to the header.
In the game, you always need to write some robot clients that communicate with the server. Our project will be used to using Lua to implement, so it is inevitable to parse TCP network data. The logic is very simple, usually using string connection, which can be completed in a few lines of code. Complete code Click here , the main code snippets are listed below.

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

The input function is used to cache the received data, and the output function is used to parse the received byte stream into a single protocol packet. The string operations involved in input and output are inefficient when they are called frequently. If the efficiency requirements of tools are improved, they will no longer meet the needs. But I want this robot to be as simple as possible. I will first consider using pure Lua to solve this problem.

The problem of the above scheme is that the efficiency of string connection is relatively low. When receiving data more frequently, string operation occupies a lot of CPU resources. Therefore, the idea of the new scheme is to avoid string connection as much as possible, as shown below.

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

Instead of string connection, the input function saves the received data to self cache_ List. Then, in the output function, parse the protocol data as much as possible at one time, and then save it in self body_ List, if self body_ If the list has data, you can directly return the data here.

See for test method here . The new method can basically analyze 64M data in an instant.

It is better to call the output function once after a period of time, which will be more efficient. The frame rate of mobile game client is generally 30 FPS or 60 FPS. Therefore, you can call the output function once every 1 / 60 seconds, or even once every 1 / 100 seconds.

Posted by landysaccount on Sun, 22 May 2022 10:38:06 +0300