一、RPC編程
RPC(Remote Procedure Call,遠端程序呼叫)是一種通過網路從遠端電腦程式上請求服務,而不需要瞭解底層網路細節的應用程式通訊協定。RPC協議構建於TCP或UDP,或者是HTTP上。允許開發人員直接調用另一台伺服器上的程式,而開發人員無需另外的為這個調用過程編寫網路通訊相關代碼,使得開發網路分布式程式在內的應用程式更加容易
RPC採用用戶端-伺服器端的工作模式,請求程式就是一個用戶端,而服務提供者就是一個伺服器端。當執行一個遠端程序呼叫時,用戶端程式首先先發送一個帶有參數的調用資訊到服務端,然後等待服務端響應。在服務端,服務進程保持睡眠狀態直到用戶端的調用資訊到達。當一個調用資訊到達時,服務端獲得進程參數,計算出結果,並向用戶端發送應答資訊。然後等待下一個調用。
在Go中,標準庫提供的net/rpc包實現了RPC協議需要的相關細節,開發人員可以很方便的使用該包編寫RPC的服務端和用戶端程式。這使得用Go語言開發的多個進程之間的通訊變得非常簡單
net/rpc包允許PRC用戶端程式通過網路或者其他IO串連調用一個遠程對象的公開方法(該方法必須是外部可訪問即首字母大寫)。在PRC服務端,可將一個對象註冊為可訪問的服務,之後該對象的公開方法就能夠以遠端方式提供訪問。一個RPC服務端可以註冊多個不通類型的對象,但<font color="Brown">不允許註冊同一類型的多個對象。</font>
一個對象中只有滿足如下條件的方法,才能被PRC服務端設定為可供遠端存取
1.<font color="Brown">必須是在對象外部可公開調用的方法(首字母大寫)</font>
2.<font color="Brown">必須有兩個參數,且參數的類型都必須是包外部可以訪問的類型或者是Go內建支援的類型</font>
3.<font color="Brown">第二個參數必須是一個指標</font>
4.<font color="Brown">方法必須返回一個error類型的值</font>
用代碼錶示
func (t T*)MethodName(argType T1,replyType *T2)error
在上面這行代碼中,類型 T T1 T2 預設會使用Go內建的encoding/gob包進行編碼和解碼
改方法的第一個參數表示由PRC用戶端傳入的參數,第二個參數表示要返回給PRC用戶端的結果。改方法最後返回一個error類型
- RPC用戶端和伺服器端的使用
RPC服務端可以通過調用 ```rpc.ServerConn```處理單個串連請求。多數情況下,通過tcp或是http在某個網路地址上監聽然後再建立該服務是個不錯的選擇 在RPC用戶端,Go的net/rpc包提供了便利的```rpc.Dial()```和```rpc.DialHTTP()```方法來與指定的RPC服務建立串連。在建立串連之後,Go的net/rpc包允許我們使用通過或者非同步方式接受RPC服務端的結果。調用RPC用戶端的```Call()```方法則進行同步處理。這個時候用戶端程式按照順序執行。當調用RPC用戶端的```Go()```方法時,則進行非同步處理。用戶端無需等待服務端的結果即可執行後面的程式,當接收到服務端響應時,再對其進行相應的處理。 無論是哪個方法,都必須要指定要調用的服務及其方法名稱,以及一個用戶端傳入參數的引用,還有一個用於接收處理結果參數的指標 如果沒有指定RPC傳輸過程中使用何種編碼解碼器,預設使用Go標準庫提供的eccoding/gob包進行資料轉送
伺服器端代碼
package mainimport ( "errors" "log" "net" "net/http" "net/rpc" "os" "time")type Args struct { A, B int}type Quotient struct { Quo, Rem int}type Arith int//計算乘積func (t *Arith) Multiply(args *Args, reply *int) error { time.Sleep(time.Second * 3) //睡三秒,同步調用會等待,非同步會先往下執行 *reply = args.A * args.B return nil}//計算商和餘數func (t *Arith) Divide(args *Args, quo *Quotient) error { time.Sleep(time.Second * 3) 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服務註冊了一個arith對象 公開方法供用戶端調用 rpc.Register(arith) //指定rpc的傳輸協議 這裡採用http協議作為rpc調用的載體 也可以用rpc.ServeConn處理單個串連請求 rpc.HandleHTTP() l, e := net.Listen("tcp", ":1234") if e != nil { log.Fatal("listen error", e) } go http.Serve(l, nil) os.Stdin.Read(make([]byte, 1))}
用戶端代碼
package mainimport ( "fmt" "log" "net/rpc" "time")type Args struct { A, B int}type Quotient struct { Quo, Rem int}func main() { //調用rpc服務端提供的方法之前,先與rpc服務端建立串連 client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234") if err != nil { log.Fatal("dialHttp error", err) return } //同步調用服務端提供的方法 args := &Args{7, 8} var reply int //可以查看源碼 其實Call同步調用是用非同步呼叫實現的。後續再詳細學習 err = client.Call("Arith.Multiply", args, &reply) //這裡會阻塞三秒 if err != nil { log.Fatal("call arith.Multiply error", err) } fmt.Printf("Arith:%d*%d=%d\n", args.A, args.B, reply) //非同步呼叫 quo := Quotient{} divCall := client.Go("Arith.Divide", args, &quo, nil) //使用select模型監聽通道有資料時執行,否則執行後續程式 for { select { case <-divCall.Done: fmt.Printf("商是%d,餘數是%d\n", quo.Quo, quo.Rem) default: fmt.Println("繼續向下執行....") time.Sleep(time.Second * 1) } }}
說明
//Go函數的原型,注意其最後一個參數是一個channel 也就是調用結果存到了這個channel裡 裡面的類型資料是*Call類型//如果你不傳遞這個channel這個參數,Go函數內部會預設建立一個*Call類型的10個長度的channel來緩衝結果資料。channel的名字叫做//Done也就是傳回值*Calll倆面的最後一個值 //好吧,其實我就是想說,cient.Go的傳回值包含了最後一個參數(channel),想擷取調用結果,可以從參數管道中直接擷取,也可以從傳回值//Done中擷取// Call represents an active RPC.type Call struct { ServiceMethod string // The name of the service and method to call. Args interface{} // The argument to the function (*struct). Reply interface{} // The reply from the function (*struct). Error error // After completion, the error status. Done chan *Call // Strobes when call is complete.}func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
二 gRpc
gRPC中文文檔
gRPC是一個高效能、開源、通用的RPC架構。基於HTTP/2協議標準設計開發,預設採用Protocol Buffers資料序列化協議([Protocol Buffers基本文法]()),支援多種開發語言。gRPC提供了一種簡單的方法來精確的定義服務,並且為用戶端和服務端自動產生可靠的功能庫
在gRPC用戶端可以直接調用不通伺服器上的遠程程式,就想調用本地程式一樣,很容易構建分布式應用和服務。和很多RPC系統一樣,服務負責實現定義好的介面並處理用戶端請求,用戶端根據介面描述直接調用需要的服務。用戶端和伺服器可以分別使用gRPC支援的不同語言實現
protobuf Golang外掛程式
執行完成後會在GOPATH/bin目錄下產生 protoc-gen-go
工具,在編譯.proto檔案時,protoc命令需要用到此外掛程式
[關於protobuf文法和基本使用]()
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
grpc-go golang第三方庫下載(需要翻牆 用ss配置http代理完成終端翻牆))
go get -u google.golang.org/grpc
流程
1.編寫.proto描述檔案 2.編譯產生.pb.go檔案 3.用戶端實現約定的介面並提供服務 4.用戶端按照約定調用方法請求服務
目錄結構
$GOPATH/src/go_demo/23gPRC/|—— hello/ |—— client/ |—— main.go // 用戶端 |—— server/ |—— main.go // 服務端|—— proto/ |—— hello.proto // proto描述檔案 |—— hello.pb.go // proto編譯後檔案
proto rpc服務描述檔案
syntax = "proto3"; //指定proto版本package proto;//定義請求結構message HelloRequest{ string name=1;}//定義響應結構message HelloReply{ string message=1;}//定義Hello服務service Hello{ //定義服務中的方法 rpc SayHello(HelloRequest)returns (HelloReply){}}
protoc -I . --go_out=plugins=grpc:. ./hello.proto
hello.proto檔案中定義了一個Hello Service 該服務包含了一個SayHello方法 同時聲明了HelloRequest和HelloReply訊息結構
用於請求和響應。用戶端使用HelloRequest參數調用SayHello方法請求服務端 服務端響應HelloReply訊息
根據hello.proto檔案編譯產生Golang源檔案 hello.pb.go
源檔案中包含訊息傳遞的請求和響應結構。服務端註冊對象的方法 建立用戶端 以及調用服務端方法
伺服器端代碼
package mainimport ( "fmt" pb "go_demo/23gRPC/proto" "net" "golang.org/x/net/context" "google.golang.org/grpc")const ( //gRPC服務地址 Address = "127.0.0.1:50052")//定義一個helloServer並實現約定的介面type helloService struct{}func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { resp := new(pb.HelloReply) resp.Message = "hello" + in.Name + "." return resp, nil}var HelloServer = helloService{}func main() { listen, err := net.Listen("tcp", Address) if err != nil { fmt.Printf("failed to listen:%v", err) } //實現gRPC Server s := grpc.NewServer() //註冊helloServer為用戶端提供服務 pb.RegisterHelloServer(s, HelloServer) //內部調用了s.RegisterServer() fmt.Println("Listen on" + Address) s.Serve(listen)}
go run main.go
Listen on127.0.0.1:50052
用戶端代碼
package mainimport ( "fmt" pb "go_demo/23gRPC/proto" "golang.org/x/net/context" "google.golang.org/grpc")const ( Address = "127.0.0.1:50052")func main() { //串連gRPC伺服器 conn, err := grpc.Dial(Address, grpc.WithInsecure()) if err != nil { fmt.Println(err) } defer conn.Close() //初始化用戶端 c := pb.NewHelloClient(conn) //調用方法 reqBody := new(pb.HelloRequest) reqBody.Name = "gRPC" r, err := c.SayHello(context.Background(), reqBody) if err != nil { fmt.Println(err) } fmt.Println(r.Message)}
go run main.go
hello gRpc