skynet框架应用 (十五) msgserver

15 msgserver

​ 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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值