14、RPC与gRPC

一、rpc基础

1 - rpc入门

  • ipc:Inter-Process Communication,进程间通信,进程间通信是指两个进程的数据之间产生交互
  • rpc:RPC是远程过程调用的简称,是分布式系统中不同节点间流行的通信方式
    • RPC传输协议
    • 消息序列化与反序列化
      在这里插入图片描述

2 - 基础的rpc通信

  • server
package main

import (
	"fmt"
	"log"
	"net"
	"net/rpc"
)

// servuce handler
type HelloService struct {
    
    
}

// Hello的逻辑 就是 将对方发送的消息前面添加一个Hello 然后返还给对方
// 由于我们是一个rpc服务, 因此参数上面还是有约束:
//
//	第一个参数是请求
//	第二个参数是响应
//
// 可以类比Http handler
func (p *HelloService) Hello(request string, response *string) error {
    
    
	*response = fmt.Sprintf("hello,%s", request)
	return nil
}

func main() {
    
    
	// 把我们的对象注册成一个rpc的 receiver
	// 其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数
	// 所有注册的方法会放在“HelloService”服务空间之下
	rpc.RegisterName("HelloService", &HelloService{
    
    })

	// 然后我们建立一个唯一的TCP链接
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
    
    
		log.Fatal("ListenTCP error:", err)
	}

	// 通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
	// 没Accept一个请求,就创建一个goroutie进行处理
	for {
    
    
		conn, err := listener.Accept()
		if err != nil {
    
    
			log.Fatal("Accept error:", err)
		}

		// 前面都是tcp的知识, 到这个RPC就接管了
		// 因此 你可以认为 rpc 帮我们封装消息到函数调用的这个逻辑
		// 提升了工作效率, 逻辑比较简洁,可以看看他代码
		go rpc.ServeConn(conn)
	}
}

  • client
package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main() {
    
    
	// 首先是通过rpc.Dial拨号RPC服务, 建立连接
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
    
    
		log.Fatal("dialing:", err)
	}

	// 然后通过client.Call调用具体的RPC方法
	// 在调用client.Call时:
	// 		第一个参数是用点号链接的RPC服务名字和方法名字,
	// 		第二个参数是 请求参数
	//      第三个是请求响应, 必须是一个指针, 有底层rpc服务帮你赋值
	var reply string
	err = client.Call("HelloService.Hello", "jack", &reply)
	if err != nil {
    
    
		log.Fatal(err)
	}

	fmt.Println(reply)
}

在这里插入图片描述

3 - 基于接口的RPC服务

  • rpc方法存在的优化
    • client call 方法, 里面3个参数2个interface{}, 你再使用的时候 可能真不知道要传入什么, 这就好像你写了一个HTTP的服务, 没有接口文档, 容易调用错误
    • 或者说,如何对rpc接口进行一次封装,用来限定接口的参数类型
// Call invokes the named function, waits for it to complete, and returns its error status.
func (client *Client) Call(serviceMethod string, args interface{
    
    }, reply interface{
    
    }) error {
    
    
	call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
	return call.Error
}
  • interface\service\interface.go
package service

const (
	//统一rpc的注册名
	SERVICE_NAME = "HelloService"
)

type HelloService interface {
    
    
	Hello(request string, response *string) error
}

  • interface\server\main.go:添加接口约束
package main

import (
	"fmt"
	"log"
	"main/interface/service"
	"net"
	"net/rpc"
)

// 通过接口约束HelloService服务
var _ service.HelloService = (*HelloService)(nil)

type HelloService struct {
    
    
}

func (p *HelloService) Hello(request string, response *string) error {
    
    
	*response = fmt.Sprintf("hello,%s", request)
	return nil
}

func main() {
    
    
	rpc.RegisterName(service.SERVICE_NAME, &HelloService{
    
    })
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
    
    
		log.Fatal("ListenTCP error:", err)
	}
	for {
    
    
		conn, err := listener.Accept()
		if err != nil {
    
    
			log.Fatal("Accept error:", err)
		}
		go rpc.ServeConn(conn)
	}
}

  • interface\client\main.go:添加接口约束
package main

import (
	"fmt"
	"main/interface/service"
	"net/rpc"
)

// 约束客户端
var _ service.HelloService = (*HelloServiceClient)(nil)

func NewHelloServiceClient(network string, address string) (*HelloServiceClient, error) {
    
    
	client, err := rpc.Dial(network, address)
	if err != nil {
    
    
		return nil, err
	}

	return &HelloServiceClient{
    
    
		client: client,
	}, nil
}

