最近公司在作分佈式相關的東西,須要用到RPC。之前對RPC不是很瞭解,網上也看了不少文章,發現看過以後並無加深個人理解,仍然雲裏霧裏,只知道是遠程過程調用,一個計算機上的程序能夠調用另外一個計算機上的服務,或一個進程調用另外一個進程提供的服務,僅此而已。也用了golang官方的net/rpc庫實現了這個目的,可是對net/rpc的底層不瞭解(只是會用),本着一顆「鑽牛角尖」的心,我決定深刻研究一下RPC的原理,並本身實現一個RPC,故有了這篇文章。git
維基百科對RPC是這樣解釋的:程序員
In distributed computing, a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction.github
我就不翻譯了,大致是說程序A調用程序B,A跟B不在一個地址空間(一般A跟B也不在同一臺電腦上),可是A調用B就跟調用本地的程序是同樣的,程序員無需對這個遠程的交互細節進行額外的編程。golang
這裏有兩個關鍵詞,我用黑體標出來了。 分佈式:代表了RPC的應用場景,通常是用在多臺計算機,而不是單臺計算機上。 在不一樣的地址空間:說明了至少有兩個進程,一個服務端進程,一個客戶端進程。服務端進程提供服務(暴露出某些接口),客戶端進程調用服務。固然測試的時候服務端和客戶端程序寫在一個文件裏面也行,在主線程裏面寫服務端程序,提供接口,而後新開一個線程寫客戶端程序,調用接口也是能夠的。編程
從RPC原理圖中咱們能夠看出,server端是服務提供方,client端是服務調用方。既然server端能夠提供服務,那麼它要先實現服務的註冊,只有註冊過的服務才能被client端調用。前文也說過,client端和server端通常不在一個進程內,甚至不在一個計算機內,那麼他們之間的通訊必然是經過網絡傳輸,這樣就涉及到了網絡傳輸協議,說的更直白的,就是如何將client端的數據(通常是要調用的服務名和相應的參數)安全傳輸到server端,而server端也能完整的接收到。在client端和server端,數據通常是以對象的形式存在,而對象是沒法進行網絡傳輸的,在網絡傳輸以前,咱們須要先把對象序列化成字節流,而後傳輸這些字節流,server端在接收到這些字節流以後,再反序列化獲得原始對象,這就是序列化與反序列化。總結一下,要實現一個RPC就必須解決這三個問題,即json
在講解具體的實現以前,咱們先看一下咱們的testrpc是如何使用的。 server.go安全
package main
import (
"log"
"net"
"testrpc"
)
type Args struct {
A, B int
}
type Arith int
func (t *Arith) Multiply(args Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func main() {
// 建立一個rpc server對象
newServer := testrpc.NewServer()
// 向rpc server對象註冊一個Arith對象,註冊後,client就能夠調用Arith的Multiply方法
arith := new(Arith)
newServer.Register(arith)
// 監聽本機的1234端口
l, e := net.Listen("tcp", "127.0.0.1:1234")
if e != nil {
log.Fatalf("net.Listen tcp :0: %v", e)
}
for {
// 阻塞直到從1234端口收到一個網絡鏈接
conn, e := l.Accept()
if e != nil {
log.Fatalf("l.Accept: %v", e)
}
//開始工做
go newServer.ServeConn(conn)
}
}
複製代碼
代碼比較簡單,也有註釋,這裏簡單說明一下流程:咱們先是new出來了一個rpc server對象,而後向這個server註冊了Arith對象,註冊後,client就可調用Arith暴露出來的全部方法,這裏只有Multiply。而後咱們監聽了本機的1234端口,而且在for循環中等待來自1234端口的鏈接,等來了一個鏈接咱們就調用ServeConn方法在一個新的goroutine中處理這個鏈接,而後咱們繼續等待新的鏈接,如此反覆。bash
client.go網絡
package main
import (
"log"
"net"
"os"
"testrpc"
)
type Args struct {
A, B int
}
func main() {
// 鏈接本機的1234端口,返回一個net.Conn對象
conn, err := net.Dial("tcp", "127.0.0.1:1234")
if err != nil {
log.Println(err.Error())
os.Exit(-1)
}
// main函數退出時關閉該網絡鏈接
defer conn.Close()
// 建立一個rpc client對象
client := testrpc.NewClient(conn)
// main函數退出時關閉該client
defer client.Close()
// 調用遠端Arith.Multiply函數
args := Args{7, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
log.Println(reply)
}
複製代碼
咱們先鏈接本機的1234端口(這個端口server.go在監聽),獲得一個net.Conn對象,而後用這個對象new出來了一個rpc client,而後再經過這個client調用服務端提供的方法Multiply,計算完後把結果存到reply中。代碼很簡單,就很少說了。app
這個用法參考了golang官方的net/rpc庫,有興趣的讀者也能夠去學習一下net/rpc的使用方法。
前面說過,server端要解決的問題有服務的註冊,也就是Register方法,那麼server端必需要可以存儲這些服務,因此server的定義能夠以下:
type Service struct {
Method reflect.Method
ArgType reflect.Type
ReplyType reflect.Type
}
type Server struct {
ServiceMap map[string]map[string]*Service
serviceLock sync.Mutex
}
複製代碼
一個Service對象就對應一個服務,一個服務包括方法、參數類型和返回值類型。Server有兩個屬性:ServiceMap和serviceLock,ServiceMap是一系列service的集合,之因此要以Map的形式是爲了方便查找,serviceLock是爲了保護ServiceMap,確保同一時刻只有一個goroutine可以寫ServiceMap。
func (server *Server) Register(obj interface{}) error {
server.serviceLock.Lock()
defer server.serviceLock.Unlock()
//經過obj獲得其各個方法,存儲在servicesMap中
tp := reflect.TypeOf(obj)
val := reflect.ValueOf(obj)
serviceName := reflect.Indirect(val).Type().Name()
if _, ok := server.ServiceMap[serviceName]; ok {
return errors.New(serviceName + " already registed.")
}
s := make(map[string]*Service)
numMethod := tp.NumMethod()
for m := 0; m < numMethod; m++ {
service := new(Service)
method := tp.Method(m)
mtype := method.Type
mname := method.Name
service.ArgType = mtype.In(1)
service.ReplyType = mtype.In(2)
service.Method = method
s[mname] = service
}
server.ServiceMap[serviceName] = s
server.ServerType = reflect.TypeOf(obj)
return nil
}
複製代碼
這裏把前面的調用Register的代碼放出來一塊兒看可能會更清楚一些。
type Arith int
func (t *Arith) Multiply(args Args, reply *int) error {
*reply = args.A * args.B
return nil
}
...
newServer := testrpc.NewServer()
newServer.Register(new(Arith))
...
複製代碼
Register的大概邏輯就是拿到obj(Register的參數)的各個暴露出來的方法(這裏只有一個Multiply),而後存到server的ServiceMap中。這裏主要用到了golang的reflect,若是對reflect不瞭解的話看Register代碼仍是比較吃力的。網上有不少講解reflect的文章,建議不瞭解reflect的讀者先去看看,這裏就不講了。註冊以後,ServiceMap大概是這個樣子
{
"Arith": {"Multiply":&{Method:Multiply, ArgType:main.Args, ReplyType:*int}}
}
複製代碼
testrpc的網絡傳輸是基於golang提供的net.Conn,這個net.Conn提供了兩個方法:Read和Write。Read表示從網絡鏈接中讀取數據,Write表示向網絡鏈接中寫數據。咱們就基於這兩個方法來實現咱們的網絡傳輸,代碼以下:
const (
EachReadBytes = 500
)
type Transfer struct {
conn net.Conn
}
func NewTransfer(conn net.Conn) *Transfer {
return &Transfer{conn: conn}
}
func (trans *Transfer) ReadData() ([]byte, error) {
finalData := make([]byte, 0)
for {
data := make([]byte, EachReadBytes)
i, err := trans.conn.Read(data)
if err != nil {
return nil, err
}
finalData = append(finalData, data[:i]...)
if i < EachReadBytes {
break
}
}
return finalData, nil
}
func (trans *Transfer) WriteData(data []byte) (int, error) {
num, err := trans.conn.Write(data)
return num, err
}
複製代碼
ReadData是從網絡鏈接中讀取數據,每次讀500字節(由EachReadBytes指定),直到讀完爲止,而後把讀到的數據返回;WriteData是向網絡鏈接中寫數據。
上節講到了網絡傳輸,咱們知道,傳輸的對象是字節流。序列化就是負責把對象變成字節流的,相反的,反序列化就是負責將字節流變成程序中的對象的。在網絡傳輸以前咱們要先進行序列化,testrpc採用了json作爲序列化方式,將來可能會加入其它的序列化方式,如gob、xml、protobuf等。咱們先來看下采用json作爲序列化的代碼:
type EdCode int
func (edcode EdCode) encode(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (edcode EdCode) decode(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
複製代碼
這裏採用了golang官方提供的json庫,代碼很簡單,就不解釋了。
以上就是Server端在拿到Client端請求後的處理過程,咱們把它封裝在了ServeConn方法裏。代碼比較長,就不在這裏貼了,有興趣的能夠去github裏面看。
func (client *Client) Call(methodName string, req interface{}, reply interface{}) error {
// 構造一個Request
request := NewRequest(methodName, req)
// encode
var edcode EdCode
data, err := edcode.encode(request)
if err != nil {
return err
}
// write
// 構造一個Transfer
trans := NewTransfer(client.conn)
_, err = trans.WriteData(data)
if err != nil {
log.Println(err.Error())
return err
}
// read
data2, err := trans.ReadData()
if err != nil {
log.Println(err.Error())
return err
}
// decode and assin to reply
edcode.decode(data2, reply)
// return
return nil
}
複製代碼
Client的定義很簡單,就一個表明網絡鏈接的conn,代碼以下:
type Client struct {
conn net.Conn
}
複製代碼
因爲我日常寫Python代碼寫得比較多,若是用Python來實現這個testrpc的話確實要更快。那麼爲何要使用golang呢?由於咱們公司最近作的一個項目是基於golang的net/rpc來實現的,工做之便,瞭解了net/rpc的使用,也稍微看了下net/rpc的底層代碼,猜出了其大概原理,因而就想本身也寫一個,就這樣。固然,之後有機會的話再用Python實現一遍。
本文主要講述了RPC的原理,以及實現了一個輕量級的RPC。這裏說一下我在調試的時候遇到的問題,因爲golang是強類型語言,那麼我必需要面對的問題是,怎麼在一個對象通過序列化和反序列化以後依然保持其原來的類型。好比一個Args類型的對象(讀者能夠翻到最前面看看Args是如何定義的)在序列化和反序列化以後變成了map[string]interface{}類型,用map[string]interface{}類型當作Args類型傳參是會報錯的,這部分感興趣的讀者能夠去看看代碼。讀者可能看過這篇文章沒什麼感受,看過一遍也就看過了,感受有收穫又感受沒收穫,我建議把代碼下載下來,而後在本身的電腦上跑一下,調試一下,我相信你會有很深的理解,不論是對RPC,仍是對golang的reflect等。好比說我,我之前對reflect掌握的不是很好,寫完這個testrpc以後基本掌握了reflect的使用。
代碼在github上,歡迎給個star和提交issue,也歡迎你提出本身的見解,好比這段代碼寫得很差,能夠這麼優化等等,均可以與我交流,多謝!
github地址:https://github.com/TanLian/testrpc
另也可關注下我我的的技術公衆號,期待一塊兒進步。