gRPC学习之四:实战四类服务方法

// 双向流式 :集合请求,集合响应

rpc MultiReqMultiResp (stream SingleRequest) returns (stream SingleResponse);

}

  • 这个grpcstream.proto文件有以下几处要注意的地方:
  1. 方法SingleReqSingleResp非常简单,和上一篇文章中的demo一样,入参是一个数据结构,服务端返回的也是一个数据结构;

  2. 方法SingleReqSingleResp是服务端流式类型,特征是返回值用stream修饰;

  3. 方法MultiReqSingleResp是客户端流式类型,特征是入参用stream修饰;

  4. 方法MultiReqMultiResp是双向类型,特征是入参和返回值都用stream修饰;

  • 似乎有规律可循:客户端如果想和服务端建立通道传输持续的数据,就在通道位置用stream修饰,一共有两个位置,第一个是进入服务端的入参,第二个是从服务端出来的返回值;

根据proto生成go源码

  1. 在grpcstream.proto所在的目录,执行以下命令:

protoc --go_out=plugins=grpc:. grpcstream.proto

  1. 如果grpcstream.proto没有语法错误,会在当前目录生成文件grpcstream.pb.go,这里面是工具protoc-gen-go自动生成的代码,里面生成的代码在开发服务端和客户端时都会用到;

  2. 对服务端来说,grpcstream.pb.go中最重要的是IGrpcStremServiceServer接口 ,服务端需要实现该接口所有的方法作为业务逻辑,接口定义如下:

type IGrpcStremServiceServer interface {

// 单项流式 :单个请求,单个响应

SingleReqSingleResp(context.Context, *SingleRequest) (*SingleResponse, error)

// 服务端流式 :单个请求,集合响应

SingleReqMultiResp(*SingleRequest, IGrpcStremService_SingleReqMultiRespServer) error

// 客户端流式 :集合请求,单个响应

MultiReqSingleResp(IGrpcStremService_MultiReqSingleRespServer) error

// 双向流式 :集合请求,集合响应

MultiReqMultiResp(IGrpcStremService_MultiReqMultiRespServer) error

}

  1. 对客户端来说,grpcstream.pb.go中最重要的是IGrpcStremServiceClient接口,如下所示,这意味这客户端可以发起哪些远程调用 :

type IGrpcStremServiceClient interface {

// 单项流式 :单个请求,单个响应

SingleReqSingleResp(ctx context.Context, in *SingleRequest, opts …grpc.CallOption) (*SingleResponse, error)

// 服务端流式 :单个请求,集合响应

SingleReqMultiResp(ctx context.Context, in *SingleRequest, opts …grpc.CallOption) (IGrpcStremService_SingleReqMultiRespClient, error)

// 客户端流式 :集合请求,单个响应

MultiReqSingleResp(ctx context.Context, opts …grpc.CallOption) (IGrpcStremService_MultiReqSingleRespClient, error)

// 双向流式 :集合请求,集合响应

MultiReqMultiResp(ctx context.Context, opts …grpc.CallOption) (IGrpcStremService_MultiReqMultiRespClient, error)

}

编写服务端代码server.go并启动

  • 在$GOPATH/src/grpcstream目录下新建文件夹server,在此文件夹下新建server.go,内容如下(稍后会指出几处要注意的地方):

package main

import (

“context”

“google.golang.org/grpc”

pb “grpcstream”

“io”

“log”

“net”

“strconv”

)

// 常量:监听端口

const (

port = “:50051”

)

// 定义结构体,在调用注册api的时候作为入参,

// 该结构体会带上proto中定义的方法,里面是业务代码

// 这样远程调用时就执行了业务代码了

type server struct {

// pb.go中自动生成的,是个空结构体

pb.UnimplementedIGrpcStremServiceServer

}

// 单项流式 :单个请求,单个响应

func (s *server) SingleReqSingleResp(ctx context.Context, req *pb.SingleRequest) (*pb.SingleResponse, error) {

id := req.GetId()

// 打印请求参数

log.Println(“1. 收到请求:”, id)

// 实例化结构体SingleResponse,作为返回值

return &pb.SingleResponse{Id: id, Name: “1. name-” + strconv.Itoa(int(id))}, nil

}

// 服务端流式 :单个请求,集合响应

func (s *server) SingleReqMultiResp(req *pb.SingleRequest, stream pb.IGrpcStremService_SingleReqMultiRespServer) error {

// 取得请求参数

id := req.GetId()

// 打印请求参数

log.Println(“2. 收到请求:”, id)

// 返回多条记录

for i := 0; i < 10; i++ {

stream.Send(&pb.SingleResponse{Id: int32(i), Name: “2. name-” + strconv.Itoa(i)})

}

return nil

}

