使用Grpc实现支付消息推送

项目中收到一个需求:

需要设备在扫码后,播放支付消息。

之前有做过类似的需求,采用是每隔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) {
            // 处理异常,
        }

    }

}

至此,代码逻辑完成。

运行结果

可以做到毫秒级的消息投递,极大的减少流量消耗。

注意事项

  1. 客户端需要有保活机制,当机器重启或者app重启时,能够通过异常情况的捕获,重新发起rpc连接。
  2. 服务端投递消息失败时,需要有状态机制。保存失败的支付消息,待连接重新上来时,进行再次投递。

总结

本次需求的客户端是Android操作系统,区别于服务端Java环境,这个时候就需要用到跨多语言的Rpc框架。Grpc完美的解决了这个问题,不论Go,Java,Kotlin等都可以进行远程调用。且性能优于我们常用的duboo和feign等框架。

整个解决方案利用gRPC的C2SS模式实现了支付消息的即时、高效、低流量推送,显著提升了用户体验,避免了传统轮询方式带来的延迟和资源浪费。此外,gRPC的跨语言特性也确保了无论是在Android客户端还是Java服务端都能无缝对接和协同工作。

相关推荐
码上一元7 天前
RPC 服务与 gRPC 的入门案例
网络·网络协议·rpc·grpc
Jrainlau15 天前
bun 实现 gRPC 服务器
前端·grpc·bun
gsls2008081 个月前
小型kv数据库leveldb配合grpc实现网络访问
数据库·grpc·leveldb
许野平2 个月前
Rust:设计 gRPC 客户端
开发语言·后端·rust·grpc·tonic
钢铁小狗侠2 个月前
如何在windows上下载和编译grpc
grpc
寒烟说2 个月前
用 Go 语言实现一个最简单的 gRPC 服务端
开发语言·后端·golang·grpc
陈亦康3 个月前
Armeria gPRC 高级特性 - 装饰器、无框架请求、阻塞处理器、Nacos集成、负载均衡、rpc异常处理、文档服务......
kotlin·grpc·armeria
假装我不帅3 个月前
asp.net core grpc快速入门
后端·asp.net·grpc
cci3 个月前
Rust gRPC---Tonic教程
后端·rust·grpc
磐石区3 个月前
gRPC etcd 服务注册与发现、自定义负载均衡
服务发现·负载均衡·etcd·grpc·picker