這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
說到rpc讓我想起了剛畢業面試的時候,被問到是否瞭解rpc?我記得當時我的回答是“課本上學過rpc,只知道是遠端程序呼叫,但沒有用過,具體也不知道是什麼”。的確,大學中介軟體這門課程裡有講到rpc,裡面還引入了一個非常難理解的概念——“樁”,英文應該叫”stub”。現在的rpc實現裡,stub這個概念好像都沒見到了,應該都是叫”method”。
實現一個rpc伺服器很難嗎?rpc伺服器也就是在tcp伺服器的基礎上加上自訂的rpc協議而已。一個rpc協議裡,主要有個3個非常重要的資訊。
- 調用的遠程method名字,一般就是一個函數名
- call參數,也就是發送給伺服器的資料
- 用戶端產生的調用請求seq
除了最後一點,其他兩點顯然就是組成一個普通的函數調用而已,這也就是遠端程序呼叫了。最後一點的seq只是rpc內部實現的一個關鍵點而已,也許還有其他的實現方式,而不是依賴seq來保證單串連的並發請求。
如何用C語言實現rpc伺服器?
用C語言來實現rpc伺服器,先假設我們從socket裡讀取到了一個來自於用戶端的call請求,這個call請求裡面封裝了上面提到的3點資訊。
執行這個call,最重要的就是——“從call裡取出method,也就是一個函數名(字串),然後要通過這個函數名去執行對應的函數”。
C語言由於沒有反射機制,於是不能通過函數的名字去調對應的函數;因此,可以使用一張hash表儲存所有遠程函數的名字到函數的對應關係。這樣就可以通過method尋找一次hash表得到真正的函數,接下來就可以執行函數了,函數的執行結果當然就是作為響應返回給用戶端。
當然,這些需要被調用執行的函數都是在伺服器初始化的時候事先註冊到這張hash表中的。到這裡,感覺一切都結束了。其實,還有參數(請求資料)和傳回值(應答資料),這部分主要是涉及到序列化演算法,留到下次的“反射和序列化”再聊了。
使用Go RPC架構寫一個簡單伺服器
定義服務
type EchoServer boolfunc (s *EchoServer) Echo(req *string, res *string) error {res = reqreturn nil}
註冊服務
echo := new(EchoServer)if err := rpc.Register(echo); err != nil {return err}// 下面基本是固定代碼,建立一個tcp伺服器var listener *net.TCPListenerif tcpAddr, err := net.ResolveTCPAddr("tcp", addr); err != nil {return err} else {if listener, err = net.ListenTCP("tcp", tcpAddr); err != nil {return err}}for {conn, err := listener.Accept()if err != nil {sc.LOG.Error(err)continue}// 伺服器走rpc架構的入口,也就是在一個tcp串連上採用rpc協議來處理請求和應答。go rpc.ServeConn(conn)}return nil
這樣,就實現了一個簡單的echo伺服器,功能邏輯都在自訂的EchoServer裡面實現。接下來只需要將EchoServer的一個執行個體對象註冊到rpc架構中,用戶端就可以直接調用EchoServer對象中的方法了。
Go rpc伺服器的內部實現在思路上和前文提到的C語言實現基本是一樣的。細心的人可能注意到了,這裡註冊的是EchoServer對象,而不是EchoSever對象的方法,然後前文的C語言實現中註冊的直接是函數,那麼rpc伺服器如何能夠根據method
去調用對應的方法
呢?Go語言在這裡其實採用反射的手段,雖然表面上是註冊的EchoServer對象,實際卻是通過反射取得了EchoServer的所有方法,然後採用了map表儲存了method到方法的映射,這就回到了前文C語言的實現思路中去了。如果沒有反射的支援,就只能一個一個的方法全部註冊一遍了,並且程式碼群組織上也不夠優雅。
編碼解碼器
rpc用戶端有一個編碼解碼器定義了如何發送請求和讀取應答,那麼伺服器端必然有一個編碼解碼器定義了如何讀取請求和發送應答,剛好是一個相反的過程,這也就是序列化和還原序列化的一個用處。
type ServerCodec interface {ReadRequestHeader(*Request) errorReadRequestBody(interface{}) errorWriteResponse(*Response, interface{}) errorClose() error} 和用戶端一樣,你只需要實現ServerCodec這個介面,就可以自訂服務端的編碼解碼器,實現自訂的rpc協議了。 Go rpc伺服器端和用戶端都是預設使用的Gob序列化協議資料。Go裡面為什麼採用自實現的Gob,而不是Google的protobuf,這個也留到下次的*"反射和序列化"*聊吧。
注意,ServerCodec介面中的ReadRequestBody(interface{}) error
方法主要是用來讀取用戶端call請求的參數資料,也就是將socket中讀出來的資料包解碼到interface{}
所指向的具體對象中去,這裡的interface{}
可以理解為C語言中的void *
。你一定注意到了,不同的rpc服務定義的參數類型完全不同,並且在rpc架構內部都採用了interface{}
來適配,那麼架構內部如何知道讀取的socket資料要解碼到什麼具體類型中去呢?這裡又是涉及到了反射,因為有了反射就可以從interface{}
得到具體的類型。
接下來真得好好的說說“反射和序列化/還原序列化”了。
幾個內部細節
- 伺服器在發送應答的時候,同樣採用了一把鎖來保證原子性寫入socket
- 請求/應答結構(ServerCodec介面中出現的
Request/Response
)採用鏈表實現一個pool,需要一個Requet/Response的時候都從free list中擷取,不再頻繁的分配。這一點只是一個最佳化。
- Go的rpc除了直接跑在tcp伺服器上,還可以跑在更高層一點的http伺服器上。
- Go的rpc調用也提供了json序列化。
準備改變一下以前寫技術文章老是大段大段代碼的風格,試試小代碼能不能將自己的想法交代清楚,哈哈。這一篇就先到此為止了。
下一篇就看一下Go RPC架構的效能,做一個benchmark比較下。