项目中收到一个需求:
需要设备在扫码后,播放支付消息。
之前有做过类似的需求,采用是每隔3秒去后台取支付结果,如果有多条支付消息,按照时间排序进行语音播放。由于采用的是短连接,当没有支付结果消息时,也会不断请求后台造成资源浪费。因为是不断轮询后台,语音播放结果也不是很及时。
经过查询技术资料,可以使用Grpc来完成上述需求,完美的规避上面提到的两个问题。整体架构如下:
核心点:
通信模式
在开始编写Grpc服务时,我们需要了解Grpc的三种通信模式(C2S,C2SS, CS2S)。分别对应三种模式
C2S | 非流模式,一问一答 |
---|---|
C2SS | 服务端流模式,一问多答 |
CS2S | 客户端流模式,多问一答 |
CS2SS | 双端流模式,多问多答 |
针对上述需求,采用的服务端流模式
,当开启连接后,客户端不断接受服务端投递过来的支付消息。
序列化
Grpc是有google开发的一款高性能rpc框架,自然底层是通过protobuf来进行接口中的数据传递。它基于HTTP/2协议进行传输,从而支持双向流、头部压缩、多路复用等特性,进一步提升通信效率与性能。
代码编写
protobuf文件
ini
syntax = "proto3";
package model;
import "google/protobuf/timestamp.proto";
option java_package = "com.**.task.grpc.proto";
option java_outer_classname = "CommandProto";
service CommandService {
rpc command(SendMessage) returns (stream ReceiveMessage) {}
}
message SendMessage {
// 商户ID
string merId = 1;
// 设备ID
string terId = 2;
}
message ReceiveMessage {
uint32 code = 1;
string message = 2;
CommandMessage data = 3;
}
message CommandMessage {
string id = 1;
string type = 2;
google.protobuf.Timestamp time = 3;
// 支付消息
string content = 4;
}
文件还是非常简单的,区别其他protobuf文件的是我们需要额外添加rpc接口
scss
service CommandService {
rpc command(SendMessage) returns (stream ReceiveMessage) {}
}
编译
通过idea自带的任务generateProto可以直接生成Java服务类和实体
配置可以参考:
ini
buildscript {
dependencies {
classpath("com.google.protobuf:protobuf-gradle-plugin:0.8.16")
}
}
apply plugin: 'com.google.protobuf'
// 生成proto文件配置,默认文件proto文件放置到src/main/proto下
protobuf {
generatedFilesBaseDir = "$projectDir/src/gen"
protoc {
artifact = 'com.google.protobuf:protoc:3.11.4'
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.4.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
发布Jar
代码编译完成后,我们需要发布出去供其他服务器来使用。由于不是在src目录下面,所以我们需要配置打包的源代码目录
ini
sourceSets {
main {
java {
srcDir 'src/gen/main/java'
srcDir 'src/gen/main/grpc'
}
}
}
publishing {
publications {
maven(MavenPublication) {
groupId = 'com.**.component'
artifactId = 'task-grpc-api'
version = '1.0.0'
from components.java
artifact sourcesJar
}
}
repositories {
maven {
allowInsecureProtocol = true
url 'http://host:port/repository/maven-releases/'
credentials {
username = 'name'
password = 'password'
}
}
}
}
服务端代码编写
引用上面打包好的Jar包,进行Grpc服务暴露。这里使用的是github上使用的比较多的grpc框架。
implementation('net.devh:grpc-server-spring-boot-starter:2.14.0.RELEASE')
java
package com.wuhanpe.command.service;
import cn.hutool.core.lang.UUID;
import cn.hutool.http.HttpStatus;
import com.google.protobuf.Timestamp;
import com.wuhanpe.task.grpc.proto.CommandProto;
import com.wuhanpe.task.grpc.proto.CommandServiceGrpc;
import com.wuhanpe.task.grpc.proto.HeartbeatProto;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
import java.util.HashMap;
import java.util.Map;
/**
* @author:zooooooooy
* @date: 2024/2/1 - 17:48
* C2SS 通过服务端定时下发指令到客户端
*/
@GrpcService
public class CommandService extends CommandServiceGrpc.CommandServiceImplBase {
private Map<String, StreamObserver<CommandProto.ReceiveMessage>> streamObserverMap = new HashMap<>();
@Override
public void command(CommandProto.SendMessage request, StreamObserver<CommandProto.ReceiveMessage> responseObserver) {
streamObserverMap.put(request.getTerId(), responseObserver);
responseObserver.onNext(CommandProto.ReceiveMessage
.newBuilder()
.setCode(HttpStatus.HTTP_OK)
.setData(CommandProto.CommandMessage
.newBuilder()
.setId(UUID.fastUUID().toString())
.setType("INFO")
.setTime(Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1000).build())
.setContent("连接成功")
)
.build());
}
public StreamObserver<CommandProto.ReceiveMessage> getCommandStreamObserver(String terId) {
return streamObserverMap.get(terId);
}
}
主要做了两个操作,客户端连上来时把连接存储到缓存中,同步返回连接成功的消息给客户端。
支付消息接收
下面就要处理我们的业务逻辑。通过spring编写一个简单的消息接收接口。
less
@RestController
@RequestMapping("v1/rpc/message")
@Slf4j
public class MessageController {
@Autowired
private TtsService ttsService;
@PostMapping("receive")
public ResponseEntity receive(@RequestBody TtsMessage msg) {
log.info("receive tts message => {}", JSONUtil.toJsonStr(msg));
ttsService.pushTtsToDevice(msg);
return ResponseEntity.ok().build();
}
}
支付消息投递
获得支付消息后,进行简单组装后,通过我们刚才建立的C2SS服务端流通道投递到服务端即可。
less
public class TtsService {
@Autowired
private CommandService commandService;
public void pushTtsToDevice(TtsMessage message) {
StreamObserver<CommandProto.ReceiveMessage> commandStreamObserver = commandService.getCommandStreamObserver(message.getTerId());
if(commandStreamObserver == null) {
log.info("device [{}] not connect to server, could not push message", message.getTerId());
return;
}
try {
commandStreamObserver.onNext(CommandProto.ReceiveMessage
.newBuilder()
.setCode(HttpStatus.HTTP_OK)
.setData(CommandProto.CommandMessage
.newBuilder()
.setId(UUID.fastUUID().toString())
.setType("TTS")
.setTime(Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1000).build())
.setContent((message.getPayType().equals("wx") ? "微信" : "支付宝") + "支付" + (Integer.parseInt(message.getPayAmount()) * 1.0 / 100) + "元")
)
.build());
} catch(StatusRuntimeException e) {
// 处理异常,
}
}
}
至此,代码逻辑完成。
运行结果
可以做到毫秒级的消息投递,极大的减少流量消耗。
注意事项
- 客户端需要有保活机制,当机器重启或者app重启时,能够通过异常情况的捕获,重新发起rpc连接。
- 服务端投递消息失败时,需要有状态机制。保存失败的支付消息,待连接重新上来时,进行再次投递。
总结
本次需求的客户端是Android操作系统,区别于服务端Java环境,这个时候就需要用到跨多语言的Rpc框架。Grpc完美的解决了这个问题,不论Go,Java,Kotlin等都可以进行远程调用。且性能优于我们常用的duboo和feign等框架。
整个解决方案利用gRPC的C2SS模式实现了支付消息的即时、高效、低流量推送,显著提升了用户体验,避免了传统轮询方式带来的延迟和资源浪费。此外,gRPC的跨语言特性也确保了无论是在Android客户端还是Java服务端都能无缝对接和协同工作。