snax.msgserver 是一个基于消息请求和回应模式的网关服务器模板。它基于 snax.gateserver 定制,可以接收客户端发起的请求数据包,并给出对应的回应。
和 service/gate.lua 不同,用户在使用它的时候,一个用户的业务处理不基于连接。即,它不把连接建立作为用户登陆、不在连接断开时让用户登出。用户必须显式的登出系统,或是业务逻辑设计的超时机制导致登出。
15.1 msgserver
和 GateServer 和 LoginServer 一样,snax.msgserver 只是一个模板,你还需要自定义一些业务相关的代码,才是一个完整的服务。与客户端的通信协议使用的是两字节数据长度协议。
15.1.1 msgserver api
--uid, subid, server 把一个登陆名转换为 uid, subid, servername 三元组
msgserver.userid(username)
---username 把 uid, subid, servername 三元组构造成一个登陆名
msgserver.username(uid, subid, server)
--你需要在 login_handler 中调用它,注册一个登陆名username对应的 serect
msgserver.login(username, secret)
--让一个登陆名失效(登出),通常在 logout_handler 里调用。
msgserver.logout(username)
--查询一个登陆名对应的连接的 ip 地址,如果没有关联的连接,会返回 nil 。
msgserver.ip(username)
15.1.2 msgserver服务模板
local msgserver = require "snax.msgserver"
local server = {}
msgserver.start(server) --服务初始化函数,要把server表传递进去。
--在打开端口时,会触发这个 register_handler函数参数name是在配置信息中配置的当前登陆点的名字
--你在这个回调要做的事件是通知登录服务器,我这个登录点准备好了
function server.register_handler(name)
end
--当一个用户登陆后,登陆服务器会转交给你这个用户的 uid 和 serect ,最终会触发 login_handler 方法。
--在这个函数里,你需要做的是判定这个用户是否真的可以登陆。然后为用户生成一个 subid ,使用 msgserver.username(uid, subid, servername) 可以得到这个用户这次的登陆名。这里 servername 是当前登陆点的名字。
--在这个过程中,如果你发现一些意外情况,不希望用户进入,只需要用 error 抛出异常。
function server.login_handler(uid, secret)
end
--当一个用户想登出时,这个函数会被调用,你可以在里面做一些状态清除的工作。
function server.logout_handler(uid, subid)
end
--当外界(通常是登陆服务器)希望让一个用户登出时,会触发这个事件。
--发起一个 logout 消息(最终会触发 logout_handler)
function server.kick_handler(uid, subid)
end
--当用户的通讯连接断开后,会触发这个事件。你可以不关心这个事件,也可以利用这个事件做超时管理。
--(比如断开连接后一定时间不重新连回来就主动登出。)
function server.disconnect_handler(username)
end
--如果用户提起了一个请求,就会被这个 request_handler会被调用。这里隐藏了 session 信息,
--等请求处理完后,只需要返回一个字符串,这个字符串会回到框架,加上 session 回应客户端。
--这个函数中允许抛出异常,框架会正确的捕获这个异常,并通过协议通知客户端。
function server.request_handler(username, msg, sz)
end
15.1.3 最简单msgserver
编写一个最最简单的simplemsgserver.lua:
local msgserver = require "snax.msgserver"
local crypt = require "skynet.crypt"
local skynet = require "skynet"
local subid = 0
local server = {} --一张表,里面需要实现前面提到的所有回调接口
local servername
--外部发消息来调用,一般用来注册可以登陆的登录名
function server.login_handler(uid, secret)
skynet.error("login_handler invoke", uid, secret)
subid = subid + 1
--通过uid以及subid获得username
local username = msgserver.username(uid, subid, servername)
skynet.error("uid",uid, "login,username", username)
msgserver.login(username, secret)--正在登录,给登录名注册一个secret
return subid
end
--外部发消息来调用,注销掉登陆名
function server.logout_handler(uid, subid)
skynet.error("logout_handler invoke", uid, subid)
local username = msgserver.username(uid, subid, servername)
msgserver.logout(username)
end
--外部发消息来调用,用来关闭连接
function server.kick_handler(uid, subid)
skynet.error("kick_handler invoke", uid, subid)
end
--当客户端断开了连接,这个回调函数会被调用
function server.disconnect_handler(username)
skynet.error(username, "disconnect")
end
--当接收到客户端的请求,这个回调函数会被调用,你需要提供应答。
function server.request_handler(username, msg)
skynet.error("recv", msg, "from", username)
return string.upper(msg)
end
--监听成功会调用该函数,name为当前服务别名
function server.register_handler(name)
skynet.error("register_handler invoked name", name)
servername = name
end
msgserver.start(server) --需要配置信息
15.1.4 发送lua消息启动msgserver
要启动msgserver,需要给msgserver发一个lua消息open(msgserver框架已经能处理open消息)例如
我们编写一个msgserver的启动msgserver服务,代码startmsgserver.lua:
local skynet = require "skynet"
skynet.start(function()
local gate = skynet.newservice("simplemsgserver")
--网关服务需要发送lua open来打开,open也是保留的命令
skynet.call(gate, "lua", "open" , {
port = 8002,
maxclient = 64,
servername = "sample", --取名叫sample,跟使用skynet.name(".sample")一样
})
end)
运行结果:
startmsgserver [:0100000a] LAUNCH snlua startmsgserver [:0100000b] LAUNCH snlua simplemsgserver [:0100000b] Listen on 0.0.0.0:8002 #开启监听端口 [:0100000b] register_handler invoked name sample #register_handler触发
15.1.5 发送lua消息给msgserver
sendtomsgserver.lua
local skynet = require "skynet"
skynet.start(function()
local gate = skynet.newservice("simplemsgserver")
--网关服务需要发送lua open来打开,open也是保留的命令
skynet.call(gate, "lua", "open" , {
port = 8002,
maxclient = 64,
servername = "sample", --取名叫sample,跟使用skynet.name(".sample")一样
})
local uid = "nzhsoft"
local secret = "11111111"
local subid = skynet.call(gate, "lua", "login", uid, secret) --告诉msgserver,nzhsoft这个用户可以登陆
skynet.error("lua login subid", subid)
skynet.call(gate, "lua", "logout", uid, subid) --告诉msgserver,nzhsoft登出
skynet.call(gate, "lua", "kick", uid, subid) --告诉msgserver,剔除nzhsoft连接
skynet.call(gate, "lua", "close") --关闭gate,也就是关掉监听套接字
end)
运行结果:
sendtomsgserver [:0100000a] LAUNCH snlua sendtomsgserver [:0100000b] LAUNCH snlua simplemsgserver [:0100000b] Listen on 0.0.0.0:8002 [:0100000b] register_handler invoked name sample [:0100000b] login_handler invoke nzhsoft 11111111 #login_handler调用 [:0100000b] uid nzhsoft login,username bnpoc29mdA==@c2FtcGxl#MQ== #login_handler调用成功,并生成一个登陆名 [:0100000a] lua login subid 1 #返回一个唯一的subid [:0100000b] logout_handler invoke nzhsoft 1 #登出调用 [:0100000b] kick_handler invoke nzhsoft 1 #kick_handler调用
15.2 loginserver与msgserver
要使用msgserver一般都要跟loginserver一起使用,下面我们让他们一起工作。
客户端登录的时候,一般先登录loginserver,然后再去连接实际登录点,msgserver一般充当真实登录点的角色,原理图如下:
15.2.1 编写一个mymsgserver.lua
local msgserver = require "snax.msgserver"
local crypt = require "skynet.crypt"
local skynet = require "skynet"
local loginservice = tonumber(...) --从启动参数获取登录服务的地址
local server = {} --一张表,里面需要实现前面提到的所有回调接口
local servername
local subid = 0
--外部发消息来调用,一般是loginserver发消息来,你需要产生唯一的subid,如果loginserver不允许multilogin,那么这个函数也不会重入。
function server.login_handler(uid, secret)
subid = subid + 1
--通过uid以及subid获得username
local username = msgserver.username(uid, subid, servername)
skynet.error("uid",uid, "login,username", username)
msgserver.login(username, secret)--正在登录,给登录名注册一个secret
return subid
end
--外部发消息来调用,登出uid对应的登录名
function server.logout_handler(uid, subid)
local username = msgserver.username(uid, subid, servername)
msgserver.logout(username) --登出
end
--一般给loginserver发消息来调用,可以作为登出操作
function server.kick_handler(uid, subid)
server.logout_handler(uid, subid)
end
--当客户端断开了连接,这个回调函数会被调用
function server.disconnect_handler(username)
skynet.error(username, "disconnect")
end
--当接收到客户端的网络请求,这个回调函数会被调用,需要给与应答
function server.request_handler(username, msg)
skynet.error("recv", msg, "from", username)
return string.upper(msg)
end
--注册一下登录点服务,主要是告诉loginservice这个有这个登录点的存在
function server.register_handler(name)
servername = name
skynet.call(loginservice, "lua", "register_gate", servername, skynet.self())
end
msgserver.start(server) --需要配置信息表server
15.2.2 编写一个mylogin.lua
修改14.2中mylogin.lua
local login = require "snax.loginserver"
local crypt = require "skynet.crypt"
local skynet = require "skynet"
local server_list = {}
local server = {
host = "127.0.0.1",
port = 8001,
multilogin = false, -- disallow multilogin
name = "login_master",
}
function server.auth_handler(token)
-- the token is base64(user)@base64(server):base64(password)
local user, server, password = token:match("([^@]+)@([^:]+):(.+)")
user = crypt.base64decode(user)
server = crypt.base64decode(server)
password = crypt.base64decode(password)
skynet.error(string.format("%s@%s:%s", user, server, password))
assert(password == "password", "Invalid password")
return server, user
end
function server.login_handler(server, uid, secret)
local msgserver = assert(server_list[server], "unknow server")
skynet.error(string.format("%s@%s is login, secret is %s", uid, server, crypt.hexencode(secret)))
--将uid以及secret发送给登陆点,告诉登陆点,这个uid可以登陆,并且让登陆点返回一个subid
local subid = skynet.call(msgserver, "lua", "login", uid, secret)
return subid --返回给客户端subid,用跟登录点握手使用
end
local CMD = {}
function CMD.register_gate(server, address)
skynet.error("cmd register_gate")
server_list[server] = address --记录已经启动的登录点
end
function server.command_handler(command, ...)
local f = assert(CMD[command])
return f(...)
end
login(server) --服务启动需要参数
15.2.3 编写一个testmsgserver.lua来启动他们
local skynet = require "skynet"
skynet.start(function()
--启动mylogin监听8001
local loginserver = skynet.newservice("mylogin")
--启动mymsgserver传递loginserver地址
local gate = skynet.newservice("mymsgserver", loginserver)
--网关服务需要发送lua open来打开,open也是保留的命令
skynet.call(gate, "lua", "open" , {
port = 8002,
maxclient = 64,
servername = "sample", --取名叫sample,跟使用skynet.name(".sample")一样
})
end)
运行testmsgserver:
$ ./skynet examples/conf testmsgserver [:01000010] LAUNCH snlua testmsgserver [:01000012] LAUNCH snlua mylogin [:01000019] LAUNCH snlua mylogin [:0100001a] LAUNCH snlua mylogin [:0100001b] LAUNCH snlua mylogin [:0100001c] LAUNCH snlua mylogin [:0100001d] LAUNCH snlua mylogin [:0100001e] LAUNCH snlua mylogin [:0100001f] LAUNCH snlua mylogin [:01000020] LAUNCH snlua mylogin [:01000012] login server listen at : 127.0.0.1 8001 [:01000022] LAUNCH snlua mymsgserver 16777234 #启动msgserver [:01000022] Listen on 0.0.0.0:8002 #监听8002端口 register_handler #向loginserver发送登录点注册信息 [:01000012] cmd register_gate #loginserver记录下来登录点的信息
15.2.4 编写一个myclient.lua
在14.4中的myclient.lua,只连接了loginserver,并没有连接具体的登录点,现在来改写一下myclient.lua
示例代码:myclient.lua
package.cpath = "luaclib/?.so"
local socket = require "client.socket"
local crypt = require "client.crypt"
if _VERSION ~= "Lua 5.3" then
error "Use lua 5.3"
end
local fd = assert(socket.connect("127.0.0.1", 8001))
local function writeline(fd, text)
socket.send(fd, text .. "\n")
end
local function unpack_line(text)
local from = text:find("\n", 1, true)
if from then
return text:sub(1, from-1), text:sub(from+1)
end
return nil, text
end
local last = ""
local function unpack_f(f)
local function try_recv(fd, last)
local result
result, last = f(last)
if result then
return result, last
end
local r = socket.recv(fd)
if not r then
return nil, last
end
if r == "" then
error "Server closed"
end
return f(last .. r)
end
return function()
while true do
local result
result, last = try_recv(fd, last)
if result then
return result
end
socket.usleep(100)
end
end
end
local readline = unpack_f(unpack_line)
local challenge = crypt.base64decode(readline()) --接收challenge
local clientkey = crypt.randomkey()
--把clientkey换算后比如称它为ckeys,发给服务器
writeline(fd, crypt.base64encode(crypt.dhexchange(clientkey)))
local secret = crypt.dhsecret(crypt.base64decode(readline()), clientkey)
print("sceret is ", crypt.hexencode(secret)) --secret一般是8字节数据流,需要转换成16字节的hex字符串来显示。
local hmac = crypt.hmac64(challenge, secret) --加密的时候需要直接传递secret字节流
writeline(fd, crypt.base64encode(hmac))
local token = {
server = "sample",
user = "nzhsoft",
pass = "password",
}
local function encode_token(token)
return string.format("%s@%s:%s",
crypt.base64encode(token.user),
crypt.base64encode(token.server),
crypt.base64encode(token.pass))
end
local etoken = crypt.desencode(secret, encode_token(token)) --使用DES加密token得到etoken, etoken是字节流
writeline(fd, crypt.base64encode(etoken)) --发送etoken,mylogin.lua将会调用auth_handler回调函数, 以及login_handler回调函数。
local result = readline() --读取最终的返回结果。
print(result)
local code = tonumber(string.sub(result, 1, 3))
assert(code == 200)
socket.close(fd) --可以关闭链接了
local subid = crypt.base64decode(string.sub(result, 5)) --解析出subid
print("login ok, subid=", subid)
----- connect to gate server 新增内容,以下通信协议全是两字节数据长度协议。
local function send_request(v, session) --打包数据v以及session
local size = #v + 4
-->I2大端序2字节unsigned int,>I4大端序4字节unsigned int
local package = string.pack(">I2", size)..v..string.pack(">I4", session)
socket.send(fd, package)
return v, session
end
local function recv_response(v)--解包数据v得到content(内容)、ok(是否成功)、session(会话序号)
local size = #v - 5
--cn:n字节字符串 ; B>I4: B unsigned char,>I4,大端序4字节unsigned int
local content, ok, session = string.unpack("c"..tostring(size).."B>I4", v)
return ok ~=0 , content, session
end
local function unpack_package(text)--读取两字节数据长度的包
local size = #text
if size < 2 then
return nil, text
end
local s = text:byte(1) * 256 + text:byte(2)
if size < s+2 then
return nil, text
end
return text:sub(3,2+s), text:sub(3+s)
end
local readpackage = unpack_f(unpack_package)
local function send_package(fd, pack)
local package = string.pack(">s2", pack) -->大端序,s计算字符串长度,2字节整形表示
socket.send(fd, package)
end
local text = "echo"
local index = 1
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002 )) --连接登录点对应的ip端口
last = ""
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index) --index用于断链恢复
local hmac = crypt.hmac64(crypt.hashkey(handshake), secret) --加密握手hash值得到hmac,保证handshake数据接收无误,没被篡改。
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac)) --发送handshake
print(readpackage()) --接收应答
print("===>",send_request(text,0)) --发送请求,并同时将当前的session 0组合发送,session用于匹配应答
print("<===",recv_response(readpackage()))
print("disconnect")
socket.close(fd)
握手包
当一个连接接入后,第一个包是握手包。握手首先由客户端发起:
--固定握手信息组合
base64(uid)@base64(server)#base64(subid):index:base64(hmac)
| | | |
--用户名 登录点 断线重登次数 handshake的杂凑值
index 至少是 1 ,每次连接都需要比之前的大。这样可以保证握手包不会被人恶意截获复用。
15.2.5 运行服务与客户端
在服务端运行testmsgserver,再起一个终端运行myclient:
$ ./skynet examples/conf testmsgserver [:01000010] LAUNCH snlua testmsgserver [:01000012] LAUNCH snlua mylogin [:01000019] LAUNCH snlua mylogin [:0100001a] LAUNCH snlua mylogin [:0100001b] LAUNCH snlua mylogin [:0100001c] LAUNCH snlua mylogin [:0100001d] LAUNCH snlua mylogin [:0100001e] LAUNCH snlua mylogin [:0100001f] LAUNCH snlua mylogin [:01000020] LAUNCH snlua mylogin [:01000012] login server listen at : 127.0.0.1 8001 [:01000022] LAUNCH snlua mymsgserver 16777234 [:01000022] Listen on 0.0.0.0:8002 register_handler [:01000012] cmd register_gate sample #loginserver register_gate调用 [:01000019] connect from 127.0.0.1:48822 (fd = 10) #loginserver有新连接产生 [:01000019] nzhsoft@sample:password #loginserver author_handler调用 [:01000012] nzhsoft@sample is login, secret is 2828d352698fff21 # loginserver login_handler调用 [:01000022] uid nzhsoft login,username bnpoc29mdA==@c2FtcGxl#MQ== #msgserver login_handler调用 [:01000022] recv echo from bnpoc29mdA==@c2FtcGxl#MQ== #msgserver接收到消息并且应答 [:01000022] bnpoc29mdA==@c2FtcGxl#MQ== disconnect #断开连接,调用msgserver的logout_handler函数。
客户端运行结果:
$ 3rd/lua/lua my_workspace/myclient.lua sceret is 2828d352698fff21 200 MQ== login ok, subid= 1 connect #连接登录点 200 OK #登录成功 ===> echo 0 #发送请求 <=== true ECHO 0 #收到应答 disconnect #断开连接 $
15.3 服务握手应答包
msgserver服务给与客户端应答如下:
200 OK --成功
404 User Not Found --用户未找到
403 Index Expired --index已经过期了
401 Unauthorized --账号密码校验失败
400 Bad Request --密钥交换失败
404与403是这登录msgserver这个登录点的时候可能出现的状态码码,剩下的状态码在之前loginserver的时候都已经讲过,这边不再复述。
以上这些包全部是两字节数据长度协议包。例如:
\x00\x06 \x32\x30\x30\x20\x4f\x4b
| |
len 200 ok
15.3.1 404用户未找到
修改myclient.lua中的handshake数据如下:
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index)
--改为
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode("username"), crypt.base64encode(token.server),crypt.base64encode(subid) , index) --token.use改成了username
这样,登录loginserver时给出的账号是nzhsoft
, msgserver中也只在login_handler中记录的nzhsoft
这个用户,如果客户端使用非nzhsoft
的握手信息登录,就会报一个404
运行myclient.lua
$ 3rd/lua/lua my_workspace/myclient.lua sceret is 4f9b6da27dc2d414 200 MQ== #登录loginserver成功 login ok, subid= 1 connect 404 User Not Found #登录点登录失败,状态码为404 ===> echo 0 3rd/lua/lua: my_workspace/myclient.lua:38: Server closed stack traceback: [C]: in function 'error' my_workspace/myclient.lua:38: in upvalue 'try_recv' my_workspace/myclient.lua:46: in local 'readpackage' my_workspace/myclient.lua:144: in main chunk [C]: in ? $
15.3.2 403 index已过期
403状态码表示index已经过期了,index主要用于防止他们恶意使用handshake来登录,handshake使用后一次必须累加index,例如登录完成后index为1,断线重连,这个时候index=2。如果还是使用之前的index=1
那么就会直接返回403.状态码。
下面我们来再次改写myclient.lua,修改如下:
--在末尾添加这几行代码,即使用相同的handshake(index)不变的情况下,再次尝试登录连接。
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002 )) --连接登录点对应的ip端口
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac)) --发送handshake
print(readpackage()) --接收应答
print("===>",send_request(text,0))
print("<===",recv_response(readpackage()))
print("disconnect")
socket.close(fd)
运行客户端:
$ 3rd/lua/lua my_workspace/myclient.lua sceret is ecc19383c970bdb9 200 NQ== login ok, subid= 5 connect 200 OK ===> echo 0 <=== true ECHO 0 disconnect #断开连接 connect #使用相同的handshake重新登录 403 Index Expired #状态码index已经过期 ===> echo 0 3rd/lua/lua: my_workspace/myclient.lua:38: Server closed stack traceback: [C]: in function 'error' my_workspace/myclient.lua:38: in upvalue 'try_recv' my_workspace/myclient.lua:46: in local 'readpackage' my_workspace/myclient.lua:154: in main chunk [C]: in ? $
15.3.3 断线重连
如果想要断线重连使用当前subid恢复连接,需要给index+1,重新计算handshake,需要这么改写myclient.lua
--第二次连接
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002 )) --连接登录点对应的ip端口
index = index + 1 --index加一
handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index) --重新计算handshake
hmac = crypt.hmac64(crypt.hashkey(handshake), secret) --
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac)) --发送handshake
print(readpackage()) --接收应答
print("===>",send_request(text,0))
print("<===",recv_response(readpackage()))
print("disconnect")
socket.close(fd)
运行结果:
$ 3rd/lua/lua my_workspace/myclient.lua sceret is 8878e738e831e1f0 200 NQ== login ok, subid= 5 connect 200 OK ===> echo 0 <=== true ECHO 0 disconnect connect #重新连接 200 OK #连接成功 ===> echo 0 <=== true ECHO 0 disconnect $
15.4 请求与应答
请求包发送给msgserver的,但是除了遵循两字节数据长度协议外,数据内容还需要遵循以下规则:
len request session
| | |
两字节长度 请求内容 四字节sessionID
由于需要msgserver的请求应答并不需要同步,可以是多个请求一起发送,不用等上一个应答到了,才请求下一个,为了把请求与应答对应起来,就需要添加一个sessionID。整个数据包如下:
--发送"12345" sessionID为1、组合好的数据包如下:
\x00\x09 \x31\x32\x33\x34\x35 \x00\x00\x00\x01
应答包收到后需要解析,需要遵循以下规则来解析:
len response ok session
| | | |
两字节长度 响应内容 一字节状态值 四字节sessionID
例如:
--应答返回"12345"
\x00\x0a \x31\x32\x33\x34\x35 \x01 \x00\x00\x00\x01
15.4.1获取最后一次返回
如果发送完请求后,还未等到响应就断开连接了,断线重连后,想获取最后一次的返回,这可以这么写客户端代码:
local text = "echo"
local index = 1
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002 )) --连接登录点对应的ip端口
last = ""
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index) --index用于断链恢复
local hmac = crypt.hmac64(crypt.hashkey(handshake), secret) --加密握手hash值得到hmac,最要是保证handshake数据接收无误
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac)) --发送handshake
print(readpackage()) --接收应答
print("===>",send_request(text,0)) --session 0 的请求发送出去
--print("<===",recv_response(readpackage())) --不接收应答就断开连接
print("disconnect")
socket.close(fd)
--断线重连
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002))
last = ""
index = index + 1
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index)
local hmac = crypt.hmac64(crypt.hashkey(handshake), secret)
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac))
print(readpackage())
print("===>",send_request("fake",0)) --伪装session0请求,再发送出去一次,发送内容可以随便填
print("===>",send_request("again",1)) --发送请求again,session+1。
print("<===",recv_response(readpackage()))
print("<===",recv_response(readpackage()))
print("disconnect")
socket.close(fd)
运行结果:
$ 3rd/lua/lua my_workspace/myclient.lua sceret is 2986b5b04a669ea7 200 Ng== login ok, subid= 6 connect 200 OK ===> echo 0 #发送完请求,不接收 disconnect connect 200 OK ===> fake 0 #假装session 0的发送, ===> again 1 <=== true ECHO 0 #应答返回并没有返回fake,而是之前的echo <=== true AGAIN 1 #使用session1的发送请求就能得到正常的应答 disconnect $
上面可以看到,想得到以后一次的响应,就把任意的请求内容和最后一次的对应的session组合再发送一次。
15.4.2 获取历史应答
只要是对应的session已经发送过了,就能获取到响应。
代码如下:
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002 )) --连接登录点对应的ip端口
last = ""
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index) --index用于断链恢复
local hmac = crypt.hmac64(crypt.hashkey(handshake), secret) --加密握手hash值得到hmac,最要是保证handshake数据接收无误
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac)) --发送handshake
print(readpackage()) --接收应答
print("===>",send_request(text,0)) --发送两次
print("===>",send_request(text,1))
--print("<===",recv_response(readpackage())) --不管是否已经接受了
--print("<===",recv_response(readpackage()))
print("disconnect")
socket.close(fd)
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002))
last = ""
index = index + 1
handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index)
hmac = crypt.hmac64(crypt.hashkey(handshake), secret)
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac))
print(readpackage())
print("===>",send_request("fake",0)) -- request again (use last session 0, so the request message is fake)
print("===>",send_request("again",1)) -- request again (use new session)
print("<===",recv_response(readpackage()))
print("<===",recv_response(readpackage()))
print("disconnect")
socket.close(fd)
运行结果:
$ 3rd/lua/lua my_workspace/myclient.lua sceret is 6b679b5ac461ce45 200 MjU= login ok, subid= 25 connect 200 OK ===> echo 0 ===> echo 1 disconnect connect 200 OK ===> fake 0 ===> again 1 <=== true ECHO 0 #获取到是上面的echo 0 的返回 <=== true ECHO 1 #获取到的是上面echo 1 的返回 disconnect
15.4.3 服务应答异常
在服务mymsgserver中的接收到请求后,会自动剥离协议中的len以及session,得到请求内容,如果
在处理请求内容的时候,应答出现异常,那么会返回ok的值为0.
例如,在mymsgserver.lua中的request_handler添加一行
error("request_handler")
运行结果:
$ 3rd/lua/lua my_workspace/myclient.lua sceret is a572bd350328d80d 200 MQ== login ok, subid= 1 connect 200 OK ===> echo 0 disconnect connect 200 OK ===> fake 0 ===> again 1 <=== false 0 #返回false 并且没有响应内容 <=== false 1 #返回false 并且没有响应内容 disconnect
15.5 agent服务
一般网关服务登录完毕后,会启动一个agent服务来专门处理客户端的请求。下面我们来写一个mymsgagent.lua
local skynet = require "skynet"
skynet.register_protocol {
name = "client",
id = skynet.PTYPE_CLIENT,
unpack = skynet.tostring,
}
local gate
local userid, subid
local CMD = {}
function CMD.login(source, uid, sid, secret) --登录成功,secret可以用来加解密数据
-- you may use secret to make a encrypted data stream
skynet.error(string.format("%s is login", uid))
gate = source
userid = uid
subid = sid
-- you may load user data from database
end
local function logout() --退出登录,需要通知gate来关闭连接
if gate then
skynet.call(gate, "lua", "logout", userid, subid)
end
skynet.exit()
end
function CMD.logout(source)
-- NOTICE: The logout MAY be reentry
skynet.error(string.format("%s is logout", userid))
logout()
end
function CMD.disconnect(source) --gate发现client的连接断开了,会发disconnect消息过来这里不要登出
-- the connection is broken, but the user may back
skynet.error(string.format("disconnect"))
end
skynet.start(function()
-- If you want to fork a work thread , you MUST do it in CMD.login
skynet.dispatch("lua", function(session, source, command, ...)
local f = assert(CMD[command])
skynet.ret(skynet.pack(f(source, ...)))
end)
skynet.dispatch("client", function(_,_, msg)
skynet.error("recv:", msg)
skynet.ret(string.upper(msg))
if(msg == "quit")then --一旦收到的消息是quit就退出当前服务,并且关闭连接
logout()
end
end)
end)
修改mymsgsever.lua
local msgserver = require "snax.msgserver"
local crypt = require "skynet.crypt"
local skynet = require "skynet"
local loginservice = tonumber(...) --从启动参数获取登录服务的地址
local server = {} --一张表,里面需要实现前面提到的所有回调接口
local servername
local subid = 0
local agents = {}
function server.login_handler(uid, secret)
subid = subid + 1
local username = msgserver.username(uid, subid, servername)--通过uid以及subid获得username
skynet.error("uid",uid, "login,newusername", username)
msgserver.login(username, secret)--正在的登录
agent = skynet.newservice("mymsgagent")
skynet.call(agent, "lua", "login", uid, subid, secret)
agents[username] = agent
return subid
end
--一般给agent调用
function server.logout_handler(uid, subid)
local username = msgserver.username(uid, subid, servername)
msgserver.logout(username) --登出
skynet.call(loginservice, "lua", "logout",uid, subid) --通知一下loginservice已经退出
agents[username] = nil
end
--一般给loginserver调用
function server.kick_handler(uid, subid)
local username = msgserver.username(uid, subid, servername)
local agent = agents[username]
if agent then
--这里使用pcall来调用skynet.call避免由于agent退出造成异常发生
pcall(skynet.call, agent, "lua", "logout") --通知一下agent,让它退出服务。
end
end
--当客户端断开了连接,这个回调函数会被调用
function server.disconnect_handler(username)
skynet.error(username, "disconnect")
end
--当接收到客户端的请求,跟gateserver一样需要转发这个消息给agent,不同的是msgserver还需要response返回值
--,而gateserver并不负责这些事
function server.request_handler(username, msg)
skynet.error("recv", msg, "from", username)
--返回值必须是字符串,所以不管之前的数据是否是字符串,都转换一遍
return skynet.tostring(skynet.rawcall(agents[username], "client", msg))
end
--注册一下登录点服务,主要是考诉loginservice这个登录点
function server.register_handler(name)
servername = name
skynet.call(loginservice, "lua", "register_gate", servername, skynet.self())
end
msgserver.start(server) --需要配置信息,跟gateserver一样,端口、ip,外加一个登录点名称
修改mylogin.lua
local login = require "snax.loginserver"
local crypt = require "skynet.crypt"
local skynet = require "skynet"
local server_list = {}
local login_users = {}
local server = {
host = "127.0.0.1",
port = 8001,
multilogin = false, -- disallow multilogin
name = "login_master",
}
function server.auth_handler(token)
-- the token is base64(user)@base64(server):base64(password)
local user, server, password = token:match("([^@]+)@([^:]+):(.+)")--通过正则表达式,解析出各个参数
user = crypt.base64decode(user)
server = crypt.base64decode(server)
password = crypt.base64decode(password)
skynet.error(string.format("%s@%s:%s", user, server, password))
assert(password == "password", "Invalid password")
return server, user
end
function server.login_handler(server, uid, secret)
local msgserver = assert(server_list[server], "unknow server")
skynet.error(string.format("%s@%s is login, secret is %s", uid, server, crypt.hexencode(secret)))
local last = login_users[uid]
if last then --判断是否登录,如果已经登录了,那就退出之前的登录
skynet.call(last.address, "lua", "kick", uid, last.subid)
end
local id = skynet.call(msgserver, "lua", "login", uid, secret) --将uid以及secret发送给登陆点,让它做好准备,并且返回一个subid
login_users[uid] = { address=msgserver, subid=id}
return id
end
local CMD = {}
function CMD.register_gate(server, address)
skynet.error("cmd register_gate")
server_list[server] = address
end
function CMD.logout(uid, subid) --专门用来处理登出的数据清除,用户信息保存等
local u = login_users[uid]
if u then
print(string.format("%s@%s is logout", uid, u.server))
login_users[uid] = nil
end
end
function server.command_handler(command, ...)
local f = assert(CMD[command])
return f(...)
end
login(server) --服务启动需要参数
修改myclient.lua
package.cpath = "luaclib/?.so"
local socket = require "client.socket"
local crypt = require "client.crypt"
if _VERSION ~= "Lua 5.3" then
error "Use lua 5.3"
end
local fd = assert(socket.connect("127.0.0.1", 8001))
local function writeline(fd, text)
socket.send(fd, text .. "\n")
end
local function unpack_line(text)
local from = text:find("\n", 1, true)
if from then
return text:sub(1, from-1), text:sub(from+1)
end
return nil, text
end
local last = ""
local function unpack_f(f)
local function try_recv(fd, last)
local result
result, last = f(last)
if result then
return result, last
end
local r = socket.recv(fd)
if not r then
return nil, last
end
if r == "" then
error "Server closed"
end
return f(last .. r)
end
return function()
while true do
local result
result, last = try_recv(fd, last)
if result then
return result
end
socket.usleep(100)
end
end
end
local readline = unpack_f(unpack_line)
local challenge = crypt.base64decode(readline()) --接收challenge
local clientkey = crypt.randomkey()
writeline(fd, crypt.base64encode(crypt.dhexchange(clientkey))) --把clientkey换算后比如称它为ckeys,发给服务器
local secret = crypt.dhsecret(crypt.base64decode(readline()), clientkey) --服务器也把serverkey换算后比如称它为skeys,发给客户端,客户端用clientkey与skeys所出secret
print("sceret is ", crypt.hexencode(secret)) --secret一般是8字节数据流,需要转换成16字节的hex字符串来显示。
local hmac = crypt.hmac64(challenge, secret) --加密的时候还是需要直接传递secret字节流
writeline(fd, crypt.base64encode(hmac))
local token = {
server = "sample",
user = "nzhsoft",
pass = "password",
}
local function encode_token(token)
return string.format("%s@%s:%s",
crypt.base64encode(token.user),
crypt.base64encode(token.server),
crypt.base64encode(token.pass))
end
local etoken = crypt.desencode(secret, encode_token(token)) --使用DES加密token得到etoken, etoken是字节流
writeline(fd, crypt.base64encode(etoken)) --发送etoken,mylogin.lua将会调用auth_handler回调函数, 以及login_handler回调函数。
local result = readline() --读取最终的返回结果。
print(result)
local code = tonumber(string.sub(result, 1, 3))
assert(code == 200)
socket.close(fd) --可以关闭链接了
local subid = crypt.base64decode(string.sub(result, 5)) --解析出subid
print("login ok, subid=", subid)
----- connect to game server 新增内容,以下通信协议全是两字节数据长度协议。
local function send_request(v, session) --打包数据v以及session
local size = #v + 4
local package = string.pack(">I2", size)..v..string.pack(">I4", session)
socket.send(fd, package)
return v, session
end
local function recv_response(v)--解包数据v得到content(内容)、ok(是否成功)、session(会话序号)
local size = #v - 5
local content, ok, session = string.unpack("c"..tostring(size).."B>I4", v)
return ok ~=0 , content, session
end
local function unpack_package(text)--解析两字节数据长度协议包
local size = #text
if size < 2 then
return nil, text
end
local s = text:byte(1) * 256 + text:byte(2)
if size < s+2 then
return nil, text
end
return text:sub(3,2+s), text:sub(3+s)
end
local readpackage = unpack_f(unpack_package)
local function send_package(fd, pack)
local package = string.pack(">s2", pack)
socket.send(fd, package)
end
local text = "echo"
local index = 1
local session = 0
print("connect")
fd = assert(socket.connect("127.0.0.1", 8002 )) --连接登录点对应的ip端口
last = ""
local handshake = string.format("%s@%s#%s:%d", crypt.base64encode(token.user), crypt.base64encode(token.server),crypt.base64encode(subid) , index) --index用于断链恢复
local hmac = crypt.hmac64(crypt.hashkey(handshake), secret) --加密握手hash值得到hmac,最要是保证handshake数据接收无误
send_package(fd, handshake .. ":" .. crypt.base64encode(hmac)) --发送handshake
print(readpackage()) --接收应答
while(true) do
text = socket.readstdin() --循环读取标准数据发送给服务器
if text then
print("===>",send_request(text,session))
print("<===",recv_response(readpackage()))
session = session + 1 --会话ID自动递增
end
socket.usleep(100)
end
print("disconnect")
socket.close(fd)
先运行服务testmsgserver,再运行两个myclient,运行结果:
$ ./skynet examples/conf testmsgserver [:0100000a] LAUNCH snlua testmsgserver [:0100000b] LAUNCH snlua mylogin [:0100000c] LAUNCH snlua mylogin [:0100000d] LAUNCH snlua mylogin [:0100000e] LAUNCH snlua mylogin [:0100000f] LAUNCH snlua mylogin [:01000010] LAUNCH snlua mylogin [:01000012] LAUNCH snlua mylogin [:01000013] LAUNCH snlua mylogin [:01000014] LAUNCH snlua mylogin [:0100000b] login server listen at : 127.0.0.1 8001 [:01000015] LAUNCH snlua mymsgserver 16777227 [:01000015] Listen on 0.0.0.0:8002 [:0100000b] cmd register_gate [:0100000c] connect from 127.0.0.1:51882 (fd = 8) [:0100000c] nzhsoft@sample:password [:0100000b] nzhsoft@sample is login, secret is c4e4d6986346fca2 [:01000015] uid nzhsoft login,newusername bnpoc29mdA==@c2FtcGxl#MQ== [:01000016] LAUNCH snlua mymsgagent #启动了一个新的agent来处理nzhsoft用户的请求 [:01000016] nzhsoft is login [:01000015] recv aaaaaaaaaaaa from bnpoc29mdA==@c2FtcGxl#MQ== [:01000016] recv: aaaaaaaaaaaa [:01000015] recv quit from bnpoc29mdA==@c2FtcGxl#MQ== [:01000016] recv: quit #收到quit消息就退出服务 [:0100000b] nzhsoft@nil is logout [:01000016] KILL self