Go的RPC標準庫
簡單使用
Go語言標準庫(net/rpc)的RPC規(guī)則:方法只能有兩個可序列化的參數(shù)憨琳,其中第二個參數(shù)是指針類型旷赖,并且返回一個error類型寞宫,同時必須是公開的方法供炼。
type HelloService struct {}
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
return nil
}
func main() {
rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
rpc.ServeConn(conn)
}
一個服務(wù)可以有多個方法吼和,rpc.RegisterName函數(shù)調(diào)用會將對象類型中所有滿足RPC規(guī)則的對象方法注冊為RPC函數(shù)涨薪,所有注冊的方法會放在“HelloService”服務(wù)空間之下。
客戶端代碼:
func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Call("HelloService.Hello", "Tom", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
跨語言使用
{"method":"HelloService.Hello","params":["hello"],"id":1}
{"id":1,"result":"hello:Tom","error":null}
無論采用何種語言炫乓,只要遵循同樣的json結(jié)構(gòu)刚夺,以同樣的流程就可以和Go語言編寫的RPC服務(wù)進行通信。這樣我們就實現(xiàn)了跨語言的RPC末捣。
HTTP上的RPC
func main() {
rpc.RegisterName("HelloService", new(HelloService))
http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
ReadCloser: r.Body,
Writer: w,
}
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":1234", nil)
}
Protobuf
Protobuf是Protocol Buffers的簡稱侠姑,它是Google公司開發(fā)的一種數(shù)據(jù)描述語言,并于2008年對外開源塔粒。Protobuf剛開源時的定位類似于XML结借、JSON等數(shù)據(jù)描述語言筐摘,通過附帶工具生成代碼并實現(xiàn)將結(jié)構(gòu)化數(shù)據(jù)序列化的功能卒茬。但是我們更關(guān)注的是Protobuf作為接口規(guī)范的描述語言,可以作為設(shè)計安全的跨語言PRC接口的基礎(chǔ)工具咖熟。
#hello.proto
syntax = "proto3";
option go_package = ".;main";
package main;
message String {
string value = 1;
}
Protobuf核心的工具集是C++語言開發(fā)的圃酵,安裝官方的protoc工具,可以從https://github.com/google/protobuf/releases下載馍管。在官方的protoc編譯器中并不支持Go語言郭赐。要想基于上面的hello.proto文件生成相應(yīng)的Go代碼,需要安裝相應(yīng)的插件确沸。然后是安裝針對Go語言的代碼生成插件捌锭,可以通過go get google.golang.org/protobuf/cmd/protoc-gen-go
命令安裝(這個是老版本的:github.com/golang/protobuf/protoc-gen-go)。然后通過以下命令生成相應(yīng)的Go代碼:protoc --go_out=. hello.proto
其中g(shù)o_out參數(shù)告知protoc編譯器去加載對應(yīng)的protoc-gen-go工具罗捎,然后通過該工具生成代碼观谦,生成代碼放到當前目錄,最后是一系列要處理的protobuf文件的列表桨菜,此時目錄下會有一個hello.pb.go的文件豁状。
不過用Protobuf定義語言無關(guān)的RPC服務(wù)接口才是它真正的價值所在捉偏。修改一下上面的hello.proto文件:
#hello.proto
syntax = "proto3";
option go_package = ".;main";
package main;
message String {
string value = 1;
}
service HelloService{
rpc Hello (String) returns (String);
}
然后安裝go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
,然后執(zhí)行命令生成gRPC代碼:protoc --go-grpc_out=. .\hello.proto
泻红。此時目錄下會出現(xiàn)一個hello_grpc.pb.go的文件夭禽。
Protobuf的protoc編譯器是通過插件機制實現(xiàn)對不同語言的支持。比如protoc命令出現(xiàn)--xxx_out格式的參數(shù)谊路,那么protoc將首先查詢是否有內(nèi)置的xxx插件讹躯,如果沒有內(nèi)置的xxx插件那么將繼續(xù)查詢當前系統(tǒng)中是否存在protoc-gen-xxx命名的可執(zhí)行程序,最終通過查詢到的插件生成代碼凶异。在上面的例子中蜀撑,--go_out=.會針對hello.proto文件里的message生成相關(guān)代碼,而--go-grpc_out=.會針對hello.proto文件里的service生成相關(guān)代碼剩彬。
gRPC
安裝gRPC的核心庫:go get google.golang.org/grpc
上文講的酷麦,執(zhí)行protoc --go-grpc_out=. .\hello.proto
就可以生成對應(yīng)的grpc代碼。gRPC插件會為服務(wù)端和客戶端生成不同的接口:
//客戶端
type helloServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient {
return &helloServiceClient{cc}
}
type HelloServiceClient interface {
Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
}
func (c *helloServiceClient) Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error) {
out := new(String)
err := c.cc.Invoke(ctx, "/main.HelloService/Hello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
//服務(wù)端
type HelloServiceServer interface {
Hello(context.Context, *String) (*String, error)
mustEmbedUnimplementedHelloServiceServer()
}
func RegisterHelloServiceServer(s grpc.ServiceRegistrar, srv HelloServiceServer) {
s.RegisterService(&_HelloService_serviceDesc, srv)
}
type UnimplementedHelloServiceServer struct {
}
func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {}
基于grpc重新實現(xiàn)前面的例子:
//服務(wù)端
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
)
type HelloServiceImpl struct {
UnimplementedHelloServiceServer
}
func (p *HelloServiceImpl) Hello(ctx context.Context, args *String) (*String, error) {
reply := &String{Value: "hello " + args.GetValue()}
return reply, nil
}
func main() {
grpcServer := grpc.NewServer()
RegisterHelloServiceServer(grpcServer, new(HelloServiceImpl))
lis, err := net.Listen("tcp", ":8899")
if err != nil {
log.Fatal(err)
}
grpcServer.Serve(lis)
}
//----------------------------------------------------------------------
//客戶端
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("localhost:8899", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
clien := NewHelloServiceClient(conn)
reply, err := client.Hello(context.Background(), &String{
Value: "Wang",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(reply.GetValue())
}
gRPC流
RPC是遠程函數(shù)調(diào)用喉恋,因此每次調(diào)用的函數(shù)參數(shù)和返回值不能太大沃饶,否則將嚴重影響每次調(diào)用的響應(yīng)時間。因此傳統(tǒng)的RPC方法調(diào)用對于上傳和下載較大數(shù)據(jù)量場景并不適合轻黑。為此糊肤,gRPC框架針對服務(wù)器端和客戶端分別提供了流特性。
gRPC和TLS
先分別生成服務(wù)端和客戶端的私鑰和證書:
#服務(wù)端
$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -days 3650 \
-subj "/C=GB/L=China/O=grpc-server/CN=server.grpc.io" \
-key server.key -out server.crt
----------------------------------------------------------------------------------
#客戶端
$ openssl genrsa -out client.key 2048
$ openssl req -new -x509 -days 3650 \
-subj "/C=GB/L=China/O=grpc-client/CN=client.grpc.io" \
-key client.key -out client.crt
以上命令將生成server.key氓鄙、server.crt馆揉、client.key和client.crt四個文件。其中以.key為后綴名的是私鑰文件抖拦,需要妥善保管升酣。以.crt為后綴名是證書文件,也可以簡單理解為公鑰文件态罪,并不需要秘密保存噩茄。在subj參數(shù)中的/CN=server.grpc.io表示服務(wù)器的名字為server.grpc.io,在驗證服務(wù)器的證書時需要用到該信息复颈。