SocketChannel

請求迴應模式是和外部服務交互時所用到的最常用模式之一,通常的協議設計方式有兩種

1.每個請求包對應一個迴應包,由TCP協議保證時序,redis的協議就是一個典型,每個redis請求都必須有一個迴應,但不必收到迴應纔可以發送下一個請求.

2.發起每個請求時帶一個唯一session標識,在發送迴應時,帶上這個標識,這樣設計可以不要求每個請求都一定要有迴應,且不必遵循先提出的請求先回應的時序,MongoDB的通訊協議就是這樣設計的.


第一種模式,用skynet的Socket API很容易實現,只要在一個coroutine中讀寫一個socket就行,但是讀的過程是阻塞的,這回導致吞吐量下降,在前一個迴應沒有收到時,無法發送下一個請求.


第二種模式,需要用skynet.fork開啓一個新線程來收取回應包,並自行和請求對應起來,實現比較繁瑣.


所以,skynet提供了一個更高層的封裝:socket channel.

local sc = require "socketchannel"

local channel = sc.channel {
  host = "127.0.0.1",
  port = 3271,
}

這樣就可以創建一個channel對象出來,其中host可以是ip地址或者域名,port是端口號.


這時,如果要在模式1下工作.只要這樣

local resp = channel:request(req [, response[, padding]])

這裏,req是一個字符串,即請求包,response是一個function,用來收取回應寶,返回值是一個字符串,是由response函數返回的迴應包的內容(可以是任意類型),response函數需要定義成這個樣子

function response(sock)
  return true, sock:readline()
end
sock是由request方法傳入的一個對象,sock有兩個方法:read(self,sz)和readline(self,sep).read可以讀制定字節數;readline可以讀以seq分割(默認爲\n)的一個字符串(不包含分割符).


response 函數的第一個返回值需要時一個boolean,如果爲true表示協議解析正常;如果爲false表示協議出錯,這回導致連接斷開且讓request的調用者也獲得一個error.


在response函數內的任何異常以及sock:read或sock:readline讀取出錯,都一以error的形式拋給request的調用者.



如果協議模式是第2種情況,那麼你需要在channel創建時給出一個通用的response解析函數.

local channel = sc.channel {
  host = "127.0.0.1",
  port = 3271,
  response = dispatch,
}
這裏dispatch是一個解析迴應包的函數,和上面提到的模式1中的解析函數類似,但其返回值需要有三個,第一個是這個迴應寶的session,第二個是包是否解析正確(同模式1),第三個是迴應內容.


socket channel 就是依靠創建時是否提供response函數來決定工作在模式1還是模式2下.


在模式2下,request的參數有所變化,第2個參數不再是response函數,(它已經在創建時給出),而是一個session.這個session可以是任意類型,但需要和response函數返回的類型一直,socket channel 會幫你匹配session 而讓request返回正確的值.


channel:close() 可以關閉一個channel,通常你可以不必主動關閉它,gc會回收channel佔用的資源.


socket channel 在創建時,並不會立即建立連接,如果你什麼都不做,那麼連接建立會推遲到第一次request請求時,這種被動建立的連接過程會不斷的嘗試,即使第一次沒有連接上,也會重試.


可以用channel:connect(true)主動嘗試一個,如果失敗,拋出error,這裏參數true表示只嘗試一次,如果不填這個參數,則一直重試下去.


由於連接可能發生在任何request之前,(只要錢一次操作檢測到連接時斷開狀態就是重新發起連接),所以socket channel支持認證流程,允許在建立連接後,立刻做一些交互,如果開啓這個功能,需要在創建channel時,填寫一個auth函數,.和response函數一樣,會給他傳入一個sock對象,auth函數不需要返回值,如果認證失敗,在auth函數中拋出error即可.


由於對端有可能在任何時候斷開連接,所以任何一次request都有可能拋出error而失敗,socket channel 將在下一次 request時重新建立連接,注意:重連並不會重發過去發生的error的請求,連接斷開並不是隱藏在內部的,所以,如果有必要,你應該在請求失敗時,業務層重新提出請求.


socket channel 也可用於僅發包而不接受迴應,只需要在request調用時不填寫response即可.

channel:response則可以用來單向接受一個包.

channel:request(req)
local resp = channel:response(dispatch)

-- 等價於

local resp = channel:request(req, dispatch)

request還有第三個參數padding,這是用來將體積巨大的消息拆分成多個包發出用的(如果協議支持,),padding需要是一個table,裏面有若干字符串.如果提供了padding參數,socket channel 將連同req 以及padding數組裏的字符串,利用socket的低優先級通道發出,(使用socket.lwrite).


這種用法下的response函數,應該多返回一個padding值,即,對於模式1返回succ:boolean  data:string   padding:boolean  三個值,對於模式2,返回 session:number succ:boolean data:string  padding:boolean 四個值


padding 標明瞭後續是否還有該長消息的後續部分.


如果迴應消息是由對個短小的消息合成,channel:request 將返回一個table,裏面有所有短消息的內容,由調用者來連接這些短消息.


關於socket channel的具體用法除了閱讀lualib/socketchannel.lua (同時這也是理解socket模塊的好材料)的實現外,也可以閱讀lualib/redis.lua和lualib/mongo.lua 這兩個爲skynet編寫的數據庫driver.

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