我们知道,建立在HTTP2/3之上的gRPC具有四种基本的通信模式或者消息交换模式(MEP: Message Exchange Pattern),即Unary、Server Stream、Client Stream和Bidirectional Stream。本篇文章通过4个简单的实例演示它们在.NET平台上的实现原理,源代码从这里查看。

目录
一、定义ProtoBuf消息

二、请求/响应的读写

三、Unary

四、Server Stream

五、Client Stream

六、Bidirectional Stream

一、定义ProtoBuf消息

我们选择简单的“Hello World”场景进行演示:客户端请求指定一个或者多个名字,回复以“Hello, {Name}!”。为此我们在一个ASP.NET Core应用中定义了如下两个ProtoBuf消息HelloRequest和HelloReply,生成两个同名的消息类型。

syntax = "proto3";

message HelloRequest {
string names = 1;
} message HelloReply {
string message = 1;
}

二、请求/响应的读写

gRPC框架的核心莫过于在服务端针对请求消息的读取和对响应消息的写入;以及在客户端针对请求消息的写入和对响应消息的读取。这四个核心功能被实现在如下这两个扩展方法中。如下面的代码片段所示,扩展方法WriteMessageAsync将指定的ProtoBuf消息写入PipeWriter对象中。为了确保消息能够被准确的读取,我们利用前置的四个字节存储了消息的字节数。

public static class ReadWriteExtensions
{
public static ValueTask<FlushResult> WriteMessageAsync(this PipeWriter writer, IMessage message)
{
var length = message.CalculateSize();
var span = writer.GetSpan(4+length);
BitConverter.GetBytes(length).CopyTo(span);
message.WriteTo(span.Slice(4, length));
writer.Advance(4 + length);
return writer.FlushAsync();
} public static async Task ReadAndProcessAsync<TMessage>(this PipeReader reader, MessageParser<TMessage> parser, Func<TMessage, Task> handler) where TMessage:IMessage<TMessage>
{
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;
}
} bool TryReadMessage(ref ReadOnlySequence<byte> buffer, out TMessage? message)
{
if(buffer.Length < 4)
{
message = default;
return false;
} Span<byte> lengthBytes = stackalloc byte[4];
buffer.Slice(0,4).CopyTo(lengthBytes);
var length = BinaryPrimitives.ReadInt32LittleEndian(lengthBytes);
if (buffer.Length < length + 4)
{
message = default;
return false;
} message = parser.ParseFrom(buffer.Slice(4, length));
buffer = buffer.Slice(length + 4);
return true;
}
}
}

ReadAndProcessAsync扩展方法从指定的PipeReader对象中读取指定类型的ProtoBuf消息,并利用指定处理器(一个Func<TMessage, Task>委托)对它进行处理。由于写入时指定了消息的字节数,所以我们可以将承载消息的字节“精准地”读出来,并利用指定的MessageParser<TMessage>对其进行序列化。

三、Unary

我们知道正常的gRPC开发需要将包含一个或者多个操作的服务定义在ProtoBuf文件中,并利用它生成一个基类,我们通过继承这个基类并重写操作对应方法。对于ASP.NET Core gRPC来说,服务操作对应的方法最终会转换成对应的终结点并以路由的形式进行注册。这个过程其实并不复杂,但不是本篇文章关注的终结点。本文会直接注册四个对应的路由终结点来演示四个基本的消息交换模式。

Unary调用最为简单,就是简单的Request/Reply模式。在如下的代码中,我们注册了一个针对请求路径“/unary”的路由,对应的处理方法为如下所示的HandleUnaryCallAsync。该方法直接调用上面定义的ReadAndProcessAsync扩展方法将请求消息(HelloRequest)从请求的BodyReader中读取出来,并生成一个对应的HelloReply消息予以应答。后者利用上面的WriteMessageAsync扩展方法写入响应的BodyWriter。

