Files
build-web-application-with-…/zh-tw/08.4.md
2019-06-22 23:41:28 +08:00

407 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 8.4 RPC
前面幾個小節我們介紹了如何基於 Socket 和 HTTP 來編寫網路應用,透過學習我們了解了 Socket 和 HTTP 採用的是類似"資訊交換"模式,即客戶端傳送一條資訊到伺服器端,然後(一般來說)伺服器端都會回傳一定的資訊以表示回應。客戶端和伺服器端之間約定了互動資訊的格式,以便雙方都能夠解析互動所產生的資訊。但是很多獨立的應用並沒有採用這種模式,而是採用類似常規的函式呼叫的方式來完成想要的功能。
RPC 就是想實現函式呼叫模式的網路化。客戶端就像呼叫本地函式一樣,然後客戶端把這些參數打套件之後透過網路傳遞到伺服器端,伺服器端解套件到處理過程中執行,然後執行的結果反饋給客戶端。
RPCRemote Procedure Call Protocol——遠端過程呼叫協議是一種透過網路從遠端計算機程式上請求服務而不需要了解底層網路技術的協議。它假定某些傳輸協議的存在如 TCP 或 UDP以便為通訊程式之間攜帶資訊資料。透過它可以使函式呼叫模式網路化。在 OSI 網路通訊模型中RPC 跨越了傳輸層和應用層。RPC 使得開發包括網路分散式多程式在內的應用程式更加容易。
## RPC 工作原理
![](images/8.4.rpc.png)
圖 8.8 RPC 工作流程圖
執行時,一次客戶端對伺服器的 RPC 呼叫,其內部操作大致有如下十步:
- 1.呼叫客戶端控制代碼;執行傳送參數
- 2.呼叫本地系統核心傳送網路訊息
- 3.訊息傳送到遠端主機
- 4.伺服器控制代碼得到訊息並取得參數
- 5.執行遠端過程
- 6.執行的過程將結果回傳伺服器控制代碼
- 7.伺服器控制代碼回傳結果,呼叫遠端系統核心
- 8.訊息傳回本地主機
- 9.客戶控制代碼由核心接收訊息
- 10.客戶接收控制代碼回傳的資料
## Go RPC
Go 標準套件中已經提供了對 RPC 的支援,而且支援三個級別的 RPCTCP、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 的伺服器端程式碼實現如下:
```Go
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 的方式來傳遞資料了。
請看下面的客戶端程式碼:
```Go
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)
}
```
我們把上面的伺服器端和客戶端的程式碼分別編譯,然後先把伺服器端開啟,然後開啟客戶端,輸入程式碼,就會輸出如下資訊:
```Go
$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1
```
透過上面的呼叫可以看到參數和回傳值是我們定義的 struct 型別,在伺服器端我們把它們當做呼叫函式的參數的型別,在客戶端作為`client.Call`的第 23 兩個參數的型別。客戶端最重要的就是這個 Call 函式,它有 3 個參數,第 1 個要呼叫的函式的名字,第 2 個是要傳遞的參數,第 3 個要回傳的參數(注意是指標型別),透過上面的程式碼例子我們可以發現,使用 Go 的 RPC 實現相當的簡單,方便。
### TCP RPC
上面我們實現了基於 HTTP 協議的 RPC接下來我們要實現基於 TCP 協議的 RPC伺服器端的實現程式碼如下所示
```Go
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 客戶端:
```Go
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 標準套件,請看伺服器端程式碼的實現:
```Go
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 方式。
請看客戶端的實現程式碼:
```Go
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 的支援,欣慰的是現在已經有第三方的開源實現了。
## links
* [目錄](<preface.md>)
* 上一節:[REST](<08.3.md>)
* 下一節:[小結](<08.5.md>)