HarmonyOS APP开发---"股动力"实时行情App,需要用到这个库
想做一个股票行情 App,K 线、分时、盘口数据要实时推送?REST 轮询延迟高又费流量,WebSocket 又缺强类型契约------
@ohos/grpc的双向流式 RPC 才是正解。
📦 仓库地址:https://gitcode.com/CPF-ApplicationTPC/ohos_grpc_node | 安装:ohpm install @ohos/grpc
写在前面
"股动力"的核心难点是"实时性"。股票行情每秒可能产生几十条 tick,用户打开个股详情页,期望看到的是毫秒级刷新的分时线和盘口。
传统方案的问题:
- REST 轮询:每秒请求一次,延迟最高 1 秒,流量浪费严重
- WebSocket:能推送,但没有强类型契约,前后端字段对不上全靠口口相传
- HTTP/1.1 长连接:单连接不能多路复用,并发请求互相阻塞
gRPC 基于 HTTP/2,天然支持流式通信 + Protobuf 强类型。服务端持续推 tick,客户端持续发订阅,单连接多路复用------这就是"股动力"选它的原因。
这篇文章聊什么
- Unary RPC------拉取股票基本信息(一次性请求)
- Server Streaming------服务端持续推送实时 tick
- 双向流------客户端发订阅,服务端推行情
#mermaid-svg-55sxZ1mWubHcDOMd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-55sxZ1mWubHcDOMd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-55sxZ1mWubHcDOMd .error-icon{fill:#552222;}#mermaid-svg-55sxZ1mWubHcDOMd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-55sxZ1mWubHcDOMd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-55sxZ1mWubHcDOMd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-55sxZ1mWubHcDOMd .marker.cross{stroke:#333333;}#mermaid-svg-55sxZ1mWubHcDOMd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-55sxZ1mWubHcDOMd p{margin:0;}#mermaid-svg-55sxZ1mWubHcDOMd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-55sxZ1mWubHcDOMd .cluster-label text{fill:#333;}#mermaid-svg-55sxZ1mWubHcDOMd .cluster-label span{color:#333;}#mermaid-svg-55sxZ1mWubHcDOMd .cluster-label span p{background-color:transparent;}#mermaid-svg-55sxZ1mWubHcDOMd .label text,#mermaid-svg-55sxZ1mWubHcDOMd span{fill:#333;color:#333;}#mermaid-svg-55sxZ1mWubHcDOMd .node rect,#mermaid-svg-55sxZ1mWubHcDOMd .node circle,#mermaid-svg-55sxZ1mWubHcDOMd .node ellipse,#mermaid-svg-55sxZ1mWubHcDOMd .node polygon,#mermaid-svg-55sxZ1mWubHcDOMd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-55sxZ1mWubHcDOMd .rough-node .label text,#mermaid-svg-55sxZ1mWubHcDOMd .node .label text,#mermaid-svg-55sxZ1mWubHcDOMd .image-shape .label,#mermaid-svg-55sxZ1mWubHcDOMd .icon-shape .label{text-anchor:middle;}#mermaid-svg-55sxZ1mWubHcDOMd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-55sxZ1mWubHcDOMd .rough-node .label,#mermaid-svg-55sxZ1mWubHcDOMd .node .label,#mermaid-svg-55sxZ1mWubHcDOMd .image-shape .label,#mermaid-svg-55sxZ1mWubHcDOMd .icon-shape .label{text-align:center;}#mermaid-svg-55sxZ1mWubHcDOMd .node.clickable{cursor:pointer;}#mermaid-svg-55sxZ1mWubHcDOMd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-55sxZ1mWubHcDOMd .arrowheadPath{fill:#333333;}#mermaid-svg-55sxZ1mWubHcDOMd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-55sxZ1mWubHcDOMd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-55sxZ1mWubHcDOMd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-55sxZ1mWubHcDOMd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-55sxZ1mWubHcDOMd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-55sxZ1mWubHcDOMd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-55sxZ1mWubHcDOMd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-55sxZ1mWubHcDOMd .cluster text{fill:#333;}#mermaid-svg-55sxZ1mWubHcDOMd .cluster span{color:#333;}#mermaid-svg-55sxZ1mWubHcDOMd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-55sxZ1mWubHcDOMd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-55sxZ1mWubHcDOMd rect.text{fill:none;stroke-width:0;}#mermaid-svg-55sxZ1mWubHcDOMd .icon-shape,#mermaid-svg-55sxZ1mWubHcDOMd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-55sxZ1mWubHcDOMd .icon-shape p,#mermaid-svg-55sxZ1mWubHcDOMd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-55sxZ1mWubHcDOMd .icon-shape .label rect,#mermaid-svg-55sxZ1mWubHcDOMd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-55sxZ1mWubHcDOMd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-55sxZ1mWubHcDOMd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-55sxZ1mWubHcDOMd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
打开个股页
Unary: 拉基本信息
Server Stream: 订阅 tick
服务端持续推送
渲染 K 线/分时
切换股票?
Bidi: 发新订阅
第一步:安装
bash
ohpm install @ohos/grpc
安装后需将仓库
protobufjs/index.d.ts替换到oh_modules对应路径,确保 Protobuf 类型完整。
第二步:Unary RPC------拉取股票基本信息
打开个股页,先拉一次静态信息(名称、昨收、总股本等):
typescript
import { grpc } from '@ohos/grpc'
const client = new grpc.Client('https://grpc.market-server.com', {
credentials: grpc.credentials.createInsecure()
})
// Unary:一次请求,一次响应
client.makeUnaryRequest(
'/stock.StockService/GetInfo',
(req) => req.serializeBinary(),
(buf) => StockInfo.deserializeBinary(buf),
StockRequest.encode({ code: '600519' }).finish(),
(err, response) => {
if (err) { console.error(err); return }
console.info(`${response.getName()} 昨收 ${response.getPreClose()}`)
// 渲染基本信息
}
)
Unary 模式和普通 HTTP 请求很像------一问一答。适合拉取不频繁变更的静态数据。
第三步:Server Streaming------持续接收实时 tick
订阅某只股票后,服务端持续推送 tick 数据:
typescript
// Server Streaming:一次请求,持续接收
const stream = client.makeServerStreamRequest(
'/stock.StockService/SubscribeTick',
(req) => req.serializeBinary(),
(buf) => Tick.deserializeBinary(buf),
SubscribeRequest.encode({ code: '600519', level: 1 }).finish()
)
stream.on('data', (tick) => {
// 每收到一个 tick,更新 K 线和盘口
console.info(`价格 ${tick.getPrice()} 量 ${tick.getVolume()}`)
updateChart(tick)
})
stream.on('end', () => {
console.info('行情推送结束(收盘)')
})
stream.on('error', (err) => {
console.error('行情异常: ' + err)
})
这就是 gRPC 流式的核心优势------一个请求建立流,服务端持续推,无需反复建连。比 REST 轮询省流量、低延迟,比 WebSocket 多了 Protobuf 强类型。
第四步:双向流------动态切换订阅
用户从 A 股切到 B 股,不需要断开重连,直接在同一个流里发新订阅:
typescript
const bidiStream = client.makeBidiStreamRequest(
'/stock.StockService/WatchMulti',
serializer,
deserializer,
metadata
)
// 收到行情
bidiStream.on('data', (tick) => {
updateChart(tick)
})
// 动态发订阅:先订阅 A,再切到 B
bidiStream.write(Subscribe.encode({ code: '600519' }).finish())
// 用户切换股票
setTimeout(() => {
bidiStream.write(Subscribe.encode({ code: '000858' }).finish())
}, 5000)
// 取消订阅
bidiStream.write(Unsubscribe.encode({ code: '600519' }).finish())
双向流是 gRPC 最强大的模式------一个连接管多只股票的订阅/取消,互不干扰 。这在 REST 里要维护一堆连接状态,在 gRPC 里就是几行 write。
为什么"股动力"选了 gRPC?
| 需求 | REST 轮询 | WebSocket | gRPC |
|---|---|---|---|
| 实时推送 | ❌ 轮询延迟高 | ✅ 可推送 | ✅ 流式推送 |
| 强类型契约 | ❌ JSON 无约束 | ❌ 无 | ✅ Protobuf |
| 多路复用 | ❌ 多连接 | ⚠️ 单流 | ✅ HTTP/2 |
| 动态订阅 | ❌ 难管理 | ⚠️ 手写协议 | ✅ 双向流 |
| 跨语言 | ✅ | ✅ | ✅ Go/Java/Python 互通 |
总结
"股动力"这个场景里,@ohos/grpc 解决了三件事:
- 静态信息------Unary RPC 一问一答,拉股票基本信息
- 实时行情------Server Streaming 持续推送 tick,毫秒级触达
- 动态订阅------双向流,切股票不断连,发个 write 就行
如果你也在做实时行情、在线协作、IoT 网关双向通信等场景,@ohos/grpc 的流式能力会让你彻底忘掉 REST 轮询的痛。