type HelloServiceClient struct {
    
    
	client *rpc.Client
}

func (c *HelloServiceClient) Hello(request string, response *string) error {
    
    
	return c.client.Call(fmt.Sprintf("%s.Hello", service.SERVICE_NAME), request, response)
}

func main() {
    
    
	c, err := NewHelloServiceClient("tcp", ":1234")
	if err != nil {
    
    
		panic(err)
	}
	var response string
	if err := c.Hello("tom", &response); err != nil {
    
    
		panic(err)
	}

	fmt.Println(response)
}

二、rpc编码

1 - gob编码

  • 什么是gob编码:标准库的RPC默认采用Go语言特有的gob编码,标准库gob是golang提供的“私有”的编解码方式,它的效率会比json,xml等更高,特别适合在Go语言程序间传递数据
  • codec\gob.go
package codec

import (
	"bytes"
	"encoding/gob"
)

func GobEncode(obj interface{
    
    }) ([]byte, error) {
    
    
	buf := bytes.NewBuffer([]byte{
    
    })
	//编码后的结果输出到buf里
	encoder := gob.NewEncoder(buf)
	//编码obj对象
	if err := encoder.Encode(obj); err != nil {
    
    
		return []byte{
    
    }, err
	}
	return buf.Bytes(), nil
}

func GobDecode(data []byte, obj interface{
    
    }) error {
    
    
	decoder := gob.NewDecoder(bytes.NewReader(data))
	return decoder.Decode(obj)
}

  • codec\gob_test.go
package codec

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

type TestStruct struct {
    
    
	F1 string
	F2 int
}

func TestGob(t *testing.T) {
    
    
	should := assert.New(t)
	gobBytes, err := GobEncode(&TestStruct{
    
    
		F1: "test_f1",
		F2: 10,
	})

	if should.NoError(err) {
    
    
		fmt.Println(gobBytes)
	}

	obj := TestStruct{
    
    }
	err = GobDecode(gobBytes, &obj)
	if should.NoError(err) {
    
    
		fmt.Println(obj)
	}
}

2 - json on tcp

  • gob编码的缺点:gob是golang提供的“私有”的编解码方式,因此从其它语言调用Go语言实现的RPC服务将比较困难
  • Go语言的RPC框架
    • RPC数据打包时可以通过插件实现自定义的编码和解码
    • RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上
  • 所有语言都支持的比较好的一些编码
    • MessagePack: 高效的二进制序列化格式。它允许你在多种语言(如JSON)之间交换数据。但它更快更小
    • JSON: 文本编码
    • XML:文本编码
    • Protobuf 二进制编码
  • json_tcp\service
package service

const (
	//统一rpc的注册名
	SERVICE_NAME = "HelloService"
)

type HelloService interface {
    
    
	Hello(request string, response *string) error
}

  • json_tcp\server\main.go
package main

import (
	"fmt"
	"log"
	"main/interface/service"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)

// 通过接口约束HelloService服务
var _ service.HelloService = (*HelloService)(nil)

type HelloService struct {
    
    
}

func (p *HelloService) Hello(request string, response *string) error {
    
    
	*response = fmt.Sprintf("hello,%s", request)
	return nil
}

