Files
build-web-application-with-…/zh-tw/08.4.md
2019-02-26 01:40:54 +08:00

10 KiB
Raw Blame History

8.4 RPC

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

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

RPCRemote 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的支援而且支援三個級別的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的伺服器端程式碼實現如下


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的第23兩個引數的型別。客戶端最重要的就是這個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的支援欣慰的是現在已經有第三方的開源實現了。