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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章