基于HTTP2/3的流式请求/响应如何实现?
我想很多人已经体验过GRPC提供的三种流式消息交换(Client Stream、Server Stream和Duplex Stream)模式,在.NET Core上构建的GRPC应用本质上是采用HTTP2/HTTP3协议的ASP.NET Core应用,我们当然也可以在一个普通的ASP.NET Core应用中实现这种流模式。不仅如此,HttpClient也提供了响应的支持,这篇文章通过一个简单的实例提供了相应的实现,源代码从这里下载。
一、双向流的效果
二、[服务端]流式请求/响应的读写
三、[客户端]流式响应/请求的读写
一、双向流的效果
在提供具体实现之前,我们不妨先来演示一下最终的效果。我们通过下面这段代码构建了一个简单的ASP.NET Core应用。如代码片段所示,在调用WebApplication的静态方法CreateBuilder将WebApplicationBuilder创建出来后,我们调用其扩展方法UseKestrel将默认终结点的监听协议设置为Http1AndHttp2AndHttp3,这样我们的应用将提供针对不同HTTP协议的全面支持。
var url = "http://localhost:9999";
var builder = WebApplication.CreateBuilder(args);
builder.WebHost
.UseKestrel(kestrel=> kestrel.ConfigureEndpointDefaults(listen=>listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3))
.UseUrls(url);
var app = builder.Build();
app.MapPost("/", httpContext=> HandleRequestAsync(httpContext, async (request, writer) => {
Console.WriteLine($"[Server]Receive request message: {request}");
await writer.WriteStringAsync(request);
}));
await app.StartAsync(); await SendStreamRequestAsync(url, ["foo", "bar", "baz", "qux"], reply => {
Console.WriteLine($"[Client]Receive reply message: {reply}\n");
return Task.CompletedTask;
});
我们针对根路径(/)注册了一个HTTP方法为POST的路由终结点,终结点处理器调用HandleRequestAsync来处理请求。这个方法提供一个Func<string, PipeWriter, Task>类型的参数作为处理器,该委托的第一个参数表示接收到的单条请求消息,PipeWriter用来写入响应内容。在这里我们将接收到的消息进行简单格式化后将其输出到控制台上,随之将其作为响应内容进行回写。
在应用启动之后,我们调用SendStreamRequestAsync方法以流的方式发送请求,并处理接收到的响应内容。该方法的第一个参数为请求发送的目标URL,第二个参数是一个字符串数组,我们将以流的方式逐个发送每个字符串。最后的参数是一个Func<string,Task>类型的委托,用来处理接收到的响应内容(字符串),在这里我们依然是将格式化的响应内容直接打印在控制台上。

