服务器通信:gRPC与GraphQL的比较

服务器通信:gRPC与GraphQL的比较

gRPC是由Google开发的一个高性能、通用的开源RPC框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。

GraphQL既是一种用于API的查询语言,且GraphQL对API中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让API更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

两者看起来并用途不相同,但其实在通信场景中很多开发者面临如何选择的问题。本文笔者带领大家从实用的角度去一一剖析gRPC与GraphQL的取舍之道!

本文主要介绍使用gRPC和GraphQL的时机,先说结论:推荐大家在客户端与服务器之间的通信场景中使用GraphQL,在服务器与服务器之间的通信场景使用gRPC。此外,文末会介绍关于例外情况的处理方式。

模拟gRPC四种消息交换模式

我们知道,建立在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";

messageHelloRequest{
stringnames=1;
}

messageHelloReply{
stringmessage=1;
}

二、请求/响应的读写

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

publicstaticclassReadWriteExtensions
{
publicstaticValueTask<FlushResult>WriteMessageAsync(
thisPipeWriterwriter,
IMessagemessage)
{
varlength=message.CalculateSize();
varspan=writer.GetSpan(4+length);
BitConverter.GetBytes(length).CopyTo(span);
message.WriteTo(span.Slice(4,length));
writer.Advance(4+length);
returnwriter.FlushAsync();
}

publicstaticasyncTaskReadAndProcessAsync<TMessage>(
thisPipeReaderreader,
MessageParser<TMessage>parser,
Func<TMessage,Task>handler)
whereTMessage:IMessage<TMessage>
{
while(true)
{
varresult=awaitreader.ReadAsync();
varbuffer=result.Buffer;
while(TryReadMessage(
refbuffer,outvarmessage))
{
awaithandler(message!);
}
reader.AdvanceTo(buffer.Start,buffer.End);
if(result.IsCompleted)
{
break;
}
}


boolTryReadMessage(
refReadOnlySequence<byte>buffer,
outTMessage?message)
{
if(buffer.Length<4)
{
message=default;
returnfalse;
}

Span<byte>lengthBytes=stackallocbyte[4];
buffer.Slice(0,4).CopyTo(lengthBytes);
varlength=BinaryPrimitives
.ReadInt32LittleEndian(lengthBytes);
if(buffer.Length<length+4)
{
message=default;
returnfalse;
}

message=parser.ParseFrom(
buffer.Slice(4,length));
buffer=buffer.Slice(length+4);
returntrue;
}
}
}

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