master_slave模式
skynet是支持在不同機器上協作的,之間通過TCP互連。不過有兩種模式可以選,一種是master/slave模式,一種是cluster模式,這裏說說master/slave模式。
- skynet的master/slave模式是一個master與多個slave的模式,master與每個slave相連,每個slave又兩兩互連。master同時會充當一箇中心節點的作用,用來協調各個slave的工作。
- 如果在config中,配置
harbor = 0
,那麼skynet會工作帶單節點模式下,其他參數(address、master、standalone)都不用設置。 - 如果在config中配置 harbor 爲1-255中的數字,那麼skynet會工作在多節點模式下,如果配置了 standalone, 那麼此節點是中心節點。
- 只要是多節點模式, address 與 master 都需要配置,其中 address 爲此節點的地址,供本節點監聽, master 爲外部中心節點的地址,供slave連接(或者供中心節點監聽)
本文配合2016年下旬最新版skynet源碼註釋更佳
帶着問題去了解
分析master/slave模式,我是帶着以下問題去了解的。
- 全局的字符串地址是怎麼實現的
- 消息是怎麼從一個節點傳遞到另外一個節點的
skynet.uniqueservice
是怎麼創建多節點有效的服務的- 爲什麼不推薦使用master/slave模式
從bootstrap說起
bootstrap 是 skynet 中啓動的第二個服務(第一個是 logger),它是一個臨時服務(工作做完就結束了),主要工作是做一些上層管理類服務的初始化工作。 bootstrap 的部分代碼:
local standalone = skynet.getenv "standalone"
-- 獲取 config 中的 standalone 參數,如果standalone存在,它應該是一個"ip地址:端口"
local harbor_id = tonumber(skynet.getenv "harbor" or 0)
-- 獲取 config 中的 harbor 參數
-- 如果 harbor 爲 0 (即工作在單節點模式下)
if harbor_id == 0 then
assert(standalone == nil) -- 如果是單節點, standalone 不能配置
standalone = true
skynet.setenv("standalone", "true") -- 設置 standalone 的環境變量爲true
-- 如果是單節點模式,則slave服務爲 cdummy.lua
local ok, slave = pcall(skynet.newservice, "cdummy")
if not ok then
skynet.abort()
end
skynet.name(".cslave", slave)
else -- 如果是多節點模式
if standalone then -- 如果是中心節點則啓動 cmaster 服務
if not pcall(skynet.newservice,"cmaster") then
skynet.abort()
end
end
-- 如果是多節點模式,則 slave 服務爲 cslave.lua
local ok, slave = pcall(skynet.newservice, "cslave")
if not ok then
skynet.abort()
end
skynet.name(".cslave", slave)
end
if standalone then -- 如果是中心節點則啓動 datacenterd 服務
local datacenter = skynet.newservice "datacenterd"
skynet.name("DATACENTER", datacenter)
end
從代碼可以看出,從配置文件裏面讀取出 harbor 配置
- 如果是 0,代表是單節點,那麼只啓動 cdummy 作爲 slave 服務的僞裝即可
- 如果是非 0(1-255),代表是多節點,啓動 cslave 服務;如果是中心節點(配置了 standalone),還需要啓動 cmaster 與 datacenter 服務
master/slave模式的C層面的初始化
除了 bootstrap 中啓動的服務,還有一個很重要的C服務需要啓動:harbor服務(源文件爲 service-src/service_harbor.c),下面先看看C層面的初始化(僅僅看master/slave)
main 函數中的 skynet_start 函數
void skynet_start(struct skynet_config * config) skynet_harbor_init(config->harbor); // 初始化 harbor id,用來後續判斷是否是非本節點的服務地址 skynet_handle_init(config->harbor); // 將 harbor id 保存在 struct handle_storage 的高八位
怎麼啓動的harbor服務 如果是單節點模式,會啓動 cdummy 服務,在 cdummy 的 skynet.start 函數中有:
harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))
harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))
同樣,如果是多節點模式,會啓動 cslave 服務,在 cdummy 的 skynet.start 函數中有:harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))
可見不管是多節點還是單節點模式都會啓動 harbor 服務,在skynet進程啓動過程中會看到類似於:[:00000005] LAUNCH harbor 0 4
的字樣
查看相關各服務的啓動工作
從初始化過程我們可以看到,讓master/slave模式工作起來的服務主要有:cmaster、cslave、harbor服務,看看這三個服務做了些什麼工作。
簡單說說harbor服務的消息處理函數
註冊消息處理函數(可以看出如果有消息過來,會調用mainloop來處理)
int harbor_init(struct harbor *h, struct skynet_context *ctx, const char * args)
skynet_callback(ctx, h, mainloop);
mainloop的處理:
static int mainloop(struct skynet_context * context, void * ud, int type, int session, uint32_t source, const void * msg, size_t sz)
struct harbor * h = ud;
switch (type) {
case PTYPE_SOCKET: { // 收到遠端 harbor 的消息
...
case PTYPE_HARBOR: { // 收到本地 slave 的命令
harbor_command(h, msg,sz,session,source);
default: { // 需要發送消息到遠端 harbor
// remote message out
const struct remote_message *rmsg = msg;
if (rmsg->destination.handle == 0) { // 如果數字地址爲0 即說明採用的是字符串地址
if (remote_send_name(h, source , rmsg->destination.name, type, session, rmsg->message, rmsg->sz)) {
return 0;
可以看到 harbor 服務的消息處理主要分爲三大類:
- 從網絡過來的數據包(PTYPE_SOCKET, 一般是兩個節點之間的harbor通信)
- 本地發送的請求(PTYPE_HARBOR, 一般是 cslave 服務的請求)
- 需要發送到另一個節點的消息(default, 這個一般是一個節點的服務要調用 skynet.send/skynet.call 發送消息到另外一個節點)
cmaster服務的工作
skynet.start(function()
-- 得到中心節點的地址
local master_addr = skynet.getenv "standalone"
skynet.error("master listen socket " .. tostring(master_addr))
-- 監聽中心節點
local fd = socket.listen(master_addr)
-- 調用 socket.start 正式開始監聽
socket.start(fd , function(id, addr)
-- 如果有遠端連接過來,會調用此函數,這裏是有 slave 連接過來了會調用此函數
skynet.error("connect from " .. addr .. " " .. id)
-- 啓動數據傳輸
socket.start(id)
-- 調用 handshake 在中心節點記錄下這個 slave,並通知其他slave說這個slave連接上來了,讓別的slave都去連接這個新的slave)
local ok, slave, slave_addr = pcall(handshake, id)
if ok then
-- 監控 slave 的協程,其實就是對處理 slave 發過來的消息
skynet.fork(monitor_slave, slave, slave_addr)
else
skynet.error(string.format("disconnect fd = %d, error = %s", id, slave))
socket.close(id)
end
end)
end)
從上面可以看到, cmaster 的主要工作爲:
- 監聽配置文件中 standalone 指定的地址,以便讓其他節點連接上來
- 如果有其他 slave 節點連接上來了,記錄下這個 slave,並且告訴其他的 slave 節點
- 調用 monitor_slave 函數接收並處理從 slave 節點過來的網路包
cslave服務的工作
skynet.start(function()
-- 得到中心節點的地址
local master_addr = skynet.getenv "master"
-- 得到 harbor id
local harbor_id = tonumber(skynet.getenv "harbor")
-- 得到本節點的地址
local slave_address = assert(skynet.getenv "address")
-- 監聽本節點
local slave_fd = socket.listen(slave_address)
skynet.error("slave connect to master " .. tostring(master_addr))
-- 連接 master 節點
local master_fd = assert(socket.open(master_addr), "Can't connect to master")
-- 註冊消息處理函數,用於處理本地請求
skynet.dispatch("lua", function (_,_,command,...)
local f = assert(harbor[command])
f(master_fd, ...)
end)
skynet.dispatch("text", monitor_harbor(master_fd))
-- 啓動 harbor 服務
harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))
-- 發送一個 "H" 消息,告訴master:hi, gay!我連接上來了,並且告訴 master:harbor id 和 節點地址
local hs_message = pack_package("H", harbor_id, slave_address)
socket.write(master_fd, hs_message)
-- 遠端節點會告訴你有多少個節點已經連接上來了,待會他們會來連接你的
local t, n = read_package(master_fd)
assert(t == "W" and type(n) == "number", "slave shakehand failed")
skynet.error(string.format("Waiting for %d harbors", n))
-- 開闢一個協程,用於處理中心節點過來的網絡包
skynet.fork(monitor_master, master_fd)
if n > 0 then
local co = coroutine.running()
socket.start(slave_fd, function(fd, addr)
skynet.error(string.format("New connection (fd = %d, %s)",fd, addr))
-- 與已連接的老前輩slave 一一建立連接
if pcall(accept_slave,fd) then
local s = 0
for k,v in pairs(slaves) do
s = s + 1
end
if s >= n then
skynet.wakeup(co)
end
end
end)
skynet.wait()
end
socket.close(slave_fd)
skynet.error("Shakehand ready")
skynet.fork(ready)
end)
從上面代碼看到,slave 還比 master 節點要複雜?是的。master節點只需要協調slave節點的工作即可了。
slave節點不僅要連接master,還要連接其他多個slave,還要與harbor服務通信,還要處理本節點其他服務的請求,詳細工作表述如下:
- 監聽本節點的地址
- 連接master節點並註冊"lua" "text"類型的消息處理函數,並啓動 harbor 服務,其中monitor_harbor是用來處理本地harbor服務發來的消息的
- 告訴 master我連接上來了,並告訴 master 我是誰(以便master告訴其他的slave,讓其他的slave來連接(通過向老的slave發送"C")),master收到後告訴你有多少個節點已連接了
- 開闢一個協程:monitor_master 來處理中新節點的消息
- 老的slave收到master的"C"的網路包(在 monitor_master 函數中收到),調用 connect_slave 函數去連接新上來的 slave
- 還是在 connect_slave 函數中,老的slave連接新的slave成功的後,老的slave調用
socket.abandon(fd)
後調用skynet.send(harbor_service, "harbor", string.format("S %d %d",fd,slave_id))
給harbor服務發送一個 "S" - 老的slave的harbor服務收到"S"後,調用
skynet_socket_start(h->ctx, fd);
(對應上面的socket.abandon(fd)
),並調用 handshake 函數將自己的 harbor id 發給對端新的 slave - 新的 slave 這時還在執行 accept_slave 函數,收取到老的slave的 harbor id,然後調用
socket.abandon(fd)
並調用skynet.send(harbor_service, "harbor", string.format("A %d %d", fd, id))
給 harbor 服務發送一個"A xxx xxx" - harbor 收到 "A xx xx" 後("harbor"類型),調用
skynet_socket_start(h->ctx, fd);
(對應上一步的socket.abandon(fd)
),並調用 handshake 函數將自己的 harbor id 發給老的slave, - 需要注意的是:這時候兩端的harbor收到 "A" 對方的 "S"後,主動連接的一方的
slave->status = STATUS_HANDSHAKE;
,被動連接的一方slave->status = STATUS_HEADER;
,harbor服務會執行到 push_socket_data 中去,這時候連接建立完成。
到這裏我們可以得出一個結論:master與slave網絡通信處理一直是在各自的cmaster/cslave服務中,slave與slave的網絡通知在連接準備階段是在cslave中處理,在連接準備完成後,slave與slave的交互全部直接通過各自節點的harbor服務
多節點字符串地址的註冊
skynet爲服務註冊一個字符串地址,主要接口有兩個:skynet.register
、skynet.name
,這兩個接口都會調用一個共同的函數:globalname 可以看到,globalname首先判斷這個字符串是不是 '.' 開頭的,如果是,就註冊一個僅僅本節點可見的字符串地址,如果不是,則調用harbor.globalname(name, handle)
註冊一個全skynet網絡可見的字符串地址。
harbor.globalname(name, handle)
skynet.send(".cslave", "lua", "REGISTER", name, handle)
function harbor.REGISTER(fd, name, handle)
globalname[name] = handle -- 在 slave 服務中緩存住這個全節點有效的名字
response_name(name) -- 檢查是否有此名字查詢的請求阻塞在這裏,如果有:返回
socket.write(fd, pack_package("R", name, handle))--發消息給master說:自己要註冊這個名字,然後由 master 將此請求轉發給所有 slave
skynet.redirect(harbor_service, handle, "harbor", 0, "N " .. name) -- 發消息給 harbor 服務說:我註冊這個名字,以便被查找
master服務收到"R"的處理如下(向所有的 slave 節點廣播 'N' 命令):
local function dispatch_slave(fd)
local t, name, address = read_package(fd)
if t == 'R' then -- 註冊全局名字
-- register name
assert(type(address)=="number", "Invalid request")
if not global_name[name] then
global_name[name] = address
end
local message = pack_package("N", name, address)
for k,v in pairs(slave_node) do
socket.write(v.fd, message) -- 向所有的 slave 節點廣播 'N' 命令
end
cslave服務收到遠端服務的"N "的處理如下(緩存住這個地址,然後向harbor服務發送一個'N'):
elseif t == 'N' then -- 收到master的從另外 slave 過來的註冊新名字的通知
globalname[id_name] = address -- 緩存住全局名字
response_name(id_name) -- 用於此名字查詢的被阻塞請求結果的返回
if connect_queue == nil then -- 如果已經準備好了,就給harbor服務發消息,讓harbor服務記錄下這個地址
skynet.redirect(harbor_service, address, "harbor", 0, "N " .. id_name)
end
harbor服務收到本地服務的"N "的處理如下:
case 'N' : { // 新命名全局名字
if (s <=0 || s>= GLOBALNAME_LENGTH) { -- 不能超過16個字符
skynet_error(h->ctx, "Invalid global name %s", name);
return;
}
update_name(h, rn.name, rn.handle);
struct keyvalue * node = hash_search(h->map, name);
if (node == NULL) {
node = hash_insert(h->map, name);
如果這個名字不存在,就會調用 hash_insert 在harbor服務裏將這個名字儲存起來。
上面幾個流程的總結如下:
- 某服務調起 harbor.REGISTER 請求,請求發給 slave 服務, slave 本身緩存住這個請求在 globalname
- 寫一個 'R' 的命令給 master , 並且寫一個 'N' 命令給此節點的 harbor 服務
- master 收到這個 'R' 命令, 緩存住名字在 global_name 中, 並且向所有的 slave 節點廣播一個 'N' 命令
- 其他的 slave 節點的 harbor 服務會收到 'N' 命令,並向 harbor 服務寫一個 'N'命令
如果A節點的A服務發起了一個註冊名字的請求,那麼等上面的流程走完以後,有以下服務知道A節點A服務的地址
- A節點的 slave 服務
- master 服務
- A節點的 harbor 服務
- 其他節點的 harbor 服務
查詢一個全局字符串地址
查詢一個全局的字符串地址的接口爲:harbor.queryname
function harbor.queryname(name)
return skynet.call(".cslave", "lua", "QUERYNAME", name)
function harbor.QUERYNAME(fd, name)
if name:byte() == 46 then -- "." , local name 如果是本節點的服務名字,就直接返回地址
skynet.ret(skynet.pack(skynet.localname(name)))
local result = globalname[name] -- 如果已經緩存過(是此節點的服務),也直接返回
if result then
skynet.ret(skynet.pack(result))
return
end
local queue = queryname[name]
if queue == nil then -- 如果爲空 說明此名字還沒查詢過
socket.write(fd, pack_package("Q", name))
queue = { skynet.response() }
queryname[name] = queue
else -- 如果不爲空 說明此名字已經查詢過 但是由於某種原因還沒返回(還沒註冊、slave還沒連接上來) 將其加入隊列 等註冊上來後再返回
table.insert(queue, skynet.response())
end
可見查詢時會向 master 發送一個"Q"命令,slave本身會並創建一個閉包隊列,master節點收到'Q'的處理:
elseif t == 'Q' then
-- query name
local address = global_name[name]
if address then
socket.write(fd, pack_package("N", name, address))
slave收到"N"後(這裏主要看response_name):
elseif t == 'N' then -- 收到master的從另外 slave 過來的註冊新名字的通知
globalname[id_name] = address -- 緩存住全局名字
response_name(id_name) -- 用於此名字查詢的被阻塞請求結果的返回
local address = globalname[name]
if queryname[name] then
local tmp = queryname[name]
queryname[name] = nil
for _,resp in ipairs(tmp) do
resp(true, address)
end
end
if connect_queue == nil then -- 如果已經準備好了,就給harbor服務發消息,讓harbor服務記錄下這個地址
skynet.redirect(harbor_service, address, "harbor", 0, "N " .. id_name)
end
上面的操作是將請求時的閉包隊列一一取出來,發送喚醒這個閉包隊列,以便查詢的一方收到結果,至此此次查詢結束。查詢全局字符串地址的流程總結爲:
- 調起 harbor.QUERYNAME 請求, 發給 slave 服務
- 如果確實是一個非本節點的全局名字服務,會有兩種情況:
- 已經查詢過,直接返回;
- 還沒有查詢過,如果此查詢隊列爲空:發送一個 'Q' 命令給 master 服務 創建查詢隊列,如果已有查詢隊列:將其加入查詢隊列即可,master 服務收到 'Q' 後:看有沒有此名字的服務註冊上來過,如果有: 發送一個 'N' 命令給這個 slave, slave 收到這個 'N' 命令後 緩存主全局名字,並且給查詢的服務返回相應的消息 並且給 harbor 服務發一個 'N' 命令(讓harbor服務記錄下這個地址)
從skynet.send函數看多節點模式消息的發送
我們知道,skynet.send 與 skynet.call都是可以直接發送到不同節點的地址的,這是如何實現的呢?
function skynet.send(addr, typename, ...)
return c.send(addr, p.id, 0 , p.pack(...))
static int lsend(lua_State *L)
if (dest_string) { //如果是字符串地址
//skynet_sendname 最終還是會調用 skynet_send
session = skynet_sendname(context, 0, dest_string, type, session , msg, len);
} else { //如果是數字地址
session = skynet_send(context, 0, dest, type, session , msg, len);
}
先看數字地址的情況
session = skynet_send(context, 0, dest, type, session , msg, len);
int skynet_send(struct skynet_context * context, uint32_t source, uint32_t destination , int type, int session, void * data, size_t sz)
if (skynet_harbor_message_isremote(destination)) { //如果目的地址不是本節點的(通過地址的高八位來判斷)
skynet_harbor_send(rmsg, source, session);
rmsg->destination.handle = destination;
skynet_context_send(REMOTE, rmsg, sizeof(*rmsg) , source, type , session); // 發送消息到 harbor 服務
skynet_mq_push(ctx->queue, &smsg);
通過高八位判斷是不是非本節點的地址,如果非本節點,將消息壓入到harbor服務的消息隊列。harbor服務會收到處理這個消息:
default: { // 需要發送消息到遠端 harbor
// remote message out
const struct remote_message *rmsg = msg;
if (rmsg->destination.handle == 0) { // 如果數字地址爲0 即說明採用的是字符串地址
if (remote_send_name(h, source , rmsg->destination.name, type, session, rmsg->message, rmsg->sz)) {
return 0;
}
} else {
if (remote_send_handle(h, source , rmsg->destination.handle, type, session, rmsg->message, rmsg->sz)) {
return 0;
}
}
由於是數字地址,所以會調用remote_send_handle
(只考慮非本節點的)
if (remote_send_handle(h, source , rmsg->destination.handle, type, session, rmsg->message, rmsg->sz))
send_remote(context, s->fd, msg,sz,&cookie);
skynet_socket_send(ctx, fd, sendbuf, sz_header+4); // 在這裏發送一個 "D" 給本地管道,本地管道收到後 封裝成網絡包發出去
int64_t wsz = socket_server_send(SOCKET_SERVER, id, buffer, sz);
send_request(ss, &request, 'D', sizeof(request.u.send));
遠端harbor服務收到這個消息:
case PTYPE_SOCKET: { // 收到遠端 harbor 的消息
const struct skynet_socket_message * message = msg;
switch(message->type) {
case SKYNET_SOCKET_TYPE_DATA:
push_socket_data(h, message);
// go though
case STATUS_CONTENT: {
int need = s->length - s->read;
if (size < need) {
memcpy(s->recv_buffer + s->read, buffer, size);
s->read += size;
return;
}
memcpy(s->recv_buffer + s->read, buffer, need);
// 分發到對應的服務上去
forward_local_messsage(h, s->recv_buffer, s->length);
再看字符串地址的情況
session = skynet_sendname(context, 0, dest_string, type, session , msg, len);
copy_name(rmsg->destination.name, addr);
rmsg->destination.handle = 0;
skynet_harbor_send(rmsg, source, session); -- 會往harbor服務壓入一條消息(和前面的數字地址地址一樣)
harbor服務收到消息:
if (rmsg->destination.handle == 0) { // 如果數字地址爲0 即說明採用的是字符串地址
if (remote_send_name(h, source , rmsg->destination.name, type, session, rmsg->message, rmsg->sz)) {
if (node->value == 0) { // 如果harbor服務沒有緩存這個字符串地址(可能是註冊字符串地址的'N'命令還沒發過來)
if (node->queue == NULL) {
node->queue = new_queue();
}
struct remote_message_header header;
header.source = source;
header.destination = type << HANDLE_REMOTE_SHIFT;
header.session = (uint32_t)session;
push_queue(node->queue, (void *)msg, sz, &header); // 把這個消息加入到隊列中,等待後續處理(將來收到'Q'命令的響應會pop_queue,然後繼續將消息轉發出去)
char query[2+GLOBALNAME_LENGTH+1] = "Q ";
query[2+GLOBALNAME_LENGTH] = 0;
memcpy(query+2, name, GLOBALNAME_LENGTH);
skynet_send(h->ctx, 0, h->slave, PTYPE_TEXT, 0, query, strlen(query)); // 那麼發送一個"Q"給 cslave 服務
return 1;
} else {
return remote_send_handle(h, source, node->value, type, session, msg, sz);
可以看出harbor服務會根據 rmsg->destination.handle 是否爲0來區分是一個字符串地址還是數字地址。遠端harbor服務收到這個包,會根據 rmsg->destination 來找到自己節點的那個服務然後轉發給那個服務。
所以,發消息給對端另外節點的流程是:
看是字符串地址還是數字地址,如果是數字地址
1. 發送消息到harbor,harbor服務會給管道發送一個'D'的命令,管道收到這個'D'命令,將消息通過網絡發到對端的harbor服務 2. 對端harbor服務收到後,根據地址將消息壓入到對應服務的消息隊列
如果是字符串地址
1. 發送消息到harbor,harbor服務會給管道發送一個'D'的命令,管道收到這個'D'命令,將消息通過網絡發到對端的harbor服務 2. 根據`rmsg->destination.handle`檢測到是字符串地址,看本地緩存有沒有此字符串地址,如果沒有:首先將此次請求加入到隊列中,然後發送消息給本節點的cslave服務,本地的cslave收到後會將地址返回給harbor服務(或通過cmaster轉發回去),當重新收到這個地址時,從隊列中取出,將消息壓入到相應的服務中去。 3. 如果本地緩存有此字符串地址,直接將此字符串地址對應的數字地址取出,然後往對應的服務壓入消息
skynet.uniqueservice的實現
skynet.uniqueservice 函數的第一參數若爲true,那麼就會創建一個全skynet網絡都有效的服務。
function skynet.uniqueservice(global, ...) -- .service 爲 service_mgr
if global == true then
return assert(skynet.call(".service", "lua", "GLAUNCH", ...))
else
return assert(skynet.call(".service", "lua", "LAUNCH", global, ...))
end
end
先看global爲字符串的情況(不考慮snax)
return assert(skynet.call(".service", "lua", "LAUNCH", global, ...))
return waitfor(service_name, skynet.newservice, realname, subname, ...)
local function waitfor(name , func, ...)
local s = service[name]
if type(s) == "number" then -- 如果已經有了,就直接返回
return s
end
local co = coroutine.running()
if s == nil then
s = {}
service[name] = s
elseif type(s) == "string" then
error(s)
end
assert(type(s) == "table")
if not s.launch and func then -- 如果是首次調用,可以直接返回
s.launch = true
return request(name, func, ...)
local function request(name, func, ...) -- 這裏的 func 爲 skynet.newservice
local ok, handle = pcall(func, ...)
local s = service[name]
assert(type(s) == "table")
if ok then
service[name] = handle
else
service[name] = tostring(handle)
end
for _,v in ipairs(s) do -- 喚醒阻塞的協程
skynet.wakeup(v)
end
if ok then
return handle
end
table.insert(s, co) -- 後續的調用都需要阻塞在這裏
skynet.wait()
s = service[name]
if type(s) == "string" then
error(s)
end
assert(type(s) == "number")
return s
end
可以看出如果global不爲ture,則認爲它是一個字符串,如果是首次針對此字符串調用 skynet.uniqueservice ,那麼會調用 skynet.newservice 創建一個服務並返回地址
如果不是首次調用並且首次調用還沒有完全完成,那麼會將當前協程插入到一個協程隊列中,然後 skynet.wait 等待首次調用完成後喚醒它
如果不是首次調用並且首次調用已經完全完成了,那麼會直接返回一個地址(首次調用完成後創建的服務的地址)。
再看global爲true的情況(不考慮snax)
skynet.start中可以看到,會針對 standalone 不同註冊不同的消息處理函數
if skynet.getenv "standalone" then -- 主節點,單節點模式
skynet.register("SERVICE") -- 只有主節點中會註冊
register_global()
else -- 多節點模式中的非主節點
register_local()
end
這裏分三種情況,之前看 bootstrap 初始化時已經知道(注意在多節點模式中只有主節點中才會註冊一個"SERVICE"名字服務):
- 單節點模式,standalone = true
- 多節點模式中的主節點, standalone = "ip:port"
- 多節點模式中的非主節點, standalone = nil
只看多節點模式,首先是主節點中,如果要創建一個全skynet網絡有效的服務。
return assert(skynet.call(".service", "lua", "GLAUNCH", ...))
function cmd.GLAUNCH(name, ...) --local function register_global()
return cmd.LAUNCH(global_name, ...)
後面就和創建本地服務一樣,可以看出,在主節點中,就只是在主節點中創建了一個本地服務。
再看非主節點:
return assert(skynet.call(".service", "lua", "GLAUNCH", ...))
function cmd.GLAUNCH(...) -- local function register_local()
return waitfor_remote("LAUNCH", ...)
return waitfor(local_name, skynet.call, "SERVICE", "lua", cmd, global_name, ...)
-- 注意此時的 func變爲 skynet.call 了, cmd爲 "LAUNCH"
可以看到會向 "SERVICE" 服務發送一個 "LAUNCH" 消息,然後再看主節點中的 service_mgr 服務收到這個消息怎麼處理的:
function cmd.LAUNCH(service_name, subname, ...)
return waitfor(service_name, skynet.newservice, realname, subname, ...)
可以看到會在本地創建一個服務,創建完成後會返回給遠端的非主節點的 service_mgr 服務,並儲存這個服務的地址,以便後續調用
順便一提 skynet.queryservice 對應 skynet.uniqueservice ,如果第一個參數爲true,那麼會阻塞的等待某個服務創建好才返回(當然服務如果已經存在就直接返回了),這沒啥好說的,都體現在 waitfor 函數裏面了。
master/slave模式的優劣
在我個人看來,master/slave最大的缺點在於不能很好的處理某個節點異常斷開的情況。
但是相比於cluster,它可以依靠一條socket連接便可以在雙方自由通信。
所以"官方"建議的是在不跨機房的機器中使用master/slave,在不同機房中或者跨互聯網網絡中使用cluster。(我覺得即便是不跨機房中,也要考慮異常斷開情況,當然skynet有提供監控這種異常的手段,但是不可避免的還有一些額外工作要做。)
Permanent link of this article:http://nulls.cc/post/skynet_srccode_analysis08_master_slave