介紹
這篇文章介紹使用 grpc-go 編寫 Client/Server 程序
一、proto 文件定義及 Go 代碼生成
gRpc 使用 protobuf 作為默認的 wire-format 傳輸,在 .proto 文件中,我們需要定義傳輸過程中需要使用的各種 Message 類型,同時我們還需要定義 service,并在 service 中提供遠程調用的各種方法。
(1)傳輸過程中的數據格式
提供 service 的 .proto
文件中定義兩個 message 類型,一個用于指定請求的參數類型 HelloRequest,一個用于返回值的類型 HelloReply
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
(2)提供的服務 Service
除了定義消息類型,.proto
文件中還需要定義一個 service 用于供客戶端遠程調用, 在這個 service 中聲明 RPC 服務所要提供的各種方法的函數簽名。
注意:這里只是函數聲明,其指明了需要接收的參數類型和返回的類型。函數的實現需要我們根據 proto 生成的 Go 語言文件,在 Server 端編寫代碼來自行實現。
// The request message containing the user's name.
service Greeter {
rpc SayHello (HelloRequest) returns(HelloReply){}
rpc SayGoodby (GoodByRequest) returns(GoodByReplay) {}
}
我們可以把上述(1)和(2)的內容放在兩個單獨的 .proto 文件中(當然,必須使用同一個包名)。也可以放在同一個 .proto 文件中。通常,為了方便,我們都會放在同一個 .proto 文件中。這里我們把上述內容放在 helloworld.proto 文件中。
現在,調用 protoc 來生成 go 代碼
protoc -I ghello/ ghello/ghello.proto --go_out=plugins=grpc:ghello
protoc -I helloworld/helloworld/ helloworld/helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
執行上述語句,protoc 會生成一個叫 helloworld.pb.go 的文件,這里面就包含了 Go 語言表述的相關代碼。
二、生成的 helloworld.pb.go 代碼解析
理解 protoc 所生成的 helloworld.pb.go
代碼有助于我們理解整個 gRpc 的調用過程。
(1)首先,按照 proto 文件中所申明的各種不同消息類型,會生成對應名稱的 struct 結構體,如下:
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
(2)為上述結構體生成一些默認的方法,比如
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
(3)為 Service 分別生成 Client 端和 Server 端的 interface 定義,如下:
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
這里要特別注意,雖然兩者都是 interface 類型,但是 Client 端的對象是由 protoc 自動生成的,其實現了 GreeterClient 接口,見(4)。而 Server 端的對象則需要我們自己去手動編寫了。因為我們是服務提供方嘛,提供什么具體的服務當然是由我們決定的。
(4)生成默認的 Client 類型,以便 gRpc 的客戶端可以使用它來連接及調用 gRpc 服務端提供的服務。
type greeterClient struct {
cc *grpc.ClientConn
}
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
注意到上述 Client 默認實現的 SayHello 函數,這個叫做 Client Stub,其實就是相當于本地實現了一個 SayHello 函數,當 grpc 的客戶端調用 SayHello 函數的時候,其調用的就是這個本地的 SayHello 函數,這個函數在內部通過 grpc.Invoke() 的方式實現了遠程調用。
(5)注冊服務
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
這里的邏輯很簡單,我們需要在我們的服務端,自己去定義一個 struct 對象,實現 .pb.go 中所聲明的 GreeterServer 接口,然后把那個 struct 注冊到 grpc 服務上。
三、啟動 gRpc 服務端和客戶端
理解了上述所生成的 pb.go 的代碼內容,現在我們就需要來編寫 gRpc 的 Server 端代碼了。先來看 Server 端代碼怎么寫才能提供 gRpc 服務。
為了簡單,我分成了如下如下步驟:
- (1) 指定需要提供服務的端口,本地未被使用的任意端口都可以,比如 50051。
- (2) 監聽端口,調用net.Listen("tcp", port)
- (3) 定義一個 server struct 來實現 proto 文件中 service 部分所聲明的所有 RPC 方法。 這步最關鍵!
- (4) 調用 grpc.NewServer() 來創建一個 Server
- (5) 把上述 struct 的實例注冊到 gprc 上
- (6)調用 s.Serve(lis) 提供 gRpc 服務
package main
import (
"log"
"net"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
// server is used to implement helloworld.GreeterServer.
type server struct{}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
有了服務端提供服務,我們就要想辦法訪問它了,這就需要我們編寫 gRpc 的客戶端了,來看看客戶端代碼怎么寫。
我也詳細分成了如下步驟:
- (1)連接服務端, 調用
conn = grpc.Dial("localhost:50051", grpc.WithInsecure())
- (2)創建
GreeterClient(conn)
,把連接成功后返回的 conn 傳入 - (3)調用具體的方法
SayHello()
package main
import (
"log"
"os"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
這里,只需要把正確類型的參數傳給 c.SayHello() 就能完成 grpc 調用了。如:&pb.HelloRequest{Name: name}
注:要連接 gRpc 的服務端,使用上述 Go 語言版本的 gRpc client 可以完成,使用其他任何 grpc 支持的語言的 client 都可以完成。比如,我們使用 cpp 版本的 client
#include <iostream>
#include <memory>
#include <string>
#include <grpc++/grpc++.h>
#ifdef BAZEL_BUILD
#include "examples/protos/helloworld.grpc.pb.h"
#else
#include "helloworld.grpc.pb.h"
#endif
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using helloworld::HelloRequest;
using helloworld::HelloReply;
using helloworld::Greeter;
class GreeterClient {
public:
GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
// Assembles the client's payload, sends it and presents the response back
// from the server.
std::string SayHello(const std::string& user) {
// Data we are sending to the server.
HelloRequest request;
request.set_name(user);
// Container for the data we expect from the server.
HelloReply reply;
// Context for the client. It could be used to convey extra information to
// the server and/or tweak certain RPC behaviors.
ClientContext context;
// The actual RPC.
Status status = stub_->SayHello(&context, request, &reply);
// Act upon its status.
if (status.ok()) {
return reply.message();
} else {
std::cout << status.error_code() << ": " << status.error_message()
<< std::endl;
return "RPC failed";
}
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
int main(int argc, char** argv) {
// Instantiate the client. It requires a channel, out of which the actual RPCs
// are created. This channel models a connection to an endpoint (in this case,
// localhost at port 50051). We indicate that the channel isn't authenticated
// (use of InsecureChannelCredentials()).
GreeterClient greeter(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()));
std::string user("world");
std::string reply = greeter.SayHello(user);
std::cout << "Greeter received: " << reply << std::endl;
return 0;
}
在服務端,查詢 db,并且賦值給 HelloReply 中的 fields。 然后把 HelloReply message 返回給客戶端。
全文完