// 客户端流式 :集合请求,单个响应

func (s *server) MultiReqSingleResp(reqStream pb.IGrpcStremService_MultiReqSingleRespServer) error {

var addVal int32 = 0

// 在for循环中接收流式请求

for {

// 一次接受一条记录

singleRequest, err := reqStream.Recv()

// 不等于io.EOF表示这是条有效记录

if err == io.EOF {

log.Println(“3. 客户端发送完毕”)

break

} else if err != nil {

log.Fatalln(“3. 接收时发生异常”, err)

break

} else {

log.Println(“3. 收到请求:”, singleRequest.GetId())

// 收完之后,执行SendAndClose返回数据并结束本次调用

addVal += singleRequest.GetId()

}

}

return reqStream.SendAndClose(&pb.SingleResponse{Id: addVal, Name: “3. name-” + strconv.Itoa(int(addVal))})

}

// 双向流式 :集合请求,集合响应

func (s *server) MultiReqMultiResp(reqStream pb.IGrpcStremService_MultiReqMultiRespServer) error {

// 简单处理,对于收到的每一条记录都返回一个响应

for {

singleRequest, err := reqStream.Recv()

// 不等于io.EOS表示这是条有效记录

if err == io.EOF {

log.Println(“4. 接收完毕”)

return nil

} else if err != nil {

log.Fatalln(“4. 接收时发生异常”, err)

return err

} else {

log.Println(“4. 接收到数据”, singleRequest.GetId())

id := singleRequest.GetId()

if sendErr := reqStream.Send(&pb.SingleResponse{Id: id, Name: “4. name-” + strconv.Itoa(int(id))}); sendErr != nil {

log.Println(“4. 返回数据异常数据”, sendErr)

return sendErr

}

}

}

}

func main() {

// 要监听的协议和端口

lis, err := net.Listen(“tcp”, port)

if err != nil {

log.Fatalf(“failed to listen: %v”, err)

}

// 实例化gRPC server结构体

s := grpc.NewServer()

// 服务注册

pb.RegisterIGrpcStremServiceServer(s, &server{})

log.Println(“开始监听,等待远程调用…”)

if err := s.Serve(lis); err != nil {

log.Fatalf(“failed to serve: %v”, err)

}

}

  • 这个server.go文件有以下几处要注意的地方:
  1. SingleReqMultiResp方法的作用是收到客户端一个请求参数,然后向客户端发送多个响应,可见多次调用stream.Send方法即可多次发送数据到客户端;

  2. MultiReqSingleResp方法可以从客户端收到多条数据,可见是在for循环中重复调用reqStream.Recv()方法,直到收到客户端的io.EOF为止,这就要就客户端在发送完数据后再给一个io.EOF过来,稍后的客户端代码会展示如何做;

  3. MultiReqMultiResp方法持续接受客户端数据,并且持续发送数据给客户端,一定要把顺序问题考虑清楚,否则会陷入异常(例如一方死循环),我这里的逻辑是收到客户端的io.EOF为止,这就要就客户端在发送完数据后再给一个io.EOF过来,如果客户端也在用for循环一直等数据,那就是双方都在等数据了,无法终止程序,稍后的客户端代码会展示如何做;

  • 在server.go所在目录执行go run server.go,控制台提示如下:

[golang@centos7 server]$ go run server.go

2020/12/13 21:29:19 开始监听,等待远程调用…

  • 此时gRPC的服务端已经启动,可以响应远程调用,接下来开发客户端代码;

编写客户端代码client.go

  • 再打开一个控制台;

  • 在$GOPATH/src/grpcstream目录下新建文件夹client,在此文件夹下新建client.go,内容如下(稍后会指出几处要注意的地方):

package main

import (

“context”

“google.golang.org/grpc”

“io”

“log”

“time”

pb “grpcstream”

)

const (

address = “localhost:50051”

defaultId = “666”

)

func main() {

// 远程连接服务端

conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())

if err != nil {

log.Fatalf(“did not connect: %v”, err)

}

// main方法执行完毕后关闭远程连接

defer conn.Close()

// 实例化数据结构

client := pb.NewIGrpcStremServiceClient(conn)

// 超时设置

ctx, cancel := context.WithTimeout(context.Background(), time.Second)

defer cancel()

log.Println(“测试单一请求应答,一对一”)

singleReqSingleResp(ctx, client)

log.Println(“测试服务端流式应答,一对多”)

singleReqMultiResp(ctx, client)

log.Println(“测试客户端流式请求,多对一”)