using GrpcService;
using System.IO.Pipelines;
using System.Net; var app = WebApplication.Create();
app.MapPost("/unary", HandleUnaryCallAsync);
await app.StartAsync(); await UnaryCallAsync(); static async Task HandleUnaryCallAsync(HttpContext httpContext)
{
var reader = httpContext.Request.BodyReader;
var write = httpContext.Response.BodyWriter;
await reader.ReadAndProcessAsync(HelloRequest.Parser, async hello =>
{
var reply = new HelloReply { Message = $"Hello, {hello.Names}!" };
await write.WriteMessageAsync(reply);
});
} static async Task UnaryCallAsync()
{
using (var httpClient = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/unary")
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
Content = new MessageContent(new HelloRequest { Names = "foobar" })
};
var reply = await httpClient.SendAsync(request);
await PipeReader.Create(await reply.Content.ReadAsStreamAsync()).ReadAndProcessAsync(HelloReply.Parser, reply =>
{
Console.WriteLine(reply.Message);
return Task.CompletedTask;
});
}
}

UnaryCallAsync模拟了客户端针对Unary服务操作的调用,具体的调用由我们熟悉的HttpClient对象完成。如代码片段所示,我们针对路由地址创建了一个HttpRequestMessage对象,并对其HTTP版本进行了设置(2.0),代表请求主体内容的HttpContent是一个MessageContent对象,具体的定义如下。MessageContent将代表ProtoBuf消息的IMessage对象作为主体内容,在重写的SerializeToStreamAsync,我们调用上面定义的WriteMessageAsync扩展方法将指定的IMessage对象写入输出流中。

public class MessageContent : HttpContent
{
private readonly IMessage _message;
public MessageContent(IMessage message) => _message = message;
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
=>await PipeWriter.Create(stream).WriteMessageAsync(_message);
protected override bool TryComputeLength(out long length)
{
length = -1;
return false;
}
}

创建的HttpRequestMessage对象利用HttpClient发送出去后,我们得到对应的HttpResponseMessage对象,并调用ReadAndProcessAsync扩展方法将主体内容读取出来并反序列化成HelloReply对象,其承载的问候消息将以如下的形式输出到控制台上。

四、Server Stream

Server Stream这种消息交换模式意味着服务端可以将内容以流的形式响应给客户端。作为模拟,客户端会携带一个名字列表(“foo,bar,baz,qux”),服务端以流的形式针对每个名字回复一个问候消息,具体的实现体现在针对请求路径“/serverstream”的路由处理方法HandleServerStreamCallAsync上。和上面一样,HandleServerStreamCallAsync方法利用我们定义的ReadAndProcessAsync方法读取作为请求的HelloRequest对象,并针对其携带的每一个名气生成一个HelloReply对象,后者最终通过我们定义的WriteMessageAsync方法予以响应。为了体验“流”的效果,我们添加了1秒的时间间隔。

using GrpcService;
using System.IO.Pipelines;
using System.Net; var app = WebApplication.Create();
app.MapPost("/unary", HandleUnaryCallAsync);
app.MapPost("/serverstream", HandleServerStreamCallAsync);
app.MapPost("/clientstream", HandleClientStreamCallAsync);
await app.StartAsync(); static async Task HandleServerStreamCallAsync(HttpContext httpContext)
{
var reader = httpContext.Request.BodyReader;
var write = httpContext.Response.BodyWriter;
await reader.ReadAndProcessAsync(HelloRequest.Parser, async hello =>
{
var names = hello.Names.Split(',');
foreach (var name in names)
{
var reply = new HelloReply { Message = $"Hello, {name}!" };
await write.WriteMessageAsync(reply);
await Task.Delay(1000);
}
});
} static async Task ServerStreamCallAsync()
{
using (var httpClient = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/serverstream")
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
Content = new MessageContent(new HelloRequest { Names = "foo,bar,baz,qux" })
};
var reply = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
await PipeReader.Create(await reply.Content.ReadAsStreamAsync()).ReadAndProcessAsync(HelloReply.Parser, reply =>
{
Console.WriteLine($"[{DateTimeOffset.Now}]{reply.Message}");
return Task.CompletedTask;
});
}
}

