理解grpc的四种通信模式以及他们的适用场景 | 豆包MarsCode AI刷题

248 阅读5分钟

1. 概述

  1. 请求-响应(Unary) 客户端建立连接,发送一个请求,服务端响应一个请求,然后连接就关闭了。
    • 适用于简单业务场景,比如查询用户信息、查询商品详情等等。
  2. 服务端流(server stream) 客户端建立连接,发送一个请求,服务端可以持续不断地返回数据,直到发送完毕。
    • 适用于请求的服务端数据太大的场景,比如查询历史订单、加载社交平台的动态流、聊天记录等
      • 为什么查询历史订单适合服务端流,请求响应模式不行吗?
        • 假如用户的历史订单数据只有几个,那么可以使用”请求响应“模式。但如果用户的历史订单数量有上千条,要是一次性发送,可能会造成传输慢、并且消耗网络带宽。而且用户等待所有订单加载完成才看到数据显式,用户体验感也不好。
    • 服务端流的优势:
      • 逐步发送数据:服务端先返回几条订单记录,客户端可以马上展示给用户。
      • 提升用户体验:用户不用等待全部订单加载完,就能看到部分订单,感觉会更流畅。
      • 减轻网络压力:数据分批发送,不会让网络一次性处理过多数据,更稳定可靠。
  3. 客户端流(client stream) 客户端建立连接,持续不断地向服务端发送数据,直到发送完毕后,服务端最后再统一返回一个数据。
    • 适用场景:
      • 批量上传:比如在电商系统中,假设一个用户要批量上传多个商品图片,可以一次性上传所有图片文件,服务端接收完后统一返回上传结果。
      • 数据聚合:假设用户上传一批数据,比如订单信息,服务端计算出这些订单的总金额后返回给用户。
  4. 双向流(bidirectional stream) 客户端和服务端各自都能随意发送数据,就类似于实时聊天通信一样。
    • 适用场景:
      • 实时消息传输:比如在电商系统的客服系统中,用户和客服之间可以实时双向发送消息。还有视频流传输,股票价格更新等等。
      • 实时数据处理:比如双向数据更新,客户端发送数据更新请求,服务端实时返回处理结果。

2. 请求-响应模式

  • 模拟实现查询用户信息的服务功能:客户端调用服务端的”用户服务(UserService)“的查询用户信息方法(GetUserInfo),根据用户id(userId)查询用户的个人信息数据,然后打印出来。 先思考一下编码思路。
  1. 定义和生成服务接口和消息数据
    1. 定义服务接口和消息数据
      • 接口有什么方法
      • 请求消息的变量属性
      • 响应消息的变量属性
    2. 编写proto文件
    3. protoc生成go代码
  2. server端:
    1. 创建服务结构体,实现”用户服务“的接口方法(GetUserInfo)
      • 结构体里嵌入接口:pb.UnimplementedXxxServiceServer
      • 方法内容: 获取请求信息中的用户id,根据id返回用户的信息。
    2. 创建监听器,监听指定的端口
    3. 创建一个grpc服务:grpc.NewServer()
    4. 把“用户服务”对象注册到grpc服务中
    5. 启动用户服务
  3. client端:
    1. 拨号连接指定的域名端口
    2. 实例化客户端对象
    3. 客户端对象调用服务的方法,发送用户id数据,得到返回的响应数据(用户信息)。
    4. 打印用户信息
// userinfo.proto
syntax="proto3";  
  
package userinfo;  
  
option go_package = ".;proto";  
  
service UserService {  
  rpc GetUserInfo(UserRequest) returns (UserResponse);  
}  
  
message UserRequest {  
  string id = 1;  
}  
  
message UserResponse {  
  string name = 1;  
  int32 age = 2;  
  string email = 3;  
}

3. 服务端流

  • 模拟实现查询用户所有历史订单的功能:客户端调用订单服务的查询订单方法(GetUserOrders),根据用户id查询用户历史订单。
  • 先思考一下编码思路:和请求响应模式差不多,仅仅是几点不同
    • proto中服务方法的参数不同
      • 返回参数多一个stream,用于客户端不断从这个流中读取数据。
    • 服务方法发送数据时要使用stream.Send()逐个发送订单数据。
    • 客户端调用服务方法获取流,使用流接收数据stream.Recv()
syntax = "proto3";  
  
package order_service;  
  
option go_package=".;order_service";  
  
service OrderService{  
  rpc GetUserOrders(OrderRequest) returns (stream OrderResponse);  
}  
  
message OrderRequest {  
  string user_id = 1;  
}  
  
message OrderResponse {  
  string order_id = 1;  
  string product_name = 2;  
  float price = 3;  
}

4. 客户端流

  • 实现一个批量提交订单数据的接口,客户端可以把多个订单一次性发送给服务端,服务端在接收到所有订单后,计算出总金额并返回。
  • 先思考编码思路,和服务端流区别在于:
    • proto文件中服务接口的定义,方法的接收参数多一个stream,能够持续不断接收数据
    • 服务的方法中,循环接收数据(steam.Recv()),接收完数据后发送响应数据并关闭连接stream.SendAndClose()
    • 客户端,调用服务方法获取流stream,然后循环发送数据stream.Send(),最后关闭连接并接收响应数据stream.CloseAndRecv()
syntax = "proto3";  
  
package order_service;  
  
option go_package=".;order_service";  
  
service OrderService {  
  rpc SubmitOrders(stream OrderRequest) returns (OrderSummary);  
}  
  
message OrderRequest {  
  string order_id = 1;  
  float price = 2;  
}  
  
message OrderSummary {  
  int32 total_orders = 1;  
  float total_price = 2;  
}

5. 双向流

  • 实现一个实时订单状态更新接口,客户端可以持续发送订单状态更新请求,服务端会在接收到更新后即时返回处理结果。
  • 先思考一下编码思路:
    • proto服务接口定义中,参数列表和返回值中都带有stream
    • 服务端:接收数据-->更新处理-->返回更新。stream.Recv()stream.Send()
    • 客户端:调用服务方法获取流stream,然后发送数据stream.Send(),接收数据stream.Recv()
      • 关闭流stream.CloseSend()
syntax = "proto3";  
  
option go_package=".;order_status_service";  
  
package order_status_service;  
  
service OrderStatusService {  
  rpc UpdateOrderStatus(stream OrderStatusRequest) returns (stream OrderStatusResponse);  
}  
  
message OrderStatusRequest {  
  string order_id = 1;  
  string status = 2;  
}  
  
message OrderStatusResponse {  
  string order_id = 1;  
  string confirmation = 2;  
}