Skip to content
Bruce edited this page Sep 11, 2023 · 15 revisions

Socket

moon框架是多线程的,每个线程都有一个asio::io_context, 所以运行在worker线程中的所有服务都有网络通信的能力。框架为lua提供了基础的Socket API,为了方便游戏开发,封装了常用协议的(如websocket),同时也支持编写自定义协议。

TCP

moon中socket api 都是异步的(除非有特殊标识), 通用API有:

  • socket.listen
  • socket.accept
  • socket.connect
  • socket.sync_connect 同步连接
  • socket.write 可以发送字符串, 或者buffer指针
  • socket.write_message 直接发送message指针, 减少拷贝
  • socket.write_then_close 发送完毕后, 关闭连接
  • socket.settimeout
  • socket.setnodelay
  • socket.set_send_queue_limit 流量控制
  • socket.close
  • socket.getaddress

现在支持三种协议(每种协议都标注了特有的socket api):

  • PTYPE_SOCKET_TCP 提供了tcp流式读写相关API, 主要用于解析自定义协议, moon中的数据库client驱动和http-server,http-client都是使用它编写的。
    • socket.read
  • PTYPE_SOCKET_WS Websocket协议
    • socket.write_text socket.write_ping socket.write_pong
    • socket.wson 注册websocket网络消息回调
    • socket.start 配合socket.wson注册的网络事件回调, 自动循环accept
  • PTYPE_SOCKET_MOON tcp协议 2Byte(big-endian)+Data, 常用作网关协议, 更加高效, 数据帧:
    • socket.on 注册网络消息回调
    • socket.start 配合socket.on注册的网络事件回调, 自动循环accept
    • socket.set_enable_chunked
                 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
                +-+-----------------------------+
                |                               |
                |                               |
                |             Len(16bit)        |
                |                               |
                |                               |
                +-+-----------------------------+
                :                               |
                +             Data...           |
                |                               |
                +-------------------------------+

Len 不包括头部2字节, 默认支持最大 65534 大小的数据包, 对于moon可以使用socket.set_enable_chunked(fd, "w")设置标记允许对超过这个长度的数据包进行分包 r:表示读,w:表示写。如果客户端需要对接该协议可以参考如下代码

local MESSAGE_CONTINUED_FLAG = 65535

local function send_message(fd,data)
    if not fd then
        return false
    end
    local len = #data
    local onesize = 0
    local offset = 0
    repeat
        if len >= MESSAGE_CONTINUED_FLAG then --需要分包
            onesize = MESSAGE_CONTINUED_FLAG
        else
            onesize = len
        end
        socket.write(fd, string.pack(">H",onesize)..string.sub(data, offset+1, offset + onesize))
        offset = offset + onesize
        len = len - onesize
        --print("write", onesize, "left", len, "offset", offset)
    until len == 0

    if onesize == MESSAGE_CONTINUED_FLAG then --这里注意,如果数据大小刚好是65535的倍数,则需要发送一个header=0的数据包,表示分包结束
        socket.write(fd, string.pack(">H", 0))
        --print("write", 0)
    end
end

local function read_message( fd )
    if not fd then
        return false
    end

    local message = {}
    repeat
        local data,err = socket.read(fd, 2)
        if not data then
            print(fd,"fd read error",err)
            return false
        end
        local len = string.unpack(">H",data)
        local data2,err2 = socket.read(fd, len)
        if not data2 then
            print(fd,"fd read error",err2)
            return false
        end
        message[#message+1] = data2
        --print("recv", len)
    until len<MESSAGE_CONTINUED_FLAG --等于65535的包,都是拆分的包,需要全部读完后再合并

    return table.concat(message)
end

客户端端想和服务器通信,只需要遵循这个分包协议,可以编写C/C++, C#, lua,python等客户端。数据内容的格式可以自定义,如google protocol buffers或者json,或者自定义的协议。

使用Lua Socket API编写服务端

local listenfd = socket.listen(host,port,moon.PTYPE_SOCKET_MOON)
socket.start(listenfd)--auto accept

--注册网络事件
socket.on("accept",function(fd, msg)
    print("accept ", fd, moon.decode(msg, "Z"))
    socket.settimeout(fd, 10)
    --socket.setnodelay(fd)
    --socket.set_enable_chunked(fd, "w")
end)

socket.on("message",function(fd, msg)
    socket.write(fd, moon.decode(msg, "Z"))
end)

socket.on("close",function(fd, msg)
    print("close ", fd, moon.decode(msg, "Z"))
end)

socket.on("error",function(fd, msg)
    print("error ", fd, moon.decode(msg, "Z"))
end)

Socket API的使用与常规的socket编程非常类似,并且有一定程度的简化。

使用Lua Socket API编写客户端

下面的代码完全是异步,采用协程封装,编写起来像同步代码一样。

local function send(fd,data)
    if not fd then
        return false
    end
    local len = #data
    return socket.write(fd, string.pack(">H",len)..data)
end

local function session_read( fd )
    if not fd then
        return false
    end
    local data,err = socket.read(fd, 2)
    if not data then
        print(fd,"fd read error",err)
        return false
    end

    local len = string.unpack(">H",data)

    data,err = socket.read(fd, len)
    if not data then
        print(fd,"fd read error",err)
        return false
    end
    return data
end

moon.async(function()
    local fd,err = socket.connect(HOST,PORT,moon.PTYPE_SOCKET_TCP)
    if not fd then
        print("connect failed", err)
        return
    end
    local send_data = "Hello world"
    send(fd, send_data)
    local rdata = session_read(fd)
    socket.close(fd)
    assert(rdata == send_data)
end)

socket.connect第三个参数表示协议类型。

多线程与Lua Socket API

对于游戏业务来说,一般来说单个线程处理一个进程的网络收发是足够的,但有时需要多线程处理网络消息。Lua Socket API 提供了相应的功能,具体做法是:提前创建好服务,accept时从这个服务所在的worker申请asio::ip::tcp::socket对象,并和这个服务绑定,这样就可以把accept到的连接分散到不同线程中。

local listenfd  = socket.listen(host,port,moon.PTYPE_SOCKET_MOON)
local slave = {}
moon.async(function()

    for _=1,servicenum do
        local sid = moon.new_service({name="slave",file="network_benchmark.lua"})
        table.insert(slave,sid)
    end

    local balance = 1
    while true do
        if balance>#slave then
            balance = 1
        end
        socket.accept(listenfd,slave[balance])
        balance = balance + 1
    end
end)

UDP

  • socket.udp
  • socket.sendto
  • socket.udp_connect
  • socket.make_endpoint
  • socket.close