什么是StreamObserver
StreamObserver 是现代 RPC 框架中一个非常关键的设计,它通过引入 异步 和 流式 的能力,解决了传统 RPC 在特定场景下的核心痛点。
我们先通过一个表格来对比传统 RPC 与基于 StreamObserver 的流式 RPC 的区别。
| 特性维度 | 传统 RPC | StreamObserver |
|---|---|---|
| 通信模式 | 单一请求-响应(同步阻塞) | 支持多种流式模式(异步非阻塞) |
| 数据交换 | 一次性完成 | 可分多次、持续进行 |
| 资源占用 | 高并发时线程资源消耗大 | 高并发下资源占用更低,吞吐量更高 |
| 实时性 | 服务端无法主动推送 | 支持服务端主动、实时推送 |
| 典型场景 | 简单交互 | 大模型输出、数据流处理、实时通信等 |
为什么需要 StreamObserver
传统 RPC 类似一次性的"问答",而 StreamObserver 则像是开启了一次可以持续交谈的"电话"。它主要解决了以下问题:
数据边界不确定或数据量巨大 :当需要传输的数据无法或不应被放入单个请求/响应包时(例如一个大文件或一个很长的列表),流式处理允许将数据分成多个小块(chunk)进行连续传输,避免内存溢出和长时间等待。
需要实时或按序处理数据流 :对于数据本身没有确定边界、但需要按照严格发送顺序处理的场景(如实时日志流、股票价格推送),流式通信能保证消息的顺序性。
需要全双工异步通信:在双向流模式下,客户端和服务端可以同时独立地发送和接收数据,无需等待对方响应,极大提升了通信效率和灵活性
想象一下向一个大语言模型提问一个复杂问题。模型的生成过程不是瞬间完成的,而是逐个 token(词元)地输出。如果使用传统 RPC,你必须要等到整个回答(可能长达数千字)全部生成完毕,才能一次性收到完整的响应。这会导致:
极差的用户体验 :用户面对空白屏幕等待很长时间。
巨大的内存压力 :客户端和服务端都需要为完整的响应内容分配内存。
缺乏中断机制 :如果生成的内容不符合预期,用户无法在中间过程进行干预或停止。
而使用 StreamObserver 的服务端流模式,这个过程就变得高效和优雅:
- 客户端发送一个包含问题的请求。
- 服务端(大模型)开始生成,每生成一小段内容(如一个句子或几个词),就立即通过 responseObserver.onNext(...)方法"推送"给客户端。
- 客户端通过预先实现的 onNext方法实时接收到每一个数据块,并立即展示给用户,实现"打字机"式的逐字输出效果。
- 当全部内容生成完毕后,服务端调用 onCompleted来优雅结束流。
这种方式实现了低延迟、低内存占用、可交互的优秀体验,这是传统 RPC 根本无法做到的。
StreamObserver是怎么使用的

- 建立连接
gRPC 使用 ManagedChannel来管理客户端和服务端的连接。这条物理连接是长连接 ,一旦建立,后续的所有流式通信(即使是多个独立的 Stream)都会复用这个通道,这是高性能的关键。
客户端:
java
// 创建通道(长连接)
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 8080)
.usePlaintext() // 简化示例,生产环境用 TLS
.build();
// 创建异步存根(Stub),用于发起流式调用
MyServiceGrpc.MyServiceStub asyncStub = MyServiceGrpc.newStub(channel);
- 发送与接收消息
这是 双向流式 RPC(Bidirectional Streaming) 的典型模式。客户端和服务端各自持有用于发送消息的 StreamObserver,并通过实现另一个 StreamObserver来接收对方的消息。
服务端:
java
// 继承自生成的 ServiceImplBase
public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase {
@Override
public StreamObserver<MyRequest> biDirectionalStream(StreamObserver<MyResponse> responseObserver) {
// 这个 responseObserver 参数,是服务端用来向客户端"发送"消息的出口。
// 该方法返回一个 StreamObserver,是服务端用来"接收"客户端消息的入口。
return new StreamObserver<MyRequest>() {
@Override
public void onNext(MyRequest request) {
// 处理从客户端收到的单个消息
String clientData = request.getData();
// 业务处理...
MyResponse response = MyResponse.newBuilder().setResult("Processed: " + clientData).build();
// 处理完后,可以立即用 responseObserver 发送响应回客户端
responseObserver.onNext(response);
}
@Override
public void onError(Throwable t) {
// 处理错误
System.err.println("Error: " + t.getMessage());
}
@Override
public void onCompleted() {
// 客户端告知数据已发送完毕
System.out.println("Client finished sending.");
// 服务端也完成响应发送
responseObserver.onCompleted();
}
};
}
}
客户端:
java
// 1. 准备一个"响应观察者",用来处理服务端发来的消息。
StreamObserver<MyResponse> responseObserver = new StreamObserver<MyResponse>() {
@Override
public void onNext(MyResponse response) {
// 处理从服务端收到的单个响应
System.out.println("Received from server: " + response.getResult());
}
@Override
public void onError(Throwable t) {
// 处理错误
System.err.println("RPC Error: " + t.getMessage());
}
@Override
public void onCompleted() {
// 服务端告知响应已全部发送完毕
System.out.println("Server finished sending.");
}
};
// 2. 发起 RPC 调用,并获得一个"请求观察者",用来向服务端发送消息。
StreamObserver<MyRequest> requestObserver = asyncStub.biDirectionalStream(responseObserver);
// 3. 使用 requestObserver 发送多条消息
try {
for (int i = 0; i < 5; i++) {
MyRequest request = MyRequest.newBuilder().setData("Message " + i).build();
requestObserver.onNext(request); // 发送一条消息
}
} catch (Exception e) {
requestObserver.onError(e); // 发送出错,通知对方
return;
}
// 4. 客户端告知服务端:我所有的消息都发完了。
requestObserver.onCompleted();
StreamObserver底层是怎么实现的
使用 Netty 这个高性能的 NIO(非阻塞 I/O)框架来具体实现 HTTP/2 协议栈。Netty 负责管理 TCP 连接、处理成千上万的并发请求而无需为每个请求创建线程(这是 NIO 的核心),以及处理所有底层的网络 I/O 操作。

流式rpc对于普通rpc的区分
普通RPC :可以看作是在HTTP/2连接上开启一个独立的"流(Stream)",发送一个请求帧,然后等待一个响应帧,之后这个流就关闭了。虽然底层的TCP连接可能被复用,但每次调用都是一个完整的、独立的流生命周期。
流式RPC:则是在单个"流"上持续进行双向对话。客户端或服务端可以不断地通过 onNext()方法向这个流中写入多个数据帧(DATA Frame),接收方则会通过回调函数持续读取。整个过程在同一个流上下文中进行,直到一方调用 onCompleted()或 onError()优雅地结束这个流。