自己瞎琢磨的,有错勿怪。
一、rpc理解
微服务会在不同的端口使用不同的语言提供相异的服务,端口之间的通信就使用rpc。这边的grpc的“g”我原先意味是golang,后来发现是google。
在golang关于rpc的官方包中,rpc主要有使用http/tcp协议,使用gob/json两个不同的维度,我之前的文章中也写过相关的例子。
grpc原先应该是cpp使用的,但是后来支持了不同的语言,特殊之处在于使用protobuf(似乎可以换成json但是我暂时没有看到这部分)作为通信的格式,此外通信方面使用的是HTTP2,所以我们会处理相关的stream,也就是流,能让我们的通信有更灵活的操作。
二、protobuf
首先可能要学习protobuf的相关知识,这里我就不写了,它的out路径方面我感觉还是有点复杂的。下面我把自己的例子放出来
syntax="proto3";
package test;//这个test其实会影响到生成的interface或者是client和server结构体的命名
option go_package="my_grpc_project/proto"; // 这是一个绝对路径的引入,从自己的项目路径开始 所以在编译的时候应该加上--go_opt=paths=source_relative 表示这个路径是相对的
import "test1.proto"; //得知import的文件应该和主文件在同一个文件夹下面
// 下面这结构体就是用来打印输出信息的
message QustMessage{
string msg=1;
}
// 定义一下从简单到双向流的四种行为
service Test{
rpc SimpleTest(QustMessage)returns(test1.ReplyMessage){}; //包名作为前缀,让其识别
rpc ServerStream(QustMessage)returns(stream test1.ReplyMessage){};
rpc ClientStream(stream QustMessage)returns(test1.ReplyMessage){};
rpc BothStream(stream QustMessage)returns(stream test1.ReplyMessage){};
};
这里定义了一个QustMessage的信息对象,里面就一个msg的string。此外,我尝试了一下引入外部的test1.proto,test1.proto定义了ReplyMessage,如下:
syntax="proto3";
package test1;
option go_package="my_grpc_project/proto";
message ReplyMessage{
string msg=1;
}
回到上文的protobuf文件,我在service当中定义了四种服务,分别是SimpleTest(发送一个请求消息,返回一个回复消息)、ServerStream(发送一个请求消息,服务器推送一堆消息)、ClientStream(客户端发送一堆请求消息,服务器返回一个消息)和BothStream(双方都发送一堆消息)。注意这里的行为更像是一种定义,没有写服务的实体,这些服务所对应的具体内容后期是需要你自己补充的。
这一步说一下,vscode可能对引入路径之类的产生报错,不要管,没事的,另外就是路径的问题,也就是go_package部分,我是建议全部写成“.”,也就是生成在本文件夹内。
然后我们需要使用指令生成相关的文件,这里生成的文件有两种,一中是简单的pd.go的文件,这种文件是把对应的protobuf转换成go的结构体,并附加一些对应的方法,比如我这边ReplyMessage当中有一个msg的string,会生成如下的方法让你获取对应的信息
func (x *ReplyMessage) GetMsg() string {
if x != nil {
return x.Msg
}
return ""
}
类似的还有String(输出)和Reset(重置ReplyMessage)的方法。
另外一个文件就是对应的test_grpc.pb.go这样类型的文件,这个文件里生成的内容会辅助你生成grpc的客户端和服务端。我会在下面具体谈一谈。
看到test_grpc.pb.go,你就应该知道protobuf生成文件名称的格式规则,就是protobuf中的package名+pb.go和package_grpc.pb.go。(注意protobuf中的package不是golang之中的package)
我这边没有写生成protobuf和grpc文件的终端代码,我觉得还是自己操作一下,贯通整个流程比较好
三、test_grpc.pb.go
如果你成功生成了这个文件,你可能需要看一看其中的长长的内容。这一部分其实有点难以表达。就拿BothStream作一个例子吧
客户端部分
首先是客户端部分,首先会创建一个整体的客户端,客户端调用BothStream方法
func (c *testClient) BothStream(ctx context.Context, opts ...grpc.CallOption) (Test_BothStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &Test_ServiceDesc.Streams[2], "/test.Test/BothStream", opts...)
if err != nil {
return nil, err
}
x := &testBothStreamClient{stream}
return x, nil
}
这个方法生成了一个Test_BothStreamClient的接口和对应方法
type Test_BothStreamClient interface {
Send(*QustMessage) error
Recv() (*ReplyMessage, error)
grpc.ClientStream
}
type testBothStreamClient struct {
grpc.ClientStream
}
func (x *testBothStreamClient) Send(m *QustMessage) error {
return x.ClientStream.SendMsg(m)
}
func (x *testBothStreamClient) Recv() (*ReplyMessage, error) {
m := new(ReplyMessage)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
实际上就是返回了一个testBothStreamClient,这结构体有Send和Recv方法以及grpc.ClientStream原有的方法。作为能发送stream的客户端,明显需要收发两个方法。
服务端
再看一下服务器端:
func _Test_BothStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(TestServer).BothStream(&testBothStreamServer{stream})
}
type Test_BothStreamServer interface {
Send(*ReplyMessage) error
Recv() (*QustMessage, error)
grpc.ServerStream
}
type testBothStreamServer struct {
grpc.ServerStream
}
func (x *testBothStreamServer) Send(m *ReplyMessage) error {
return x.ServerStream.SendMsg(m)
}
func (x *testBothStreamServer) Recv() (*QustMessage, error) {
m := new(QustMessage)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
服务器端基本上也一样,也是接受两个方法,很合理,毕竟两边都是流,只是多了一个如下的内部函数
func _Test_BothStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(TestServer).BothStream(&testBothStreamServer{stream})
}
这个函数,我们将在服务器和客户端建立起来的时候再讨论。
其他的三个函数都是类似的,虽然方法上有点区别,可以通过查看test_grpc.pb.go去理解。
四、调用
客户端
首先是客户端发起调用,这部分直接跟着官网写,这里为了方便,依旧是调用BothStream
func main() {
conn, err := grpc.Dial("localhost:996", grpc.WithTransportCredentials(insecure.NewCredentials()))
// 这上面的安全参数居然一定要写
if err != nil {
log.Fatal("dial fail: ", err)
}
defer conn.Close()
testClient := pb.NewTestClient(conn)
// 创建一个context
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 开始调用相关的函数
// 调用第四个函数,这第四个函数采取的是一来一回的模式
testBothStreamClient, err := testClient.BothStream(ctx)
if err != nil {
fmt.Println("both stream 调用失败:", err)
}
msgArr := []string{"你", "好", "啊"}
index := 0
giveout := func(index int) (err error) {
if index > len(msgArr)-1 {
err = io.EOF
return
}
err = testBothStreamClient.Send(&pb.QustMessage{Msg: msgArr[index]})
return
}
for {
err1 := giveout(index)
if err1 == io.EOF {
testBothStreamClient.CloseSend()
fmt.Println(err1)
break
}
if err1 != nil {
fmt.Println("1", err1)
break
}
res4, err := testBothStreamClient.Recv()
if err != nil {
fmt.Println("close and Recv fail:", err)
}
fmt.Println("the forth func", res4.GetMsg())
index++
}
}
这边我写了一个giveout函数,用for循环去发送msgArr := []string{"你", "好", "啊"}字体,采取了一问一答的方式,接受服务器端发送的信息
服务器
下面是服务器端,照理照抄官网
package main
import (
"context"
"fmt"
"io"
"log"
"strconv"
pb "my_grpc_project/proto"
"net"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedTestServer
}
// 第四个函数
func (s *server) BothStream(tbss pb.Test_BothStreamServer) (err error) {
for {
res, err := tbss.Recv()
if err == io.EOF {
fmt.Println("全部接受完成")
break
}
if err != nil {
fmt.Println("接受失败")
break
}
err = tbss.Send(&pb.ReplyMessage{Msg: fmt.Sprintf("接收到你的消息:%s\n", res.GetMsg())})
if err != nil {
fmt.Println("发送信息失败")
break
}
}
return
}
func main() {
listener, err := net.Listen("tcp", "localhost:996")
if err != nil {
log.Fatalln("listen fail: ", err)
}
var opt []grpc.ServerOption
grpcServer := grpc.NewServer(opt...)
pb.RegisterTestServer(grpcServer, &server{})
fmt.Println("grpc客户端启动,正在localhost:996进行监听")
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
服务器端需要具体写一下对应的BothStream方法,服务器端接受请求的时候就会调用这个方法,我们上面还有一个问题,既然会调用我自己定义的方法,那么在test_grpc.pb.go当中为什么会有这个函数呢
func _Test_BothStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(TestServer).BothStream(&testBothStreamServer{stream})
}
以我目前的知识,我认为,应该和开启服务端的这个内容一起来看
pb.RegisterTestServer(grpcServer, &server{})
我在注册服务器的时候,将这个server带着自定义方法的server也传了进去,这应该对应的就是_Test_BothStream_Handler函数的srv参数,每次服务端调用实际的BothStream方法的时候,实际上调用的_Test_BothStream_Handler函数,而在这个函数之中调用srv中的你自己定义的方法。所以大概就是这样的一个流程。我还讲整个测试的内容传入了gitee,需要的可以看看,包含完整的四个函数的调用,当然这只是初步,并没有包含更细节的配置。my_grpc_project: grpc使用测试https://gitee.com/huangfengnt/my_grpc_project