模拟客户端调用的ServerStreamCallAsync方法在生成一个携带多个名字的HttpRequestMessage对象,并利用HttpClient将其发送出去。由于服务端是以流的形式对请求进行响应的,所以我们在调用SendAsync方法是将HttpCompletionOption.ResponseHeadersRead枚举作为第二个参数,这样我们才能在收到响应头部之后得到代表响应消息的HttpResponseMessage对象。这样的响应将会携带4个问候消息,我们同样利用ReadAndProcessAsync方法将读取并以如下的形式输出到控制台上。

五、Client Stream

Client Stream与Server Stream正好相反,客户端会以流的形式将请求内容提交给服务端进行处理。由于我们以HttpClient来模拟客户端,所以我们只能从HttpRequestMessage上作文章。具体来说,我们需要自定义一个HttpContent类型,让它以“客户端流”的形式相对方发送内容。这个自定义的HttpContent就是如下这个ClientStreamContent<TMessage>类型。如代码片段所示,ClientStreamContent<TMessage>是对一个ClientStreamWriter<TMessage>对象的封装,客户端程序利用后者以流的形式向服务端输出TMessage对象承载的内容。对于ClientStreamWriter<TMessage>方法来说,作为输出流的Stream对象是在ClientStreamContent<TMessage>重写的SerializeToStreamAsync方法中指定的。WriteAsync方法利用我们定义的WriteMessageAsync扩展方法实现了针对ProtoBuf消息的输出。客户端通过调用Complete方法决定客户端流是否终结,ClientStreamContent<TMessage>重写的SerializeToStreamAsync通过WaitAsync进行等待。

public class ClientStreamContent<TMessage> : HttpContent where TMessage:IMessage<TMessage>
{
private readonly ClientStreamWriter<TMessage> _writer;
public ClientStreamContent(ClientStreamWriter<TMessage> writer)=> _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 ClientStreamWriter<TMessage> where TMessage: IMessage<TMessage>
{
private readonly TaskCompletionSource<Stream> _streamSetSource = new();
private readonly TaskCompletionSource _streamEndSuource = new(); public ClientStreamWriter<TMessage> SetOutputStream(Stream outputStream)
{
_streamSetSource.SetResult(outputStream);
return this;
} public async Task WriteAsync(TMessage message)
{
var stream = await _streamSetSource.Task;
await PipeWriter.Create(stream).WriteMessageAsync(message);
} public void Complete()=> _streamEndSuource.SetResult();
public Task WaitAsync() => _streamEndSuource.Task;
}

针对Client Stream的模拟体现在针对路径“/clientstream”的路由处理方法HandleClientStreamCallAsync。这个方法没有什么特别之处,它进行时调用ReadAndProcessAsync方法将HelloRequest消息读取出来,并将生成的问候语直接输出到本地(服务端)控制台上而已。

using GrpcService;
using System.IO.Pipelines;
using System.Net; var app = WebApplication.Create();
app.MapPost("/unary", HandleUnaryCallAsync);
app.MapPost("/serverstream", HandleServerStreamCallAsync);
app.MapPost("/clientstream", HandleClientStreamCallAsync);
await app.StartAsync(); await ClientStreamCallAsync(); static async Task HandleClientStreamCallAsync(HttpContext httpContext)
{
var reader = httpContext.Request.BodyReader;
var write = httpContext.Response.BodyWriter;
await reader.ReadAndProcessAsync(HelloRequest.Parser, async hello =>
{
var names = hello.Names.Split(',');
foreach (var name in names)
{
Console.WriteLine($"[{DateTimeOffset.Now}]Hello, {name}!");
await Task.Delay(1000);
}
});
} static async Task ClientStreamCallAsync()
{
using (var httpClient = new HttpClient())
{
var writer = new ClientStreamWriter<HelloRequest>();
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/clientstream")
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
Content = new ClientStreamContent<HelloRequest>(writer)
};
_ = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
foreach (var name in new string[] {"foo","bar","baz","qux" })
{
await writer.WriteAsync(new HelloRequest { Names = name});
await Task.Delay(1000);
}
writer.Complete();
}
}

