ai时代下的RPC传输——StreamObserver

什么是StreamObserver

StreamObserver 是现代 RPC 框架中一个非常关键的设计,它通过引入 异步​ 和 流式​ 的能力,解决了传统 RPC 在特定场景下的核心痛点。

我们先通过一个表格来对比传统 RPC 与基于 StreamObserver 的流式 RPC 的区别。

特性维度 传统 RPC StreamObserver
通信模式 单一请求-响应(同步阻塞) 支持多种流式模式(异步非阻塞)
数据交换 一次性完成 可分多次、持续进行
资源占用 高并发时线程资源消耗大 高并发下资源占用更低,吞吐量更高
实时性 服务端无法主动推送 支持服务端主动、实时推送
典型场景 简单交互 大模型输出、数据流处理、实时通信等

为什么需要 StreamObserver

传统 RPC 类似一次性的"问答",而 StreamObserver 则像是开启了一次可以持续交谈的"电话"。它主要解决了以下问题:
数据边界不确定或数据量巨大 :当需要传输的数据无法或不应被放入单个请求/响应包时(例如一个大文件或一个很长的列表),流式处理允许将数据分成多个小块(chunk)进行连续传输,避免内存溢出和长时间等待。
需要实时或按序处理数据流 :对于数据本身没有确定边界、但需要按照严格发送顺序处理的场景(如实时日志流、股票价格推送),流式通信能保证消息的顺序性。
需要全双工异步通信:在双向流模式下,客户端和服务端可以同时独立地发送和接收数据,无需等待对方响应,极大提升了通信效率和灵活性

想象一下向一个大语言模型提问一个复杂问题。模型的生成过程不是瞬间完成的,而是逐个 token(词元)地输出。如果使用传统 RPC,你必须要等到整个回答(可能长达数千字)全部生成完毕,才能一次性收到完整的响应。这会导致:
极差的用户体验 :用户面对空白屏幕等待很长时间。
巨大的内存压力 :客户端和服务端都需要为完整的响应内容分配内存。
缺乏中断机制 :如果生成的内容不符合预期,用户无法在中间过程进行干预或停止。

而使用 StreamObserver 的服务端流模式,这个过程就变得高效和优雅:

  1. 客户端发送一个包含问题的请求。
  2. 服务端(大模型)开始生成,每生成一小段内容(如一个句子或几个词),就立即通过 responseObserver.onNext(...)方法"推送"给客户端。
  3. 客户端通过预先实现的 onNext方法实时接收到每一个数据块,并立即展示给用户,实现"打字机"式的逐字输出效果。
  4. 当全部内容生成完毕后,服务端调用 onCompleted来优雅结束流。

这种方式实现了低延迟、低内存占用、可交互的优秀体验,这是传统 RPC 根本无法做到的。

StreamObserver是怎么使用的

  1. 建立连接

gRPC 使用 ManagedChannel来管理客户端和服务端的连接。这条物理连接是长连接 ,一旦建立,后续的所有流式通信(即使是多个独立的 Stream)都会复用这个通道,这是高性能的关键。

客户端:

java 复制代码
// 创建通道(长连接)
ManagedChannel channel = ManagedChannelBuilder
    .forAddress("localhost", 8080)
    .usePlaintext() // 简化示例,生产环境用 TLS
    .build();

// 创建异步存根(Stub),用于发起流式调用
MyServiceGrpc.MyServiceStub asyncStub = MyServiceGrpc.newStub(channel);
  1. 发送与接收消息

这是 双向流式 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()优雅地结束这个流。

相关推荐
proware2 小时前
qt与egl的那些事儿
qt·rockchip·3588·egl
xmRao2 小时前
Qt 结合 SDL2 实现 PCM 音频文件播放
开发语言·qt·pcm
枫叶丹42 小时前
【Qt开发】Qt系统(九)-> Qt TCP Socket
c语言·开发语言·网络·c++·qt·tcp/ip
Tandy12356_12 小时前
手写TCP/IP协议栈——HTTP协议实现(完结篇)
c语言·网络·网络协议·tcp/ip·计算机网络·http
IOT-Power12 小时前
QT构建构建DataBus总线
开发语言·qt
yangSnowy12 小时前
webSocket 通信详解
网络·websocket·网络协议
上海云盾安全满满14 小时前
如何隐藏业务的IP
网络·网络协议·tcp/ip
SNAKEpc1213814 小时前
深入理解PyQtGraph核心组件交互
python·qt·pyqt
Henry Zhu12314 小时前
Qt Model/View架构详解(四):高级特性
开发语言·qt·架构