RPC

前面幾個小節我們介紹了如何基於 Socket 和 HTTP 來編寫網路應用,透過學習我們了解了 Socket 和 HTTP 採用的是類似"資訊交換"模式,即客戶端傳送一條資訊到伺服器端,然後(一般來說)伺服器端都會回傳一定的資訊以表示回應。客戶端和伺服器端之間約定了互動資訊的格式,以便雙方都能夠解析互動所產生的資訊。但是很多獨立的應用並沒有採用這種模式,而是採用類似常規的函式呼叫的方式來完成想要的功能。

RPC 就是想實現函式呼叫模式的網路化。客戶端就像呼叫本地函式一樣,然後客戶端把這些參數打套件之後透過網路傳遞到伺服器端,伺服器端解套件到處理過程中執行,然後執行的結果反饋給客戶端。

RPC(Remote Procedure Call Protocol)——遠端過程呼叫協議,是一種透過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。它假定某些傳輸協議的存在,如 TCP 或 UDP,以便為通訊程式之間攜帶資訊資料。透過它可以使函式呼叫模式網路化。在 OSI 網路通訊模型中,RPC 跨越了傳輸層和應用層。RPC 使得開發包括網路分散式多程式在內的應用程式更加容易。

RPC 工作原理

圖 8.8 RPC 工作流程圖

執行時,一次客戶端對伺服器的 RPC 呼叫,其內部操作大致有如下十步:

  • 1.呼叫客戶端控制代碼;執行傳送參數

  • 2.呼叫本地系統核心傳送網路訊息

  • 3.訊息傳送到遠端主機

  • 4.伺服器控制代碼得到訊息並取得參數

  • 5.執行遠端過程

  • 6.執行的過程將結果回傳伺服器控制代碼

  • 7.伺服器控制代碼回傳結果,呼叫遠端系統核心

  • 8.訊息傳回本地主機

  • 9.客戶控制代碼由核心接收訊息

  • 10.客戶接收控制代碼回傳的資料

Go RPC

Go 標準套件中已經提供了對 RPC 的支援,而且支援三個級別的 RPC:TCP、HTTP、JSONRPC。但 Go 的 RPC 套件是獨一無二的 RPC,它和傳統的 RPC 系統不同,它只支援 Go 開發的伺服器與客戶端之間的互動,因為在內部,它們採用了 Gob 來編碼。

Go RPC 的函式只有符合下面的條件才能被遠端存取,不然會被忽略,詳細的要求如下:

  • 函式必須是匯出的(首字母大寫)

  • 必須有兩個匯出型別的參數,

  • 第一個參數是接收的參數,第二個參數是回傳給客戶端的參數,第二個參數必須是指標型別的

  • 函式還要有一個回傳值 error

舉個例子,正確的 RPC 函式格式如下:

func (t *T) MethodName(argType T1, replyType *T2) error

T、T1 和 T2 型別必須能被encoding/gob套件編解碼。

任何的 RPC 都需要透過網路來傳遞資料,Go RPC 可以利用 HTTP 和 TCP 來傳遞資料,利用 HTTP 的好處是可以直接複用net/http裡面的一些函式。詳細的例子請看下面的實現

HTTP RPC

http 的伺服器端程式碼實現如下:

package main

import (
    "errors"
    "fmt"
    "net/http"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)
    rpc.HandleHTTP()

    err := http.ListenAndServe(":1234", nil)
    if err != nil {
        fmt.Println(err.Error())
    }
}

透過上面的例子可以看到,我們註冊了一個 Arith 的 RPC 服務,然後透過rpc.HandleHTTP函式把該服務註冊到了 HTTP 協議上,然後我們就可以利用 http 的方式來傳遞資料了。

請看下面的客戶端程式碼:

package main

import (
    "fmt"
    "log"
    "net/rpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server")
        os.Exit(1)
    }
    serverAddress := os.Args[1]

    client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

我們把上面的伺服器端和客戶端的程式碼分別編譯,然後先把伺服器端開啟,然後開啟客戶端,輸入程式碼,就會輸出如下資訊:

$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1

透過上面的呼叫可以看到參數和回傳值是我們定義的 struct 型別,在伺服器端我們把它們當做呼叫函式的參數的型別,在客戶端作為client.Call的第 2,3 兩個參數的型別。客戶端最重要的就是這個 Call 函式,它有 3 個參數,第 1 個要呼叫的函式的名字,第 2 個是要傳遞的參數,第 3 個要回傳的參數(注意是指標型別),透過上面的程式碼例子我們可以發現,使用 Go 的 RPC 實現相當的簡單,方便。

TCP RPC

上面我們實現了基於 HTTP 協議的 RPC,接下來我們要實現基於 TCP 協議的 RPC,伺服器端的實現程式碼如下所示:

package main

import (
    "errors"
    "fmt"
    "net"
    "net/rpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        rpc.ServeConn(conn)
    }

}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

上面這個程式碼和 http 的伺服器相比,不同在於 : 在此處我們採用了 TCP 協議,然後需要自己控制連線,當有客戶端連線上來後,我們需要把這個連線交給 rpc 來處理。

如果你留心了,你會發現這它是一個阻塞型的單使用者的程式,如果想要實現多併發,那麼可以使用 goroutine 來實現,我們前面在 socket 小節的時候已經介紹過如何處理 goroutine。 下面展現了 TCP 實現的 RPC 客戶端:

package main

import (
    "fmt"
    "log"
    "net/rpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server:port")
        os.Exit(1)
    }
    service := os.Args[1]

    client, err := rpc.Dial("tcp", service)
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

這個客戶端程式碼和 http 的客戶端程式碼對比,唯一的區別一個是 DialHTTP,一個是 Dial(tcp),其他處理一模一樣。

JSON RPC

JSON RPC 是資料編碼採用了 JSON,而不是 gob 編碼,其他和上面介紹的 RPC 概念一模一樣,下面我們來示範一下,如何使用 Go 提供的 json-rpc 標準套件,請看伺服器端程式碼的實現:

package main

import (
    "errors"
    "fmt"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        jsonrpc.ServeConn(conn)
    }

}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

透過範例我們可以看出 json-rpc 是基於 TCP 協議實現的,目前它還不支援 HTTP 方式。

請看客戶端的實現程式碼:

package main

import (
    "fmt"
    "log"
    "net/rpc/jsonrpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server:port")
        log.Fatal(1)
    }
    service := os.Args[1]

    client, err := jsonrpc.Dial("tcp", service)
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

總結

Go 已經提供了對 RPC 的良好支援,透過上面 HTTP、TCP、JSON RPC 的實現,我們就可以很方便的開發很多分散式的 Web 應用,我想作為讀者的你已經領會到這一點。但遺憾的是目前 Go 尚未提供對 SOAP RPC 的支援,欣慰的是現在已經有第三方的開源實現了。

Last updated