【Skynet】Sproto初學與應用

參考自: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
	}
}


這樣,就需要在服務器端處理函數中返回一個 response 表格。服務器代碼如下:
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的,導致報錯。 


發佈了129 篇原創文章 · 獲贊 47 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章