程序启动后控制台上将出现如上图所示的输出,客户端/服务端接收内容的交错输出体现了我们希望的“双向流式”消息交换模式。我们将在后续介绍HandleRequestAsync和SendStreamRequestAsync方法的实现逻辑。
二、[服务端]流式请求/响应的读写
HandleRequestAsync方法定义如下。如代码片段所示,我们利用请求的BodyReader和响应的BodyWriter来对请求和响应内容进行读写,它们的类型分别是PipeReader和PipeWriter。在一个循环中,在利用BodyReader将请求缓冲区内容读取出来后,我们将得到的ReadOnlySequence<byte>对象作为参数调用辅助方法TryReadMessage读取单条请求消息,并调用handler参数表示的处理器进行处理。当请求内容接收完毕后,循环终止。
static async Task HandleRequestAsync(HttpContext httpContext, Func<string, PipeWriter, Task> handler)
{
var reader = httpContext.Request.BodyReader;
var writer = httpContext.Response.BodyWriter;
while (true)
{
var result = await reader.ReadAsync();
var buffer = result.Buffer;
while (TryReadMessage(ref buffer, out var message))
{
await handler(message, writer);
}
reader.AdvanceTo(buffer.Start, buffer.End);
if (result.IsCompleted)
{
break;
}
}
}
由于客户端发送的单条字符串消息长度不限,为了精准地将其读出来,我们需要在输出编码后的消息内容前添加4个字节的整数来表示消息的长度。所以在如下所示的TryReadMessage方法中,我们会先将字节长度读取出来,再据此将消息自身内容读取出来,最终通过解码得到消息字符串。
static bool TryReadMessage(ref ReadOnlySequence<byte> buffer, [NotNullWhen(true)]out string? message)
{
var reader = new SequenceReader<byte>(buffer);
if (!reader.TryReadLittleEndian(out int length))
{
message = default;
return false;
} message = Encoding.UTF8.GetString(buffer.Slice(4, length));
buffer = buffer.Slice(length + 4);
return true;
}
响应消息的写入是通过如下针对PipeWriter的WriteStringAsync扩展方法实现的,这里的PipeWriter就是响应的BodyWriter,针对“Length + Payload“的消息写入也体现在这里。
public static class Extensions
{
public static ValueTask<FlushResult> WriteStringAsync(this PipeWriter writer, string content)
{
var length = Encoding.UTF8.GetByteCount(content);
var span = writer.GetSpan(4 + length);
BitConverter.TryWriteBytes(span, length);
Encoding.UTF8.GetBytes(content, span.Slice(4));
writer.Advance(4 + length);
return writer.FlushAsync();
}
}
三、[客户端]流式响应/请求的读写
客户端利用HttpClient发送请求。针对HttpClient的请求通过一个HttpRequestMessage对象表示,其主体内容体现为一个HttpContent。流式请求的发送是通过如下这个StreamContent类型实现的,它派生于HttpContent。我们重写了SerializeToStreamAsync方法,利用自定义的StreamContentWriter将内容写入请求输出流。
public class StreamContent(StreamContentWriter writer) : HttpContent
{
private readonly StreamContentWriter _writer = writer;
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => _writer.SetOutputStream(stream).WaitAsync();
protected override bool TryComputeLength(out long length) => (length = -1) != -1;
} public class StreamContentWriter
{
private readonly TaskCompletionSource<Stream> _streamSetSource = new();
private readonly TaskCompletionSource _streamEndSource = new();
public StreamContentWriter SetOutputStream(Stream outputStream)
{
_streamSetSource.SetResult(outputStream);
return this;
} public async Task WriteAsync(string content)
{
var stream = await _streamSetSource.Task;
await PipeWriter.Create(stream).WriteStringAsync(content);
} public void Complete() => _streamEndSource.SetResult();
public Task WaitAsync() => _streamEndSource.Task;
}
StreamContentWriter提供了四个方法,SetOutputStream方法用来设置请求输出流,上面重写的SerializeToStreamAsync调用了此方法。单条字符串消息的写入实现在WriteAsync方法中,它最终调用的依然是上面提供的WriteStringAsync扩展方法。整个流式请求的过程通过一个TaskCompletionSource对象提供的Task来表示,当客户端完成所有输出后,会调用Complete方法,该方法进一步调用这个TaskCompletionSource对象的SetResult方法。由于WaitAsync方法返回TaskCompletionSource对象提供的Task,SerializeToStreamAsync方法会调用此方法等待”客户端输出流“的终结。
如下的代码片段体现了SendStreamRequestAsync方法的实现。在这里我们创建了一个表示流式请求的HttpRequestMessage对象,我们将协议版本设置为HTTP2,作为主体内容的HttpContent正式根据StreamContentWriter对象创建的StreamContent对象。
static async Task SendStreamRequestAsync(string url,string[] lines, Func<string, Task> handler)
{
using var httpClient = new HttpClient();
var writer = new StreamContentWriter();
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
Content = new StreamingWeb.StreamContent(writer)
};
var task = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
_ = Task.Run(async () =>
{
var response = await task;
var reader = PipeReader.Create(await response.Content.ReadAsStreamAsync());
while (true)
{
var result = await reader.ReadAsync();
var buffer = result.Buffer;
while (TryReadMessage(ref buffer, out var message))
{
await handler(message);
}
reader.AdvanceTo(buffer.Start, buffer.End);
if (result.IsCompleted)
{
break;
}
}
}); foreach (string line in lines)
{
await writer.WriteAsync($"{line} ({DateTimeOffset.UtcNow})");
await Task.Delay(1000);
}
writer.Complete();
}
我们将这个HttpRequestMessage作为请求利用HttpClient发送出去,实际上发送的内容最终是通过调用StreamContentWriter对象的WriteAsync方法输出的,我们每隔1秒发送一条消息。HttpClient将请求发出去之后会得到一个通过HttpResponseMessage对象表示的响应,在一个异步执行的Task中,我们根据响应流创建一个PipeReader对象,并在一个循环中调用上面定义的TryReadMessage方法逐条读取接收到的单条消息进行处理。
基于HTTP2/3的流式请求/响应如何实现?的更多相关文章
- Demo:基于 Flink SQL 构建流式应用
Flink 1.10.0 于近期刚发布,释放了许多令人激动的新特性.尤其是 Flink SQL 模块,发展速度非常快,因此本文特意从实践的角度出发,带领大家一起探索使用 Flink SQL 如何快速构 ...
- 基于grpc的流式方式实现双向通讯(python)
grpc介绍 grpc是谷歌开源的一套基于rpc实现的通讯框架(官网有更完整的定义).在搞懂grpc之前,首先要弄懂rpc是什么.下面是自己理解的rpc定义,若有不对,望指出: rpc官方称为 远程过 ...
- Serverless Streaming:毫秒级流式大文件处理探秘
摘要:本文将以图片处理的场景作为例子详细描述当前的问题以及华为云FunctionGraph函数工作流在面对该问题时采取的一系列实践. 文章作者|旧浪:华为云Serverless研发专家.平山:华为云中 ...
- TCP/IP协议,,OSI的七层参考模型,HTTP请求响应机制
一.TCP/IP协议 TCP/IP是Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是In ...
- Go gRPC教程-客户端流式RPC(四)
前言 上一篇介绍了服务端流式RPC,客户端发送请求到服务器,拿到一个流去读取返回的消息序列. 客户端读取返回的流的数据.本篇将介绍客户端流式RPC. 客户端流式RPC:与服务端流式RPC相反,客户端不 ...
- HTTP请求响应机制与响应状态码
转载来源:http://blog.csdn.net/xyw591238/article/details/51907143 HTTP协议 Internate的基本协议是TCP/IP(传输控制协议和网际协 ...
- 流式处理的新贵 Kafka Stream - Kafka设计解析(七)
原创文章,转载请务必将下面这段话置于文章开头处. 本文转发自技术世界,原文链接 http://www.jasongj.com/kafka/kafka_stream/ Kafka Stream背景 Ka ...
- 流式计算新贵Kafka Stream设计详解--转
原文地址:https://mp.weixin.qq.com/s?__biz=MzA5NzkxMzg1Nw==&mid=2653162822&idx=1&sn=8c4611436 ...
- CSS3与页面布局学习笔记(四)——页面布局大全(负边距、双飞翼、多栏、弹性、流式、瀑布流、响应式布局)
一.负边距与浮动布局 1.1.负边距 所谓的负边距就是margin取负值的情况,如margin:-100px,margin:-100%.当一个元素与另一个元素margin取负值时将拉近距离.常见的功能 ...
- Openresty的同步输出与流式响应
Openresty的同步输出与流式响应 默认情况下, ngx.say和ngx.print都是异步输出的,先来看一个例子: location /test { content_by_lua_block { ...
随机推荐
- ASIC 功能验证VTB
目标 设计流程 验证设计文档和RTL code之间的关系 RTL code(DUT) - 可以当作是一个黑盒,DUT内部是完全不可见的 白盒验证 - DUT内部RTL完全可见 灰盒验证 - DUT内部 ...
- Laravel - 419 unknown status 解决
在 head 头部加入下方代码就可解决<meta name="_token" content="{{ csrf_token() }}"/>
- TiKV 服务部署的注意事项
TiKV 服务部署的注意事项 背景 最近发现tikv总是会掉线 不知道是哪里触发了啥样子的bug. 所以想着使用systemd 管理一下, 至少在tikv宕机的时候能够拉起来服务. 二进制文件 pd- ...
- [转帖]Jmeter创建数据库(JDBC)测试-4
上一章节讲述如何建立HTTP请求测试,本章节将介绍使用MySQL数据库驱动程序进行JDBC测试.要使用该驱动程序,必须将其包含的.jar文件(例如mysql-connector-java-XXX-bi ...
- 关于信创CPU测试的一些想法和思路
关于信创CPU测试的一些想法和思路 背景 最近荷兰政府颁布了关于半导体设备出口管制的最新条例. 好像45nm以下的工艺的设备都可能收到限制. 对中国的相关厂商比如长鑫还有华虹的影响应该都比较大. 认为 ...
- [转帖]gdb调试常见命令详细总结(附示例操作)
一.简介 通过gdb调试我们可以监控程序执行的每一个细节,包括变量的值.函数的调用过程.内存中数据.线程的调度等,从而发现隐藏的错误或者低效的代码,程序的调试过程主要有:单步执行,跳入函数,跳出函数, ...
- [转帖]微服务集成skywalking实现全链路日志追踪方案
目录 1.安装部署skywalking 1.1 环境准备 1.2 部署步骤 2.微服务整合skywalking实现链路监控 2.1 下载skywalking官方版本 2.2 将微服务引入skywalk ...
- 🛠 开源即时通讯(IM)项目OpenIM源码部署指南
OpenIM的部署涉及多个组件,并支持多种方式,包括源码.Docker和Kubernetes等.这要求在确保不同部署方式之间的兼容性同时,还需有效管理各版本之间的差异.确实,这些都是复杂的问题,涉及到 ...
- 虚幻引擎 4 学习笔记 [1] :蓝图编程 Demo
虚幻引擎 4 学习笔记 [1] :蓝图编程 Demo 最近学习虚幻引擎,主要看的是 Siki 学院的课,课程链接:Unreal蓝图案例 - 基础入门 - SiKi学院|SiKi学堂 - unity ...
- dwm 美化
在之前的博客中,我们将arch linux这个系统进行了一些美化,当然也是仅仅做到能看这个地步,要说跟网上其他那些惊艳的特效对比,肯定是不如的.但是我一直秉持一个观点,美化应该适可而止,只要不是丑的你 ...