目录
- 一、rpc基础
- 1 - rpc入门
- 2 - 基础的rpc通信
- 3 - 基于接口的RPC服务
- 二、rpc编码
- 1 - gob编码
- 2 - json on tcp
- 3 - json on http(待补充)
- 三、prtotobuf编码
- 1 - prtotobuf概述
- 2 - protobuf编译器
- 3 - 序列化和反序列化
- 4 - 基于protobuf的RPC(待补充00:41:00到01:10:00)
- 四、proto3语法
- 1 - 消息定义
- 2 - 基础类型
- 3 - 枚举类型
- 4 - 数组类型
- 5 - Map
- 6 - Oneof
- 7 - Any
- 8 - 类型嵌套
- 9 - 引用包
- 五、gRPC
- 1 - gRPC概念
- 2 - gRPC简单实现
- 3 - 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编译器:
- 作用:把定义文件(.proto)(IDL: 接口描述语言), 编译成不同语言的数据结构
- 下载地址:https://github.com/protocolbuffers/protobuf/releases
- 将bin中的protoc.exe配置到环境变量中运行 ->
protoc --version
-
通用命令查看已设置的环境变量路径:
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;
}
- 进入路径pbrpc:
protoc -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()
- 采用断言来判断one of的类型:
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)
}
}