go rpc 源碼分析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

1.概述

go 源碼中帶了rpc架構,以相對精簡的當時方式實現了rpc功能,目前源碼中的rpc官方已經宣布不再添加新功能,並推薦使用grpc.
作為go標準庫中rpc架構,還是有很多地方值得借鑒及學習,這裡將從源碼角度分析go原生rpc架構,以及分享一些在使用過程中遇到的坑.

2.server端

server端主要分為兩個步驟,首先進行方法註冊,通過反射處理將方法取出,並存到map中.然後是網路調用,主要是監聽連接埠,讀取資料包,解碼請求
調用反射處理後的方法,將傳回值編碼,返回給用戶端.

2.1 方法註冊

2.1.1 Register
// Register publishes the receiver's methods in the DefaultServer.func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) }// RegisterName is like Register but uses the provided name for the type// instead of the receiver's concrete type.func RegisterName(name string, rcvr interface{}) error {    return DefaultServer.RegisterName(name, rcvr)}

如上,方法註冊的入口函數有兩個,分別為Register以及RegisterName,這裡interface{}通常是帶方法的對象.如果想要自訂方法的接收對象,則可以使用RegisterName.

2.1.2 反射處理過程
type methodType struct {    sync.Mutex // protects counters    method     reflect.Method    //反射後的函數    ArgType    reflect.Type      //請求參數的反射值    ReplyType  reflect.Type      //返回參數的反射值    numCalls   uint              //調用次數}type service struct {    name   string                 // 服務名,這裡通常為register時的對象名或自訂對象名    rcvr   reflect.Value          // 服務的接收者的反射值    typ    reflect.Type           // 接收者的類型    method map[string]*methodType // 對象的所有方法的反射結果.}

反射處理過程,其實就是將對象以及對象的方法,通過反射產生上面的結構,如註冊Arith.Multiply(xx,xx) error 這樣的對象時,產生的結構為 map["Arith"]service, service 中ethod為 map["Multiply"]methodType.

幾個關鍵代碼如下:

產生service對象

func (server *Server) register(rcvr interface{}, name string, useName bool) error {    //產生service    s := new(service)    s.typ = reflect.TypeOf(rcvr)    s.rcvr = reflect.ValueOf(rcvr)    sname := reflect.Indirect(s.rcvr).Type().Name()     ....    s.name = sname    // 通過suitableMethods將對象的方法轉換成map[string]*methodType結構    s.method = suitableMethods(s.typ, true)        ....    //service儲存為索引值對    if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {        return errors.New("rpc: service already defined: " + sname)    }    return nil}

產生 map[string] *methodType

func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType {    methods := make(map[string]*methodType)    //通過反射,遍曆所有的方法    for m := 0; m < typ.NumMethod(); m++ {        method := typ.Method(m)        mtype := method.Type        mname := method.Name        // Method must be exported.        if method.PkgPath != "" {            continue        }        // Method needs three ins: receiver, *args, *reply.        if mtype.NumIn() != 3 {            if reportErr {                log.Println("method", mname, "has wrong number of ins:", mtype.NumIn())            }            continue        }        //取出請求參數類型        argType := mtype.In(1)        ...        // 取出響應參數類型,響應參數必須為指標        replyType := mtype.In(2)        if replyType.Kind() != reflect.Ptr {            if reportErr {                log.Println("method", mname, "reply type not a pointer:", replyType)            }            continue        }        ...        // 去除函數的傳回值,函數的傳回值必須為error.        if returnType := mtype.Out(0); returnType != typeOfError {            if reportErr {                log.Println("method", mname, "returns", returnType.String(), "not error")            }            continue        }                //將方法儲存成key-value        methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}    }    return methods}

2.2 網路調用

// Request 每次rpc調用的請求的頭部分type Request struct {    ServiceMethod string   // 格式為: "Service.Method"    Seq           uint64   // 用戶端產生的序號    next          *Request // server端保持的鏈表}// Response 每次rpc調用的響應的頭部分type Response struct {    ServiceMethod string    // 對應請求部分的 ServiceMethod    Seq           uint64    // 對應請求部分的 Seq    Error         string    // 錯誤    next          *Response // server端保持的鏈表}

如上,網路調用主要用到上面的兩個結構體,分別是請求參數以及返回參數,通過轉碼器(gob/json)實現二進位到結構體的相互轉換.主要涉及到下面幾個步驟:

關鍵代碼如下:
取出請求,並得到相應函數的調用參數

func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) {    // Grab the request header.    req = server.getRequest()    //編碼器讀取產生請求    err = codec.ReadRequestHeader(req)    if err != nil {        //錯誤處理        ...        return    }    keepReading = true    //取出服務名以及方法名    dot := strings.LastIndex(req.ServiceMethod, ".")    if dot < 0 {        err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)        return    }    serviceName := req.ServiceMethod[:dot]    methodName := req.ServiceMethod[dot+1:]    //從註冊時產生的map中查詢出相應的方法的結構    svci, ok := server.serviceMap.Load(serviceName)    if !ok {        err = errors.New("rpc: can't find service " + req.ServiceMethod)        return    }    svc = svci.(*service)    //擷取出方法的類型    mtype = svc.method[methodName]    if mtype == nil {        err = errors.New("rpc: can't find method " + req.ServiceMethod)    }

迴圈處理,不斷讀取連結上的位元組流,解密出請求,調用方法,編碼響應,回寫到用戶端.

func (server *Server) ServeCodec(codec ServerCodec) {    sending := new(sync.Mutex)    for {        //讀取請求        service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)        if err != nil {            ...        }        //調用        go service.call(server, sending, mtype, req, argv, replyv, codec)    }    codec.Close()}

通過參數進行函數調用

func (s *service) call(server *Server, sending *sync.Mutex, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {    mtype.Lock()    mtype.numCalls++    mtype.Unlock()    function := mtype.method.Func    // 通過反射進行函數調用    returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})    // 傳回值是不為空白時,則取出錯誤的string    errInter := returnValues[0].Interface()    errmsg := ""    if errInter != nil {        errmsg = errInter.(error).Error()    }        //發送相應,並釋放請求結構    server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)    server.freeRequest(req)}

3.client端

// 非同步呼叫func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {}// 同步調用func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {}
// Call represents an active RPC.type Call struct {    ServiceMethod string      // 服務名及方法名 格式:服務.方法    Args          interface{} // 函數的請求參數 (*struct).    Reply         interface{} // 函數的響應參數 (*struct).    Error         error       // 方法完成後 error的狀態.    Done          chan *Call  // 方法調用結束後的channel.}

client端部分則相對要簡單很多,主要提供Call以及Go兩個方法,分別表示同步調用以及非同步呼叫,但其實同步調用底層實現其實也是非同步呼叫,調用時主要用到了Call結構,相關解釋如上.

3.1 主要流程

3.2 關鍵代碼

發送請求部分代碼,每次send一次請求,均產生一個call對象,並使用seq作為key儲存在map中,服務端返回時從map取出call,進行相應處理.

func (client *Client) send(call *Call) {    //請求層級的鎖    client.reqMutex.Lock()    defer client.reqMutex.Unlock()    // Register this call.    client.mutex.Lock()    if client.shutdown || client.closing {        call.Error = ErrShutdown        client.mutex.Unlock()        call.done()        return    }    //產生seq,每次調用均產生唯一的seq,在服務端相應後會通過該值進行匹配    seq := client.seq    client.seq++    client.pending[seq] = call    client.mutex.Unlock()    // 請求並發送請求    client.request.Seq = seq    client.request.ServiceMethod = call.ServiceMethod    err := client.codec.WriteRequest(&client.request, call.Args)    if err != nil {        //發送請求錯誤時,將map中call對象刪除.        client.mutex.Lock()        call = client.pending[seq]        delete(client.pending, seq)        client.mutex.Unlock()        if call != nil {            call.Error = err            call.done()        }    }}

接收響應部分的代碼,這裡是一個for迴圈,不斷讀取tcp上的流,並解碼成Response對象以及方法的Reply對象.

func (client *Client) input() {    var err error    var response Response    for err == nil {        response = Response{}        err = client.codec.ReadResponseHeader(&response)        if err != nil {            break        }        //通過response中的 Seq擷取call對象        seq := response.Seq        client.mutex.Lock()        call := client.pending[seq]        delete(client.pending, seq)        client.mutex.Unlock()        switch {        case call == nil:            err = client.codec.ReadResponseBody(nil)            if err != nil {                err = errors.New("reading error body: " + err.Error())            }        case response.Error != "":            //服務端返回錯誤,直接將錯誤返回            call.Error = ServerError(response.Error)            err = client.codec.ReadResponseBody(nil)            if err != nil {                err = errors.New("reading error body: " + err.Error())            }            call.done()        default:            //通過編碼器,將Resonse的body部分解碼成reply對象.            err = client.codec.ReadResponseBody(call.Reply)            if err != nil {                call.Error = errors.New("reading body " + err.Error())            }            call.done()        }    }    // 用戶端退出處理    client.reqMutex.Lock()    client.mutex.Lock()    client.shutdown = true    closing := client.closing    if err == io.EOF {        if closing {            err = ErrShutdown        } else {            err = io.ErrUnexpectedEOF        }    }    for _, call := range client.pending {        call.Error = err        call.done()    }    client.mutex.Unlock()    client.reqMutex.Unlock()    if debugLog && err != io.EOF && !closing {        log.Println("rpc: client protocol error:", err)    }}

4.一些坑

  • 同步調用無法逾時

由於原生rpc只提供兩個方法,同步的Call以及非同步Go,同步的Call服務端不返回則會一直阻塞,這裡如果存在大量的不返回,會導致協程一直無法釋放.

  • 非同步呼叫逾時後會記憶體流失

基於非同步呼叫加channel實現逾時功能也會存在泄漏問題,原因是client的請求會存在map結構中,Go函數退出並不會清理map的內容,因此如果server端不返回的話,map中的請求會一直儲存,從而導致記憶體流失.

5. 總結

總的來說,go原生rpc算是個基礎版本的rpc,代碼精簡,可擴充性高,但是只是實現了rpc最基本的網路通訊,像逾時熔斷,連結管理(保活與重連),服務註冊發現,還是欠缺的,因此還是達不到生產環境開箱即用,不過git就有一個基於rpc的功能增強版本,叫rpcx,支援了大部分主流rpc的特性.

6. 參考

rpc https://golang.org/pkg/net/rpc/

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.