func main() {
    
    
	rpc.RegisterName(service.SERVICE_NAME, &HelloService{
    
    })
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
    
    
		log.Fatal("ListenTCP error:", err)
	}
	for {
    
    
		conn, err := listener.Accept()
		if err != nil {
    
    
			log.Fatal("Accept error:", err)
		}
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

  • json_tcp\client\main.go
package main

import (
	"fmt"
	"main/interface/service"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)

// 约束客户端
var _ service.HelloService = (*HelloServiceClient)(nil)

func NewHelloServiceClient(network string, address string) (*HelloServiceClient, error) {
    
    
	conn, err := net.Dial(network, address)
	if err != nil {
    
    
		return nil, err
	}
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

	return &HelloServiceClient{
    
    
		client: client,
	}, nil
}

type HelloServiceClient struct {
    
    
	client *rpc.Client
}

func (c *HelloServiceClient) Hello(request string, response *string) error {
    
    
	return c.client.Call(fmt.Sprintf("%s.Hello", service.SERVICE_NAME), request, response)
}

func main() {
    
    
	c, err := NewHelloServiceClient("tcp", ":1234")
	if err != nil {
    
    
		panic(err)
	}
	var response string
	if err := c.Hello("tom", &response); err != nil {
    
    
		panic(err)
	}

	fmt.Println(response)
}

3 - json on http(待补充)

三、prtotobuf编码

1 - prtotobuf概述

  • Protobuf概述
    • Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源
    • Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能
    • Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具
  • 编解码工具的参考选项
    • 编解码效率
    • 高压缩比
    • 多语言支持
      在这里插入图片描述
  • protobuf使用流程
    在这里插入图片描述
  • protobuf简单例子
    • syntax: 表示采用proto3的语法。第三版的Protobuf对语言进行了提炼简化,所有成员均采用类似Go语言中的零值初始化(不再支持自定义默认值),因此消息成员也不再需要支持required特性
    • package:指明当前是main包(这样可以和Go的包名保持一致,简化例子代码),当然用户也可以针对不同的语言定制对应的包路径和名称
    • option:protobuf的一些选项参数, 这里指定的是要生成的Go语言package路径, 其他语言参数各不相同
    • message: 关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员编码时用1编号代替名字
syntax = "proto3";

package hello;
option go_package="gitee.com/infraboard/go-course/day21/pb";

message String {
    
    
    string value = 1;
}

2 - protobuf编译器

  • 关于数据编码

    • 在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据
    • 但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅
    • 我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分
  • protobuf编译器

  • 通用命令查看已设置的环境变量路径where protoc
    在这里插入图片描述

  • include编译器库:这个根据需要,覆盖对应的include文件目录
    在这里插入图片描述

  • 安装protobuf的go语言插件

    • Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的插件
    • go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
      在这里插入图片描述
  • 生成pb文件protoc -I="." --go_out=./pb --go_opt=module="gitee.com/infraboard/go-course/day21/pb" pb/hello.proto

    • -I:-IPATH, --proto_path=PATH, 指定proto文件搜索的路径, 如果有多个路径 可以多次使用-I 来指定, 如果不指定默认为当前目录
    • –go_out: --go指插件的名称, 我们安装的插件为: protoc-gen-go, 而protoc-gen是插件命名规范, go是插件名称, 因此这里是–go, 而–go_out 表示的是 go插件的 out参数, 这里指编译产物的存放目录
    • –go_opt: protoc-gen-go插件opt参数, 这里的module指定了go module, 生成的go pkg 会去除掉module路径,生成对应pkg
    • pb/hello.proto: 我们proto文件路径
  • 如果需要为当前所有的proto生成pb文件:进入到protobuf/pb目录执行protoc -I="." --go_out=. --go_opt=module="gitee.com/infraboard/go-course/day21/pb" *.proto
    在这里插入图片描述

  • 安装依赖库:生成之后我们可以看到有需要用到一些库,这里可以使用go mod tidy进行同步安装
    在这里插入图片描述

3 - 序列化和反序列化

  • 如何实现序列化和反序列化:使用google.golang.org/protobuf/proto工具提供的API来进行序列化与反序列化
    • Marshal:序列化
    • UnMarshal:反序列化
  • 测试序列化与反序列化:protobuf\pb\hello_test.go
package pb_test

import (
	"fmt"
	"main/protobuf/pb"
	"testing"

	"github.com/stretchr/testify/assert"
	"google.golang.org/protobuf/proto"
)

func TestMarshal(t *testing.T) {
    
    
	should := assert.New(t)
	str := &pb.String{
    
    Value: "hello"}

	// object -> protobuf -> []byte
	pbBytes, err := proto.Marshal(str)
	if should.NoError(err) {
    
    
		fmt.Println(pbBytes)
	}
	// []byte -> protobuf -> object
	obj := pb.String{
    
    }
	err = proto.Unmarshal(pbBytes, &obj)
	if should.NoError(err) {
    
    
		fmt.Println(obj.Value)
	}
}

在这里插入图片描述

4 - 基于protobuf的RPC(待补充00:41:00到01:10:00)

  • 定义交互数据结构:pbrpc\service\pb\hello.proto
syntax = "proto3";

package hello;
option go_package="pbrpc/service";

message Request {
    
    
    string value = 1;
}

message Response {
    
    
    string value = 1;
}
  • 进入路径pbrpcprotoc -I="./service/pb/" --go_out=./service --go_opt=module="pbrpc/service" hello.proto
    在这里插入图片描述
    在这里插入图片描述
  • pbrpc\service\interface.go
package service

const (
	SERVICE_NAME = "HelloService"
)

type HelloService interface {
    
    
	Hello(request *Request, response *Response) error
}

四、proto3语法

1 - 消息定义

  • 定义消息类型
syntax = "proto3";

/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
    
    
  string query = 1;
  int32 page_number = 2; // Which page number do we want?
  int32 result_per_page = 3;
}
  • 第一行是protobuf的版本, 我们主要讲下 message 的定义语法
    • comment: 注射 /* */或者 //
    • message_name: 同一个pkg内,必须唯一
    • filed_rule: 可以没有, 常用的有repeated, oneof
    • filed_type: 数据类型, protobuf定义的数据类型, 生产代码的会映射成对应语言的数据类型
    • filed_name: 字段名称, 同一个message 内必须唯一
    • field_number: 字段的编号, 序列化成二进制数据时的字段编号, 同一个message 内必须唯一, 1 ~ 15 使用1个Byte表示, 16 ~ 2047 使用2个Byte表示
