源碼分爲3個文件夾,分別爲service、lualib、src。
其中service主要是服務端 業務邏輯 ,lualib爲 基礎工具封裝 ,src爲 C語言服務封裝 。
一般閱讀代碼時先從main入手,跟着邏輯一步一步不斷深入。
main.lua
文件路徑:service/main.lua
local skynet = require "skynet"
skynet.start(function()
skynet.error("Server start") -- 打印Server start
if not skynet.getenv "daemon" then
local console = skynet.newservice("console") -- 創建控制檯
end
skynet.newservice("debug_console",8000) -- 啓動控制檯服務
local proto = skynet.uniqueservice "protoloader" -- 啓動封裝的protoloader服務
skynet.call(proto, "lua", "load", { -- 調用protoloader服務的load接口
"proto.c2s", -- 客戶端 to 服務器
"proto.s2c", -- 服務器 to 客戶端
})
local hub = skynet.uniqueservice "hub" -- 啓動hub服務
skynet.call(hub, "lua", "open", "0.0.0.0", 5678) -- 調用hub服務中的open接口,監聽5678網絡端口
skynet.exit()
end)
入口函數很簡介,首先啓動了控制檯服務,該服務使得skynet在運行時,可以打印log以及信息到終端界面。
protoloader服務爲proto協議的封裝,注意這裏的protoloader爲自定義封裝,而不是skynet核心代碼裏自帶的那個。
首先調用skynetuniqueservice掛起了protoloader服務,所以接下來看看protoloader是怎麼運行的。
protoloader.lua
文件路徑:service/protoloader.lua
local skynet = require "skynet"
local sprotoparser = require "sprotoparser" --加載skynet/lualib下的sproto解析器
local sprotoloader = require "sprotoloader" --sproto加器器
local service = require "service" --加載範例根目錄lualib下的service
local log = require "log" --加載範例根目錄lualib下的log
local loader = {} --保存函數
local data = {} --保存加載後的sproto協議在skynet sprotoparser裏的序號,key值爲文件名
local function load(name)
local filename = string.format("proto/%s.sproto", name)
local f = assert(io.open(filename), "Can't open " .. name)
local t = f:read "a"
f:close() --以上爲讀取文件內容
return sprotoparser.parse(t) --調用skynet的sprotoparser解析sproto協議
end
function loader.load(list)
for i, namein ipairs(list) do
local p = load(name) --加載sproto協議
log("load proto [%s] in slot %d", name, i)
data[name] = i
sprotoloader.save(p, i) --保存解析後的sproto協議
end
end
function loader.index(name)
return data[name] --返回sproto協議在skynet sprotoloader裏序號
end
-- 初始化服務的info信息和函數
service.init {
command = loader,
info = data
}
可以看到主要是定義瞭如何加載proto協議並保存,以及獲取協議的編號(slot),然後最後面調用了另一個lua文件的代碼service.init方法。
service.lua
文件路徑:lualib/service.lua
local skynet = require "skynet"
local log = require "log"
local service = {}
-- 初始化服務,主要功能爲:1:註冊服務info 2:註冊服務的命令函數 3:啓動服務
function service.init(mod)
local funcs = mod.command
if mod.infothen
skynet.info_func(function() --
return mod.info
end)
-- 這裏僅作調試用,當在調試模式下,輸入 “info 服務ID” 就會打印上面返回的信息
-- 調試模式的啓動方法爲 nc 127.0.0.1 8000
end
skynet.start(function()
if mod.require then
local s = mod.require
for _, namein ipairs(s) do
service[name] = skynet.uniqueservice(name) --啓動服務,並將該服務器保存在service下
end
end
if mod.initthen
mod.init()
end
skynet.dispatch("lua", function (_,_, cmd, ...) -- 修改lua協議的dispatch函數,對當前調用init的服務註冊函數
-- skynet.dispatch函數也是服務啓動的結束標示
local f = funcs[cmd] --獲取命令函數
if f then
skynet.ret(skynet.pack(f(...))) --返回命令調用結果,所有通過ret的返回值都要用pack打包
else
log("Unknown command : [%s]", cmd)
skynet.response()(false)
end
end)
end)
end
--[[skynet.ret 在當前協程(爲處理請求方消息而產生的協程)中給請求方(消息來源)的消息做迴應
skynet.retpack 跟skynet.ret的區別是向請求方作迴應時要用skynet.pack打包]]--
return service
該代碼做的主要有3個工作,
- 設定當前skynet服務debug info信息
- 設定當前skynet服務在接收到其他服務調用信息時的處理,比如上一段protoloader代碼中的load函數和index函數。其他服務只要發送”函數名”或者“參數”,即可調用對應的函數。其中一個值得注意的點是,skynet中服務與服務的通信必須通過 skynet.ret 和 skynet.retpack 函數進行打包和封包處理。
- 加載其他服務,並保存在service這個table中。
- 往後所有服務的代碼都會通過init函數處理info信息、註冊函數、加載其他服務。
回到main.lua中,可以看到調用了service.init中設定的load函數。接着掛起了hub服務。
爲了不讓文章變的太過冗長,下面會省略一些具體實現邏輯,主要以梳理流程爲主,只有在關鍵點纔會有詳細的解釋。
hub.lua
文件路徑:service/hub.lua
local skynet = require "skynet"
local socket = require "socket"
local proxy = require "socket_proxy" --加載範例lualib目錄下的socket_proxy
local log = require "log"
local service = require "service"
local hub = {} --保存函數
local data = { socket = {} } --保存監聽到的鏈接
-- 調用auth服務
local function auth_socket(fd)
...
end
local function assign_agent(fd, userid)
...
end
--所有監聽到的新鏈接都會首先交給這個函數處理
function new_socket(fd, addr)
...
end
--打開監聽
function hub.open(ip, port)
...
end
--關閉監聽
function hub.close()
...
end
--服務初始化,並掛起auth和manager服務
service.init {
...
require = {
"auth",
"manager",
}
}
可以看到沒有執行具體邏輯,主要以定義爲主,並啓動了兩個服務,manager和auth。
在最開始local proxy = require “socket_proxy”中還啓動了socket_proxyd服務。
不過在這邊並有具體的邏輯執行,所以先不管,順着流程繼續看。
監聽網絡端口
回到main.lua,調用了hub服務中的open函數,開始監聽網絡端口。來看看該函數的具體實現。
function hub.open(ip, port)
log("Listen %s:%d", ip, port)
assert(data.fd == nil, "Already open") --判斷監聽是否打開
data.fd = socket.listen(ip, port) --新建監聽端口
data.ip = ip
data.port = port
socket.start(data.fd, new_socket) --開始監聽,將監聽到的鏈接返回到new_socket函數
end
這時如果客戶端發起請求,就會將監聽到的鏈接交給new_socket函數處理
function new_socket(fd, addr)
data.socket[fd] = "[AUTH]"
proxy.subscribe(fd) --將新鏈接提交給socketproxy
local ok , userid = pcall(auth_socket, fd)
if okthen
data.socket[fd] = userid
if pcall(assign_agent, fd, userid) then
return -- succ
else
...
end
else
...
end
end
首先調用了socket_proxy.lua中的subscribe函數
文件路徑:lualib/socket_proxy.lua
function proxy.subscribe(fd)
local addr = map[fd]
if not addrthen
addr = skynet.call(proxyd, "lua", fd) --向proxyd 發送命令,創建基於fd鏈接的服務,詳見socket_proxyd.lua
map[fd] = addr --保存服務地址
end
end
向proxyd服務發送鏈接對象
文件路徑:service/socket_proxyd.lua
local function subscribe(fd)
local addr = socket_fd_addr[fd]
if addrthen
return addr --如果連接已經保存則直接返回服務ID
end
addr = assert(skynet.launch("package", skynet.self(), fd)) --創建c語言服務package(連接代理),跑當前服務(self()返回當前服務ID),返回的是c服務的地址
socket_fd_addr[fd] = addr --保存c服務的地址,key值爲鏈接
socket_addr_fd[addr] = fd --保存鏈接,key值爲c服務的地址
socket_init[addr] = skynet.response() --保存該鏈接的response函數,迴應函數,這裏同時會迴應之前call過來的服務,告訴他addr
end
這裏創建了一個c語言編寫的package服務,不必關心具體實現,只要知道它的功能主要是讓當前鏈接成爲一個單獨的skynet服務即可,返回值是該服務的ID。
最後通過skynet.response函數返回服務ID給調用前的函數,也就是socket_proxy的subscribe。
處理鏈接數據
一路回到hub.lua的new_socket函數。又將鏈接提交給auth_socket函數
local function auth_socket(fd)
return (skynet.call(service.auth, "lua", "shakehand" , fd))
end
通過service.init啓動的auth服務,調用其中的shakehand函數,併發送fd鏈接對象。
文件路徑:service/auth.lua
function auth.shakehand(fd)
local c = client.dispatch { fd = fd } --將鏈接交給client對信息進行處理
return c.userid
end
這裏可以理解爲和客戶端一第次“握手”,調用client的dispatch函數,併發送鏈接。
文件路徑:lualib/client.lua
--消息處理
function client.dispatch( c )
local fd = c.fd
proxy.subscribe(fd)
local ERROR = {}
while true do
local msg, sz = proxy.read(fd) --讀取連接發來的數據
local type, name, args, response = host:dispatch(msg, sz) --sproto解析數據
assert(type == "REQUEST") --此處保證連接數據爲請求
if c.exitthen --exit參數爲退出循環標誌
return c
end
local f = handler[name] --通過sproto解析出來的數據,獲取回調函數
if f then
-- f may block , so fork and run
-- 此處創建一個協程運行回調函數
skynet.fork(function()
local ok, result = pcall(f, c, args) -- 回調函數具體詳見agent,auth,回調函數在那邊實現
-- 此處回調函數都爲顯式傳參,將c顯式傳到回調函數中
if okthen
proxy.write(fd, response(result)) -- 使用sproto解析出來的response函數包裝返回值,併發送數據
else
log("raise error = %s", result)
proxy.write(fd, response(ERROR, result))
end
end)
else
-- unsupported command, disconnected
error ("Invalid command " .. name)
end
end
end
這個函數爲消息處理最關鍵的函數,所有請求都通過這裏進行分發,其中回調函數都保存在handler這個table中,當前範例的回調函數都在agent.lua和,auth.lua中定義。
通過代碼可以看,該函數是一個死循環,也就是說,會不斷的讀取客戶端發送過來的信息進行處理。只有當exit變量爲ture時才退出,這個exit怎麼來的先不管。
接下來通過客戶端發送的請求流程繼續梳理。
網絡請求流程
回顧下客戶端的消息流程:
首先會發送signin請求,發現sign失敗,用戶不存在,繼續發送signup請求註冊用戶,註冊成功後再次發送signin請求,登入成功,接着會發送login請求,後面的就是正常的業務請求了。請求順序如下:
signin > signup > signin > login > other
一步一步來,首先是signin,回到上面的dispatch,通過請求信息調用名爲signin回調函數。signin的回調實現在auth.lua中
文件路徑:service/auth.lua
-- signin登入請求回調
function cli:signin(args)
log("signin userid = %s", args.userid)
if users[args.userid] then
self.userid = args.userid --self爲修改隱式參數
self.exit = true --退出client中dispatch循環,表示登入成功,退出auth服務,進入下一個服務
return SUCC
else
return FAIL
end
end
可以看到,當客戶端第一次請求signin時,是返回失敗FAIL的。這時又會回到client的dispatch函數,返回客戶端註冊失敗消息後,繼續讀取鏈接信息。
當客戶端接到signin失敗後,會緊接着發送signup請求。
同樣來到auth.lua
-- signup註冊賬號請求回調
function cli:signup(args)
log("signup userid = %s", args.userid)
if users[args.userid] then
return FAIL
else
users[args.userid] = true
return SUCC
end
end
這時就回返回成功,回到dispath發送註冊成功消息。
接下來客戶端再次發送sigin請求就會signin成功。
要注意的是,在signin回調函數中,如果signin成功則會修改exit變量,這會導致退出client的dispatch循環。爲什麼這裏能修改exit變量呢,原因是所有回調函數都是 隱式傳參 的,在函數定義中使用了“ 冒號 ”,表示隱式傳送 self參數 。在dispatch中可以看到調用回調時,都會傳進c變量。pcall(f, c, args)
登入成功後就會退出dispatch的循環,那麼跟蹤代碼退到hub中的new_socket方法,發現又調用了assign_agent函數。
local function assign_agent(fd, userid)
skynet.call(service.manager, "lua", "assign", fd, userid)
end
assign_gent則調用了service中保存的manager服務,同時調用manage服務中的assign方法,並傳入鏈接對象和登入成功返回的userid。
文件路徑:service/manager.lua
function manager.assign(fd, userid)
local agent
repeat
agent = users[userid] --判斷是否有當前用戶的服務
if not agentthen --若沒有則創建一個
agent = new_agent()
if not users[userid] then
-- double check
users[userid] = agent
else
free_agent(agent)
agent = users[userid]
end
end
until skynet.call(agent, "lua", "assign", fd, userid) --此處返回ture,跳出循環
log("Assign %d to %s [%s]", fd, userid, agent)
end
順着流程走,由於客戶端第一次登入,會調用new_agent方法。
local function new_agent()
-- todo: use a pool
return skynet.newservice "agent"
end
這裏會爲每個用戶啓動一個agent服務。
接着until中會調用該agent服務的assign方法。
文件路徑:service/agent.lua
function agent.assign(fd, userid)
...
skynet.fork(new_user, fd)
return true
end
做了很簡單一件事,調用了new_user這個方法,這裏要注意的是,使用了skynet.fork方法調用,該方法的作用是,不會阻塞線程,爲什麼要這麼做接着往後看。
local function new_user(fd)
local ok, error = pcall(client.dispatch , { fd = fd }) --進入客戶端消息循環,若此處客戶端長時間沒有任何操作,而報超時錯誤返回
...
end
可以看到,這裏調用了client中的dispatch,前面知道該函數是個死循環,這就是爲什麼之前使用了skynet.fork的原因了。
好了,這樣就又進入消息處理循環了,之前客戶端已經發送了signin,signup,signin,這三個請求,接下來會發送login請求,去到login請求回調。該回調定義在agent.lua中。
function cli:login()
assert(not self.login)
if data.fdthen --重複登入
log("login fail %s fd=%d", data.userid, self.fd)
return { ok = false }
end
data.fd = self.fd
self.login = true
log("login succ %s fd=%d", data.userid, self.fd)
client.push(self, "push", { text = "welcome" }) -- push message to client
return { ok = true }
end
主要就是調用client.push函數發送welcome信息,再來看看client中push函數的實現
function client.push(c, t, data)
proxy.write(c.fd, sender(t, data))
end
調用socket_proxy中的write
function proxy.write(fd, msg, sz)
skynet.send(get_addr(fd), "client", msg, sz)
end
這裏用到了skynet.send函數,第一參數是服務地址,就是之前創建的package服務。
以上就是範例中服務端代碼的主要邏輯。另外我對服務端代碼進行了大部分註釋。
鏈接: http://pan.baidu.com/s/1dFKD2oh 密碼: rfc9