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