<comment>

message  <message_name> {
    
    
  <filed_rule>  <filed_type> <filed_name> = <field_number> 
        			类型         名称             编号  
}
  • 如果你想保留一个编号,以备后来使用可以使用 reserved 关键字声明
message Foo {
    
    
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

2 - 基础类型

  • Value(Filed) Types:protobuf 定义了很多Value Types, 他和其他语言的映射关系如下

在这里插入图片描述

3 - 枚举类型

  • 使用enum来声明枚举类型
syntax = "proto3";

package hello;
option go_package="gitee.com/infraboard/go-course/day21/pb";

enum Color {
    
    
    RED = 0;
    GREEN = 1;
    BLACK = 2;
}
  • 对应生成的pb文件
    在这里插入图片描述
  • 枚举声明语法
    • enum_name: 枚举名称
    • element_name: pkg内全局唯一, 很重要
    • element_name: 必须从0开始, 0表示类型的默认值, 32-bit integer
enum <enum_name> {
    
    
    <element_name> = <element_number>
}
  • 别名:如果你的确有2个同名的枚举需求: 比如 TaskStatus 和 PipelineStatus 都需要Running,就可以添加一个: option allow_alias = true;
enum EnumAllowingAlias {
    
    
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }

在这里插入图片描述

  • 枚举也支持预留值
enum Foo {
    
    
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

4 - 数组类型

  • 数组类型使用repeated
syntax = "proto3";

package hello;
option go_package="protobuf/pb";

message Host{
    
    
    string name = 1;
    string ip = 2;
}

message SearchResponse {
    
    
    int64 total = 1;
    //定义一个HOST数组
    repeated Host hosts = 2;
}

在这里插入图片描述
在这里插入图片描述

5 - Map

  • protobuf 声明map的语法map<key_type, value_type> map_field = N;
syntax = "proto3";

package hello;
option go_package="protobuf/pb";

message Host{
    
    
    string name = 1;
    string ip = 2;
    map <string,string> tags = 3;
}

message SearchResponse {
    
    
    int64 total = 1;
    //定义一个HOST数组
    repeated Host hosts = 2;
}

在这里插入图片描述

6 - Oneof

  • Oneof:限定类型只能是其中一种
message ProtobufEventHeader{
    
    
    string id = 1;
    map<string,string> headers = 2;
}

message JSONEventHeader{
    
    
    string id = 1;
    bytes headers = 2;
}

message Event{
    
    
    oneof header{
    
    
        ProtobufEventHeader protobuf = 1;
        JSONEventHeader json = 2;
    }
}
  • 两种使用方法
    • 采用断言来判断one of的类型:e.GetHeader()
    • 直接通过get获取,判断返回是否为nil
      • err1 := e.GetProtobuf()
      • err2 := e.GetJson()

7 - Any

  • Any:当我们无法明确定义数据类型的时候, 可以使用Any表示
    • 注意需要引入包:import "google/protobuf/any.proto"
    • protoc如果报错,需要在-I中指定google protobuf的路径
// 这里是应用其他的proto文件, 后面会讲 ipmort用法
import "google/protobuf/any.proto";

message ErrorStatus {
    
    
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
func TestAny(t *testing.T) {
    
    
	es := &pb.ErrorStatus{
    
    Message: "hello"}
	anyEs, err := anypb.New(es)

	if err != nil {
    
    
		panic("err")
	}
	fmt.Println(anyEs) //[type.googleapis.com/hello.ErrorStatus]:{message:"hello"}

	obj := pb.ErrorStatus{
    
    }
	anyEs.UnmarshalTo(&obj)
	fmt.Println(obj.Message) //hello
}

8 - 类型嵌套

  • 可以再message里面嵌套message(不建议使用): 但是不允许 匿名嵌套, 必须指定字段名称
message Outer {
    
                      // Level 0
    message MiddleAA {
    
      // Level 1
      message Inner {
    
       // Level 2
        int64 ival = 1;
        bool  booly = 2;
      }
    }
    message MiddleBB {
    
      // Level 1
      message Inner {
    
       // Level 2
        int32 ival = 1;
        bool  booly = 2;
      }
    }
  }

在这里插入图片描述
在这里插入图片描述

9 - 引用包

  • import “google/protobuf/any.proto”;
    • 上面这在情况就是读取的标准库, 我们在安装protoc的时候, 已经把改lib 挪到usr/local/include下面了,所以可以找到
    • 如果我们proto文件并没有在/usr/local/include目录下, 通过-I 可以添加搜索的路径, 这样就编译器就可以找到我们引入的包了
      • protoc -I=. --go_out=./pb --go_opt=module="gitee.com/infraboard/go-course/day21/pb" pb/import.proto

五、gRPC

1 - gRPC概念

  • 什么是grpc:gRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。gRPC基于HTTP/2协议设计,可以基于一个HTTP/2链接提供多个服务,对于移动设备更加友好

  • GRPC技术栈:Stub: 应用程序通过gRPC插件生产的Stub代码和gRPC核心库通信,也可以直接和gRPC核心库通信

    • 数据交互格式: protobuf
    • 通信方式: 最底层为TCP或Unix Socket协议,在此之上是HTTP/2协议的实现
    • 核心库: 在HTTP/2协议之上又构建了针对Go语言的gRPC核心库
    • Stub: 应用程序通过gRPC插件生产的Stub代码和gRPC核心库通信,也可以直接和gRPC核心库通信
      在这里插入图片描述
  • 安装grpc插件

# protoc-gen-go 插件之前已经安装
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# 安装protoc-gen-go-grpc插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
  • 查看当前插件的版本:``
    在这里插入图片描述

2 - gRPC简单实现

  • grpc\server\pb\hello.proto
    • 根据需求进入对应的路径:C:\develop_project\go_project\proj1\grpc\server
    • 生成命令:protoc -I="." --go_out=. --go_opt=module="grpc/server" --go-grpc_out=. --go-grpc_opt=module="grpc/server" pb/hello.proto
    • 可以看到生成了grpc\server\pb\hello_grpc.pb.go
    • 如果是第一次生成还需要同步下:go mod tidy
syntax = "proto3";

package hello;
option go_package="grpc/server/pb";

//PS C:\develop_project\go_project\proj1\grpc\server>
//protoc -I="." --go_out=. --go_opt=module="grpc/server" pb/hello.proto 

//grpc生成
//PS C:\develop_project\go_project\proj1\grpc\server>
//protoc -I="." --go_out=. --go_opt=module="grpc/server" --go-grpc_out=. --go-grpc_opt=module="grpc/server" pb/hello.proto

service HelloService{
    
    
    rpc hello (HelloRequest) returns(HelloResponse);
}

message HelloRequest{
    
    
    string value = 1;
}

message HelloResponse{
    
    
    string value = 1;
}

在这里插入图片描述

  • server:grpc\server\main.go
package main

import (
	"context"
	"fmt"
	"log"
	"main/grpc/server/pb"
	"net"

	"google.golang.org/grpc"
)

type HelloServiceServer struct {
    
    
	pb.UnimplementedHelloServiceServer
}

func (p *HelloServiceServer) Hello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
	return &pb.HelloResponse{
    
    
		Value: fmt.Sprintf("hello,%s", req.Value),
	}, nil
}

func main() {
    
    
	//把实现类注册给 GRPC Server
	server := grpc.NewServer()
	pb.RegisterHelloServiceServer(server, &HelloServiceServer{
    
    })

	listen, err := net.Listen("tcp", ":1234")
	if err != nil {
    
    
		panic(err)
	}

	log.Printf("grpc listen addr: 127.0.0.1:1234")

	//监听Socket,HTTP2内置
	if err := server.Serve(listen); err != nil {
    
    
		panic(err)
	}
}

  • client:grpc\client\main.go
package main

import (
	"context"
	"fmt"
	"main/grpc/server/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
    
    
	//第一步,建立网络连接
	conn, err := grpc.DialContext(context.Background(), "127.0.0.1:1234", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		panic(err)
	}

	//grpc为我们生成一个客户端调用服务端的SDK
	client := pb.NewHelloServiceClient(conn)
	resp, err := client.Hello(context.Background(), &pb.HelloRequest{
    
    Value: "tom"})
	if err != nil {
    
    
		panic(err)
	}
	fmt.Println(resp.Value)
}

在这里插入图片描述

3 - gRPC流

  • 为什么要使用个RPC流
    • RPC是远程函数调用,因此每次调用的函数参数和返回值不能太大,否则将严重影响每次调用的响应时间
    • 因此传统的RPC方法调用对于上传和下载较大数据量场景并不适合
    • 为此,gRPC框架针对服务器端和客户端分别提供了流特性
  • 如何制定gRPC流
    • 关键字stream指定启用流特性,参数部分是接收客户端参数的流,返回值是返回给客户端的流
    • 定义gRPC流的语法:rpc <function_name> (stream <type>) returns (stream <type>) {}
  • grpc\server\pb\hello.proto
syntax = "proto3";

package hello;
option go_package="grpc/server/pb";

//PS C:\develop_project\go_project\proj1\grpc\server>
//protoc -I="." --go_out=. --go_opt=module="grpc/server" pb/hello.proto 

//grpc生成
//PS C:\develop_project\go_project\proj1\grpc\server>
//protoc -I="." --go_out=. --go_opt=module="grpc/server" --go-grpc_out=. --go-grpc_opt=module="grpc/server" pb/hello.proto

service HelloService{
    
    
    rpc Channel(stream HelloRequest) returns(stream HelloResponse){
    
    }
}

message HelloRequest{
    
    
    string value = 1;
}

message HelloResponse{
    
    
    string value = 1;
}
  • grpc\server\main.go
package main

import (
	"fmt"
	"io"
	"log"
	"main/grpc/server/pb"
	"net"

	"google.golang.org/grpc"
)

type HelloServiceServer struct {
    
    
	pb.UnimplementedHelloServiceServer
}

func (p *HelloServiceServer) Channel(stream pb.HelloService_ChannelServer) error {
    
    
	for {
    
    
		//接收请求
		req, err := stream.Recv()
		if err != nil {
    
    
			if err == io.EOF {
    
    
				//当前客户端退出
				log.Printf("client closed")
				return nil
			}
			return err
		}
		fmt.Printf("recv req value : %s\n", req.Value)
		resp := &pb.HelloResponse{
    
    Value: fmt.Sprintf("hello,%s", req.Value)}
		//响应请求
		err = stream.Send(resp)
		if err != nil {
    
    
			if err == io.EOF {
    
    
				log.Printf("client closed")
				return nil
			}
			return err //服务端发送异常, 函数退出, 服务端流关闭
		}
	}
}

func main() {
    
    
	//把实现类注册给 GRPC Server
	server := grpc.NewServer()
	pb.RegisterHelloServiceServer(server, &HelloServiceServer{
    
    })

	listen, err := net.Listen("tcp", ":1234")
	if err != nil {
    
    
		panic(err)
	}

	log.Printf("grpc listen addr: 127.0.0.1:1234")

	//监听Socket,HTTP2内置
	if err := server.Serve(listen); err != nil {
    
    
		panic(err)
	}
}

  • grpc\client\main.go
package main

import (
	"context"
	"fmt"
	"main/grpc/server/pb"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
    
    
	//第一步,建立网络连接
	conn, err := grpc.DialContext(context.Background(), "127.0.0.1:1234", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		panic(err)
	}

	//grpc为我们生成一个客户端调用服务端的SDK
	client := pb.NewHelloServiceClient(conn)
	stream, err := client.Channel(context.Background())
	if err != nil {
    
    
		panic(err)
	}

	//启用一个Goroutine来发送请求
	go func() {
    
    
		for {
    
    
			err := stream.Send((&pb.HelloRequest{
    
    Value: "tom"}))
			if err != nil {
    
    
				panic(err)
			}
			time.Sleep(1 * time.Second)
		}
	}()
	for {
    
    
		//主循环,负责接收服务端响应
		resp, err := stream.Recv()
		if err != nil {
    
    
			panic(err)
		}
		fmt.Println(resp)
	}
}

猜你喜欢

转载自blog.csdn.net/qq23001186/article/details/130197269