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
注册的网络事件回调, 自动循环acceptsocket.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