參考自:Skynet基礎入門例子詳解(5)(魔王魂影)
Sproto是雲風專門爲Skynet開發的輕量協議。
借鑑官方例子,我們可以在 ./examples/proto.lua 中添加自己的一個 “say” 協議
local sprotoparser = require "sprotoparser"
local proto = {}
proto.c2s = sprotoparser.parse [[
.package {
type 0 : integer
session 1 : integer
}
handshake 1 {
response {
msg 0 : string
}
}
get 2 {
request {
what 0 : string
}
response {
result 0 : string
}
}
set 3 {
request {
what 0 : string
value 1 : string
}
}
quit 4 {}
say 5 {
request {
name 0 : string
msg 1 : string
}
}
]]
proto.s2c = sprotoparser.parse [[
.package {
type 0 : integer
session 1 : integer
}
heartbeat 1 {}
]]
return proto
然後編寫自己的客戶端 ./examples/client2.lua
package.cpath = "luaclib/?.so"
package.path = "lualib/?.lua;examples/?.lua"
if _VERSION ~= "Lua 5.3" then
error "Use lua 5.3"
end
local socket = require "clientsocket"
-- 通信協議
local proto = require "proto"
local sproto = require "sproto"
local host = sproto.new(proto.s2c):host "package"
local request = host:attach(sproto.new(proto.c2s))
local fd = assert(socket.connect("127.0.0.1", 8888))
local session = 0
local function send_request(name, args)
session = session + 1
local str = request(name, args, session)
-- 解包測試
--[=[
local host2 = sproto.new(proto.c2s):host "package"
local req_type, name, arg, func = host2:dispatch(str)
print(req_type)
print(name)
if type(arg) == "table" then
print(arg.name)
print(arg.msg)
end
print(func)
--]=]
socket.send(fd, str)
print("Request:", session)
end
send_request("handshake") --此處如果多次發送數據,可能造成收端粘包,致使數據無法正常解析
while true do
-- 接收服務器返回消息
local str = socket.recv(fd)
-- print(str)
if str~=nil and str~="" then
print("server says: "..str)
-- socket.close(fd)
-- break;
end
-- 讀取用戶輸入消息
local readstr = socket.readstdin()
if readstr then
if readstr == "quit" then
send_request("quit")
socket.close(fd)
break
else
-- 把用戶輸入消息發送給服務器
send_request("say", { name = "mick", msg = readstr })
end
else
socket.usleep(100)
end
end
我們知道
local host = sproto.new(proto.s2c):host "package"
local request = host:attach(sproto.new(proto.c2s))
host 和request 分別是一對打包和解包協議,可以在本地檢測。發現解包函數返回了四個參數:請求類型,請求名,請求參數,迴應函數。其中,請求名和請求參數正好與打包函數裏的入參 name 及 args 對應。
最後是服務器 ./examples/socket2.lua
local skynet = require "skynet"
local socket = require "socket"
local proto = require "proto"
local sproto = require "sproto"
local host
local REQUEST = {}
function REQUEST:say()
print("say", self.name, self.msg)
end
function REQUEST:handshake()
print("handshake")
end
function REQUEST:quit()
print("quit")
end
local function request(name, args, response)
local f = assert(REQUEST[name])
local r = f(args)
do return name end
if response then
-- 生成迴應包(response是一個用於生成迴應包的函數。)
-- 處理session對應問題
-- return response(r)
end
end
local function send_package(fd,pack)
-- 協議與客戶端對應(兩字節長度包頭+內容)
local package = string.pack(">s2", pack)
socket.write(fd, package)
end
local function accept(id)
-- 每當 accept 函數獲得一個新的 socket id 後,並不會立即收到這個 socket 上的數據。這是因爲,我們有時會
-- 希望把這個 socket 的操作權轉讓給別的服務去處理。
-- 任何一個服務只有在調用 socket.start(id) 之後,纔可以收到這個 socket 上的數據。
socket.start(id)
host = sproto.new(proto.c2s):host "package"
-- request = host:attach(sproto.new(proto.c2s))
while true do
local str = socket.read(id)
if str then
local type,str2,str3,str4 = host:dispatch(str)
if type=="REQUEST" then
-- REQUEST : 第一個返回值爲 "REQUEST" 時,表示這是一個遠程請求。如果請求包中沒有 session
-- 字段,表示該請求不需要回應。這時,第 2 和第 3 個返回值分別爲消息類型名(即在 sproto 定義
-- 中提到的某個以 . 開頭的類型名),以及消息內容(通常是一個 table );如果請求包中有
-- session 字段,那麼還會有第 4 個返回值:一個用於生成迴應包的函數。
local ok, result = pcall(request, str2,str3,str4)
print("ok result", ok, result)
if ok then
if result then
socket.write(id, "收到了" .. result)
-- 暫時不使用迴應包迴應
-- print("response:"..result)
-- send_package(id,result)
end
else
skynet.error(result)
end
end
if str2 == "quit" then
socket.close(id)
return
end
if type=="RESPONSE" then
-- RESPONSE :第一個返回值爲 "RESPONSE" 時,第 2 和 第 3 個返回值分別爲 session 和
-- 消息內容。消息內容通常是一個 table ,但也可能不存在內容(僅僅是一個迴應確認)。
-- 暫時不處理客戶端的迴應
print("client response")
end
else
socket.close(id)
return
end
end
end
skynet.start(function()
print("==========Socket Start=========")
local id = socket.listen("127.0.0.1", 8888)
print("Listen socket :", "127.0.0.1", 8888)
socket.start(id , function(id, addr)
-- 接收到客戶端連接或發送消息()
print("connect from " .. addr .. " " .. id)
-- 處理接收到的消息
accept(id)
end)
end)
最後別忘了在 ./examples/main.lua 中起一個 socket2.lua 服務。
這個例子讓我清晰了不少:
1、打包與解包;
2、服務器中,pcall() 函數有兩個返回值,第一個返回值應該在程序正常運行完成後返回 true,後面的返回值來自 pcall 調用的函數的返回值。
3、鑑於TCP的流服務,我們在客戶端代碼中,多次數據的發送過程代碼儘量用 readstdin 分離開,否則所有數據可能一同到達服務器,而服務器並沒有針對TCP數據流的粘包和拼包處理。這裏可以參見 ./examples/client.lua 中的處理。
於是有了下面改進過的服務器代碼:
local skynet = require "skynet"
local socket = require "socket"
local proto = require "proto"
local sproto = require "sproto"
local host
local last = ""
local REQUEST = {}
function REQUEST:say()
print("say", self.name, self.msg)
end
function REQUEST:handshake()
print("handshake")
end
function REQUEST:quit()
print("quit")
end
local function request(name, args, response)
local f = assert(REQUEST[name])
local r = f(args)
do return name end
if response then
-- 生成迴應包(response是一個用於生成迴應包的函數。)
-- 處理session對應問題
-- return response(r)
end
end
local function unpack_package(text)
local size = #text
if size < 2 then
print("size: ", size)
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 function recv_package(last, fd)
local result
result, last = unpack_package(last)
if result then
return result, last
end
local r = socket.read(fd)
if r then
return nil, last .. r
else
return nil, nil
end
end
local function accept(id)
socket.start(id)
last = ""
host = sproto.new(proto.c2s):host "package"
-- request = host:attach(sproto.new(proto.c2s))
while true do
local str
str, last = recv_package(last, id)
if str then
local type,str2,str3,str4 = host:dispatch(str)
if type=="REQUEST" then
local ok, result = pcall(request, str2,str3,str4)
print("ok result", ok, result)
if ok then
if result then
socket.write(id, "收到了" .. result .. '\n')
-- 暫時不使用迴應包迴應
-- print("response:"..result)
-- send_package(id,result)
end
else
skynet.error(result)
end
end
if str2 == "quit" then
print("quit!")
socket.close(id)
return
end
if type=="RESPONSE" then
-- 暫時不處理客戶端的迴應
print("client response")
end
elseif not last then
print("disconnected!")
socket.close(id)
return
end
end
end
skynet.start(function()
print("==========Socket Start=========")
local id = socket.listen("127.0.0.1", 8888)
print("Listen socket :", "127.0.0.1", 8888)
socket.start(id , function(id, addr)
-- 接收到客戶端連接或發送消息()
print("connect from " .. addr .. " " .. id)
-- 處理接收到的消息
accept(id)
end)
end)
此外,客戶端也得修改,需要添加打包代碼:
package.cpath = "luaclib/?.so"
package.path = "lualib/?.lua;examples/?.lua"
if _VERSION ~= "Lua 5.3" then
error "Use lua 5.3"
end
local socket = require "clientsocket"
-- 通信協議
local proto = require "proto"
local sproto = require "sproto"
local host = sproto.new(proto.s2c):host "package"
local request = host:attach(sproto.new(proto.c2s))
local fd = assert(socket.connect("127.0.0.1", 8888))
local function send_package(fd,pack)
-- 協議與客戶端對應(兩字節長度包頭+內容)
local package = string.pack(">s2", pack)
socket.send(fd, package)
end
local session = 0
local function send_request(name, args)
session = session + 1
local str = request(name, args, session)
send_package(fd, str)
print("Request:", session)
end
send_request("handshake")
send_request("say", { name = "mick", msg = "hello world" })
while true do
-- 接收服務器返回消息
local str = socket.recv(fd)
-- print(str)
if str~=nil and str~="" then
print("server says: "..str)
-- socket.close(fd)
-- break;
end
-- 讀取用戶輸入消息
local readstr = socket.readstdin()
if readstr then
if readstr == "quit" then
send_request("quit")
socket.close(fd)
break
else
-- 把用戶輸入消息發送給服務器
send_request("say", { name = "mick", msg = readstr })
end
else
socket.usleep(100)
end
end
這樣就爲client ----> server 的傳輸過程添加了打包解包過程,client端在起始處就可以多次發送 send_request() 而不用擔心粘包問題。上面的版本對於 server ---> client 的傳輸過程並未添加打包解包過程,我們可以進一步仿照上面的版本添加,同時還可以嘗試服務器在收到 request 進行迴應,由於目前我還不清楚 response() 函數的具體工作內容,但該函數內部進行了打包處理,我們可以嘗試用一下。當然,如果是服務端單獨發送消息給客戶端(如心跳包),這樣就需要和客戶端一樣調用sproto進行封包。(具體可參考examples/agent)
要添加回應,還需修改 sproto,修改如下:
say 5 {
request {
name 0 : string
msg 1 : string
}
response {
name 0 : string
msg 1 : string
}
}
local skynet = require "skynet"
local socket = require "socket"
local proto = require "proto"
local sproto = require "sproto"
local host
local last = ""
local REQUEST = {}
function REQUEST:say()
print("say", self.name, self.msg)
return {name = "cxl", msg = "hello"} #對應 response ,即響應。改值會調用 response 打包
end
function REQUEST:handshake()
print("handshake")
end
function REQUEST:quit()
print("quit")
end
local function request(name, args, response)
local f = assert(REQUEST[name])
local r = f(args)
if response and r then
-- 生成迴應包(response是一個用於生成迴應包的函數。)
-- 處理session對應問題
return response(r)
end
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 function recv_package(last, fd)
local result
result, last = unpack_package(last)
if result then
return result, last
end
local r = socket.read(fd)
if r then
return nil, last .. r
else
return nil, nil
end
end
local function send_package(id, pack)
local package = string.pack(">s2", pack)
socket.write(id, package)
end
local function accept(id)
socket.start(id)
last = ""
host = sproto.new(proto.c2s):host "package"
host2 = sproto.new(proto.s2c):host "package"
-- request = host:attach(sproto.new(proto.c2s))
while true do
local str
str, last = recv_package(last, id)
if str then
local type,str2,str3,str4 = host:dispatch(str)
if type=="REQUEST" then
local ok, result = pcall(request, str2,str3,str4)
if ok then
if result then
send_package(id,result)
end
else
skynet.error(result)
end
end
if str2 == "quit" then
print("quit!")
socket.close(id)
return
end
if type=="RESPONSE" then
-- 暫時不處理客戶端的迴應
print("client response")
end
elseif not last then
print("disconnected!")
socket.close(id)
return
end
end
end
skynet.start(function()
print("==========Socket Start=========")
local id = socket.listen("127.0.0.1", 8888)
print("Listen socket :", "127.0.0.1", 8888)
socket.start(id , function(id, addr)
-- 接收到客戶端連接或發送消息()
print("connect from " .. addr .. " " .. id)
-- 處理接收到的消息
accept(id)
end)
end)
結果如下:
客戶端從終端接收輸入:
服務器:
可以發現兩邊的打包解包全都正確,而且服務器對客戶端的每次請求都有響應,目前響應都是 {name = "cxl", msg = "hello"}。當初想在服務器端直接對 response 的包用 dispatch() 解包,直接報錯。這是因爲對於 response 類型的解包,dispatch() 知道是迴應,就會知道是自己啓動的該次服務,就回去查找對應 session id。但其實,我們屬於提前解包,並未在客戶端解包,服務器端是沒有該 session id的,導致報錯。