multiReqSingleResp(ctx, client)

log.Println(“测试双向流式请求应答,多对多”)

multiReqMultiResp(ctx, client)

log.Println(“测试完成”)

}

func singleReqSingleResp(ctx context.Context, client pb.IGrpcStremServiceClient) error {

// 远程调用

r, err := client.SingleReqSingleResp(ctx, &pb.SingleRequest{Id: 101})

if err != nil {

log.Fatalf(“1. 远程调用异常 : %v”, err)

return err

}

// 将服务端的返回信息打印出来

log.Printf(“response, id : %d, name : %s”, r.GetId(), r.GetName())

return nil

}

func singleReqMultiResp(ctx context.Context, client pb.IGrpcStremServiceClient) error {

// 远程调用

recvStream, err := client.SingleReqMultiResp(ctx, &pb.SingleRequest{Id: 201})

if err != nil {

log.Fatalf(“2. 远程调用异常 : %v”, err)

return err

}

for {

singleResponse, err := recvStream.Recv()

if err == io.EOF {

log.Printf(“2. 获取数据完毕”)

break

}

log.Printf(“2. 收到服务端响应, id : %d, name : %s”, singleResponse.GetId(), singleResponse.GetName())

}

return nil

}

func multiReqSingleResp(ctx context.Context, client pb.IGrpcStremServiceClient) error {

// 远程调用

sendStream, err := client.MultiReqSingleResp(ctx)

if err != nil {

log.Fatalf(“3. 远程调用异常 : %v”, err)

return err

}

// 发送多条记录到服务端

for i:=0; i<10; i++ {

if err = sendStream.Send(&pb.SingleRequest{Id: int32(300+i)}); err!=nil {

log.Fatalf(“3. 通过流发送数据异常 : %v”, err)

return err

}

}

singleResponse, err := sendStream.CloseAndRecv()

if err != nil {

log.Fatalf(“3. 服务端响应异常 : %v”, err)

return err

}

// 将服务端的返回信息打印出来

log.Printf(“response, id : %d, name : %s”, singleResponse.GetId(), singleResponse.GetName())

return nil

}

func multiReqMultiResp(ctx context.Context, client pb.IGrpcStremServiceClient) error {

// 远程调用

intOutStream, err := client.MultiReqMultiResp(ctx)

if err != nil {

log.Fatalf(“4. 远程调用异常 : %v”, err)

return err

}

// 发送多条记录到服务端

for i:=0; i<10; i++ {

if err = intOutStream.Send(&pb.SingleRequest{Id: int32(400+i)}); err!=nil {

log.Fatalf(“4. 通过流发送数据异常 : %v”, err)

return err

}

}

// 服务端一直在接收,直到收到io.EOF为止

// 因此,这里必须发送io.EOF到服务端,让服务端知道发送已经结束(很重要)

intOutStream.CloseSend()

// 接收服务端发来的数据

for {

singleResponse, err := intOutStream.Recv()

if err == io.EOF {

log.Printf(“4. 获取数据完毕”)

break

} else if err != nil {

log.Fatalf(“4. 接收服务端数据异常 : %v”, err)

break

}

log.Printf(“4. 收到服务端响应, id : %d, name : %s”, singleResponse.GetId(), singleResponse.GetName())

}

return nil

}

  • 这个client.go文件有以下几处要注意的地方:
  1. singleReqMultiResp方法会接收服务端的多条记录,在for循环中调用recvStream.Recv即可收到所有数据;

  2. multiReqSingleResp方法会向服务端发送多条数据,由于服务端在等待io.EOF作为结束标志,因此调用sendStream.CloseAndRecv即可发送io.EOF,并得到服务端的返回值;

  3. multiReqMultiResp方法在持续向服务端发送数据,并且也在持续获取服务端发来的数据,在发送数据完成后,必须调用intOutStream.CloseSend方法,即可发送io.EOF,让服务端不再接收数据,避免前面提到的死循环;

  4. 在main方法中,依次发起四类服务方法的调用;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Java)

一线互联网大厂Java核心面试题库

image

正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等已整理上传,感兴趣的朋友可以看看支持一波!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
va开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**[外链图片转存中…(img-KXOh6vVZ-1713852387491)]

[外链图片转存中…(img-42oFGbNC-1713852387491)]

[外链图片转存中…(img-3MIkKLQv-1713852387492)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Java)

[外链图片转存中…(img-HzPVGtQK-1713852387492)]

一线互联网大厂Java核心面试题库

[外链图片转存中…(img-KIw9vApr-1713852387492)]

正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等已整理上传,感兴趣的朋友可以看看支持一波!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值