在用于模拟Client Stream调用的ClientStreamCallAsync方法中,我们首先创建了一个ClientStreamWriter<HelloRequest>对象,并利用它创建了对应的ClientStreamContent<HelloRequest>对象,后者将作为HttpRequestMessage消息的主体内容。在调用HttpClient的SendAsync方法后,我们并没有作任何等待(否则程序将卡在这里),而是利用ClientStreamWriter<HelloRequest>对象以流的形式发送了四个请求。服务端在接收到每个请求后,会将对应的问候语以如下的形式输出到控制台上。

六、Bidirectional Stream

Bidirectional Stream将连接作为真正的“双工通道”。这次我们不再注册额外的路由,而是直接利用前面模拟Unary的路由终结点来演示双向通信。在如下所示的客户端模拟方法BidirectionalStreamCallAsync中,我们采用上面的方式以流的形式发送了4个HelloRequest。

using GrpcService;
using System.IO.Pipelines;
using System.Net; var app = WebApplication.Create();
app.MapPost("/unary", HandleUnaryCallAsync);
app.MapPost("/serverstream", HandleServerStreamCallAsync);
app.MapPost("/clientstream", HandleClientStreamCallAsync);
await app.StartAsync(); await BidirectionalStreamCallAsync(); static async Task BidirectionalStreamCallAsync()
{
using (var httpClient = new HttpClient())
{
var writer = new ClientStreamWriter<HelloRequest>();
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/unary")
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
Content = new ClientStreamContent<HelloRequest>(writer)
};
var task = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
_ = Task.Run(async () =>
{
var response = await task;
await PipeReader.Create(await response.Content.ReadAsStreamAsync()).ReadAndProcessAsync(HelloReply.Parser, reply =>
{
Console.WriteLine($"[{DateTimeOffset.Now}]{reply.Message}");
return Task.CompletedTask;
});
}); foreach (var name in new string[] { "foo", "bar", "baz", "qux" })
{
await writer.WriteAsync(new HelloRequest { Names = name });
await Task.Delay(1000);
}
writer.Complete();
}
}

于此同时,我们在得到表示响应消息的HttpResponseMessage后,调用ReadAndProcessAsync方法将作为响应的问候语以如下的方式输出到控制台上。

用最少的代码模拟gRPC四种消息交换模式的更多相关文章

  1. gRPC四种模式、认证和授权实战演示,必赞~~~

    前言 上一篇对gRPC进行简单介绍,并通过示例体验了一下开发过程.接下来说说实际开发常用功能,如:gRPC的四种模式.gRPC集成JWT做认证和授权等. 正文 1. gRPC四种模式服务 以下案例演示 ...

  2. Activity有四种加载模式(转)

    Activity有四种加载模式: standard singleTop singleTask singleInstance 在多Activity开发中,有可能是自己应用之间的Activity跳转,或者 ...

  3. 区分Activity的四种加载模式

    在多Activity开发中,有可能是自己应用之间的Activity跳转,或者夹带其他应用的可复用Activity.可能会希望跳转到原来某个Activity实例,而不是产生大量重复的Activity. ...

  4. Android学习记录(8)—Activity的四种加载模式及有关Activity横竖屏切换的问题

    Activity有四种加载模式:standard(默认), singleTop, singleTask和 singleInstance.以下逐一举例说明他们的区别: standard:Activity ...

  5. 区分Activity的四种加载模式【转载】

    此文为转载,文章来源:http://marshal.easymorse.com/archives/2950 文章作者:   Marshal's Blog 参考文章:http://blog.csdn.n ...

  6. 四种软件开发模式:tdd、bdd、atdd和ddd的概念

    看一些文章会看到TDD开发模式,搜索后发现有主流四种软件开发模式,这里对它们的概念做下笔记. TDD:测试驱动开发(Test-Driven Development) 测试驱动开发是敏捷开发中的一项核心 ...

  7. 活动 Activity 四种加载模式

    singleTop要求如果创建intent的时候栈顶已经有要创建的Activity的实例,则将intent发送给该实例,而不发送给新的实例.(注意是栈顶,不在栈顶照样创建新实例!) singleTas ...

  8. 【Android进阶】Activity的四种加载模式

    Activity的四种加载模式: 1.standard :系统的默认模式,一次跳转即会生成一个新的实例.假设有一个activity命名为Act1, 执行语句:startActivity(new Int ...

  9. android中的LaunchMode详解----四种加载模式

    Activity有四种加载模式: standard singleTop singleTask singleInstance 配置加载模式的位置在AndroidManifest.xml文件中activi ...

  10. activity的四种加载模式介绍

      四种加载模式的介绍: a) Standard : 系统默认模式,一次跳转即会生成一个新的实例:    b) SingleTop : 和 standard 类似,唯一的区别就是当跳转的对象是位于栈顶 ...

随机推荐

  1. e1000e网卡驱动在麒麟3.2.5上编译安装

    一.清空原驱动 因为系统安装完毕后系统中自带了e1000e的网卡驱动,会影响后面自行编译的驱动 所以先用find命令找出并删除掉所有关于e1000e的驱动文件 find / -name "* ...

  2. 开源即时通讯GGTalk 8.0发布,增加Linux客户端,支持在统信UOS、银河麒麟上运行!

    GGTalk在2021年推出7.0后,经过一年多时间的开发,终于推出8.0版本,实现了Linux客户端. 这几年,信创国产化的势头越来越猛,政府事企业单位都在逐步转向使用国产OS.国产CPU.国产数据 ...

  3. 阿色全息脑图,及制作软件AHMM

    阿色全息脑图 AHMM 全息脑图是按照大系统观原理开发的新型思维工具,用于升维思考. 让您以系统的观点看待世界,专注系统的结构信息--全息,抓住事物的本质,透过表象和数据发现规律. 世间每项事物都是一 ...

  4. 快Key:按一下鼠标【滚轮】,帮你自动填写用户名密码,快速登录,可制作U盘随身(开源免费-附安装文件和源代码)

    * 代码以本文所附下载文件包为准,安装文件和源文件包均在本文尾部可下载. * 快Key及本文所有内容仅供交流使用,使用者责任自负,由快Key对使用者及其相关人员或组织造成的任何损失均由使用者自负,与本 ...

  5. 思维导图学《On Java》基础卷 + 进阶卷

    说明 目录 思维导图 导读 第 1 章 什么是对象 第 3 章 一切都是对象 第 6 章 初始化和清理 第 7 章 实现隐藏 第 8 章 复用 第 9 章 多态 第 10 章 接口 第 11 章 内部 ...

  6. MySQL配置不当导致Sonarqube出错的一次经历:Packet for query is too large (16990374 > 13421568)

    公司里部署了Jenkins + Sonarqube对项目代码进行构建和代码质量扫描. 某个大型项目报告项目构建失败.进jenkins看,该项目构建日志中的报错信息是这样的: 通过错误堆栈中的信息可以判 ...

  7. PAT (Basic Level) Practice 1018 锤子剪刀布 分数 20

    大家应该都会玩"锤子剪刀布"的游戏:两人同时给出手势,胜负规则如图所示: 现给出两人的交锋记录,请统计双方的胜.平.负次数,并且给出双方分别出什么手势的胜算最大. 输入格式: 输入 ...

  8. python合并多个excel

    前言 1.工作中,经常需要合并多个Excel文件.如果文件数量比较多,则工作量大,易出错,此时,可以使用Python来快速的完成合并. 2.使用方法:将需要合并的多个Excel文件放到同一个文件夹下, ...

  9. 简析 Linux 的 CPU 时间

    从 CPU 时间说起... 下面这个是 top 命令的界面,相信大家应该都不陌生. top - 19:01:38 up 91 days, 23:06, 1 user, load average: 0. ...

  10. 监控CPU状况并发送邮件shell脚本

    #!/bin/bash #监控CPU状况并发送邮件 DATE=$(date +%y%m%d) TEMP=$(mktemp tmp.XXX.txt) cat /proc/cpuinfo >$TEM ...