鉴于内容过多,先上太长不看版:

  • grpc 就是请求流&响应流特殊一点的 Http 请求,性能和 WebAPI 比起来只快在 Protobuf 上;

附上完整试验代码:GrpcWithOutSDK.zip

另附小Demo,基于 ControllerHttpClient 的在线聊天室:ChatRoomOnController.zip


本文内容有点长,涉及较多基础知识点,某些结论可能直接得出,没有上下文,限于篇幅,不会在本文内详细描述,如有疑惑请友好交流或尝试搜索互联网。

本文仅代表个人试验结果和观点,可能会有偏颇,请自行判断。


一、背景

个人经常在网上看到 grpc高性能 字眼的文章;有幸也面试过一些同僚,问及 grpc 对比 WebAPI,答案都是更快、性能更高;至于能快多少,答案就各种各样了,几倍到几十倍的回答都有,但大概是统一的:“grpc 要快得多”。那么具体快在哪里呢?回答我就觉得不那么准确了。

现在我们就来探索一下 grpcWebAPI 的差别是什么? grpc 快在哪里?

二、验证请求模型

就是个常规的 asp.net core 使用 grpc 的步骤

创建服务端

  • 建立一个 asp.net core grpc 项目

  • 添加一个测试的 reverse.proto 用于测试 grpc 的几种通讯模式,并为其生成服务端
  1. syntax = "proto3";
  2. option csharp_namespace = "GrpcWithOutSDK";
  3. package reverse;
  4. service Reverse {
  5. rpc Simple (Request) returns (Reply);
  6. rpc ClientSide (stream Request) returns (Reply);
  7. rpc ServerSide (Request) returns (stream Reply);
  8. rpc Bidirectional (stream Request) returns (stream Reply);
  9. }
  10. message Request {
  11. string message = 1;
  12. }
  13. message Reply {
  14. string message = 1;
  15. }
  • 新建 ReverseService.cs 实现具体的方法逻辑
  1. public class ReverseService : Reverse.ReverseBase
  2. {
  3. private readonly ILogger<ReverseService> _logger;
  4. public ReverseService(ILogger<ReverseService> logger)
  5. {
  6. _logger = logger;
  7. }
  8. private static Reply CreateReplay(Request request)
  9. {
  10. return new Reply
  11. {
  12. Message = new string(request.Message.Reverse().ToArray())
  13. };
  14. }
  15. private void DisplayReceivedMessage(Request request, [CallerMemberName] string? methodName = null)
  16. {
  17. _logger.LogInformation($"{methodName} Received: {request.Message}");
  18. }
  19. public override async Task Bidirectional(IAsyncStreamReader<Request> requestStream, IServerStreamWriter<Reply> responseStream, ServerCallContext context)
  20. {
  21. while (await requestStream.MoveNext())
  22. {
  23. DisplayReceivedMessage(requestStream.Current);
  24. await responseStream.WriteAsync(CreateReplay(requestStream.Current));
  25. }
  26. }
  27. public override async Task<Reply> ClientSide(IAsyncStreamReader<Request> requestStream, ServerCallContext context)
  28. {
  29. var total = 0;
  30. while (await requestStream.MoveNext())
  31. {
  32. total++;
  33. DisplayReceivedMessage(requestStream.Current);
  34. }
  35. return new Reply
  36. {
  37. Message = $"{nameof(ServerSide)} Received Over. Total: {total}"
  38. };
  39. }
  40. public override async Task ServerSide(Request request, IServerStreamWriter<Reply> responseStream, ServerCallContext context)
  41. {
  42. DisplayReceivedMessage(request);
  43. for (int i = 0; i < 5; i++)
  44. {
  45. await responseStream.WriteAsync(CreateReplay(request));
  46. }
  47. }
  48. public override Task<Reply> Simple(Request request, ServerCallContext context)
  49. {
  50. return Task.FromResult(CreateReplay(request));
  51. }
  52. }

最后记得 app.MapGrpcService<ReverseService>();

创建客户端

  • 新建一个控制台项目,并添加Google.ProtobufGrpc.Net.ClientGrpc.Tools这几个包的引用
  • 引用之前写好的 reverse.proto 并为其生成客户端
  • 写几个用于测试各种通讯模式的方法
  1. private static async Task Bidirectional(Reverse.ReverseClient client)
  2. {
  3. var stream = client.Bidirectional();
  4. var sendTask = Task.Run(async () =>
  5. {
  6. for (int i = 0; i < 10; i++)
  7. {
  8. await stream.RequestStream.WriteAsync(new() { Message = $"{nameof(Bidirectional)}-{i}" });
  9. }
  10. await stream.RequestStream.CompleteAsync();
  11. });
  12. var receiveTask = Task.Run(async () =>
  13. {
  14. while (await stream.ResponseStream.MoveNext(default))
  15. {
  16. DisplayReceivedMessage(stream.ResponseStream.Current);
  17. }
  18. });
  19. await Task.WhenAll(sendTask, receiveTask);
  20. }
  21. private static async Task ClientSide(Reverse.ReverseClient client)
  22. {
  23. var stream = client.ClientSide();
  24. for (int i = 0; i < 5; i++)
  25. {
  26. await stream.RequestStream.WriteAsync(new() { Message = $"{nameof(ClientSide)}-{i}" });
  27. }
  28. await stream.RequestStream.CompleteAsync();
  29. var reply = await stream.ResponseAsync;
  30. DisplayReceivedMessage(reply);
  31. }
  32. private static async Task Sample(Reverse.ReverseClient client)
  33. {
  34. var reply = await client.SimpleAsync(new() { Message = nameof(Sample) });
  35. DisplayReceivedMessage(reply);
  36. }
  37. private static async Task ServerSide(Reverse.ReverseClient client)
  38. {
  39. var stream = client.ServerSide(new() { Message = nameof(ServerSide) });
  40. while (await stream.ResponseStream.MoveNext(default))
  41. {
  42. DisplayReceivedMessage(stream.ResponseStream.Current);
  43. }
  44. }
  • 测试代码
  1. const string Host = "http://localhost:5035";
  2. var channel = GrpcChannel.ForAddress(Host);
  3. var grpcClient = new Reverse.ReverseClient(channel);
  4. await Sample(grpcClient);
  5. await ClientSide(grpcClient);
  6. await ServerSide(grpcClient);
  7. await Bidirectional(grpcClient);

进行验证

  • 将服务端的 Microsoft.AspNetCore 日志等级调整为 Information 以打印请求日志
  • 运行服务端与客户端
  • 不出意外的话服务端会看到如下输出(为便于观察,已按方法进行分段,不重要的信息已省略)
  1. info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
  2. Request starting HTTP/2 POST http://localhost:5035/reverse.Reverse/Simple application/grpc -
  3. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
  4. Executing endpoint 'gRPC - /reverse.Reverse/Simple'
  5. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
  6. Executed endpoint 'gRPC - /reverse.Reverse/Simple'
  7. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
  8. Request finished HTTP/2 POST http://localhost:5035/reverse.Reverse/Simple application/grpc - - 200 - application/grpc 99.1956ms
  1. info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
  2. Request starting HTTP/2 POST http://localhost:5035/reverse.Reverse/ClientSide application/grpc -
  3. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
  4. Executing endpoint 'gRPC - /reverse.Reverse/ClientSide'
  5. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
  6. Executed endpoint 'gRPC - /reverse.Reverse/ClientSide'
  7. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
  8. Request finished HTTP/2 POST http://localhost:5035/reverse.Reverse/ClientSide application/grpc - - 200 - application/grpc 21.9445ms
  1. info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
  2. Request starting HTTP/2 POST http://localhost:5035/reverse.Reverse/ServerSide application/grpc -
  3. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
  4. Executing endpoint 'gRPC - /reverse.Reverse/ServerSide'
  5. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
  6. Executed endpoint 'gRPC - /reverse.Reverse/ServerSide'
  7. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
  8. Request finished HTTP/2 POST http://localhost:5035/reverse.Reverse/ServerSide application/grpc - - 200 - application/grpc 12.7054ms
  1. info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
  2. Request starting HTTP/2 POST http://localhost:5035/reverse.Reverse/Bidirectional application/grpc -
  3. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
  4. Executing endpoint 'gRPC - /reverse.Reverse/Bidirectional'
  5. info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
  6. Executed endpoint 'gRPC - /reverse.Reverse/Bidirectional'
  7. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
  8. Request finished HTTP/2 POST http://localhost:5035/reverse.Reverse/Bidirectional application/grpc - - 200 - application/grpc 41.2414ms

对日志进行一些分析我们可以发现:

  • 所有类型的 grpc 通讯模式执行逻辑都是相同的,都是一次完整的http请求周期;
  • 请求的协议使用的是 HTTP/2
  • 方法都为 POST
  • 所有grpc方法都映射到了对应的终结点 /{package名}.{service名}/{方法名}
  • 请求&响应的 ContentType 都为 application/grpc

三、进一步验证请求模型

如果我们上一步的分析是对的,那么数据只能承载在 请求流 & 响应流 中,我们可以尝试获取流中的数据,进一步分析具体细节;

dump请求&响应数据

借助 asp.net core 的中间件,我们可以比较容易的进行 请求流 & 响应流 的内容 dump

请求流 是只读的,响应流 是只写的,我们需要两个代理流替换原有的流,进行数据dump,将数据保存到 MemoryStream 中,以便我们观察;

这两个流分别为 ReadCacheProxyStream.csWriteCacheProxyStream.cs,直接上代码:

  1. public class ReadCacheProxyStream : Stream
  2. {
  3. private readonly Stream _innerStream;
  4. public MemoryStream CachedStream { get; } = new MemoryStream(1024);
  5. public override bool CanRead => _innerStream.CanRead;
  6. public override bool CanSeek => false;
  7. public override bool CanWrite => false;
  8. public override long Length => _innerStream.Length;
  9. public override long Position { get => _innerStream.Length; set => throw new NotSupportedException(); }
  10. public ReadCacheProxyStream(Stream innerStream)
  11. {
  12. _innerStream = innerStream;
  13. }
  14. public override void Flush() => throw new NotSupportedException();
  15. public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);
  16. public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
  17. public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
  18. {
  19. var len = await _innerStream.ReadAsync(buffer, cancellationToken);
  20. if (len > 0)
  21. {
  22. CachedStream.Write(buffer.Span.Slice(0, len));
  23. }
  24. return len;
  25. }
  26. public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
  27. public override void SetLength(long value) => throw new NotSupportedException();
  28. public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
  29. }
  30. public class WriteCacheProxyStream : Stream
  31. {
  32. private readonly Stream _innerStream;
  33. public MemoryStream CachedStream { get; } = new MemoryStream(1024);
  34. public override bool CanRead => false;
  35. public override bool CanSeek => false;
  36. public override bool CanWrite => _innerStream.CanWrite;
  37. public override long Length => _innerStream.Length;
  38. public override long Position { get => _innerStream.Length; set => throw new NotSupportedException(); }
  39. public WriteCacheProxyStream(Stream innerStream)
  40. {
  41. _innerStream = innerStream;
  42. }
  43. public override void Flush() => throw new NotSupportedException();
  44. public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);
  45. public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
  46. public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
  47. public override void SetLength(long value) => throw new NotSupportedException();
  48. public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
  49. public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
  50. {
  51. await _innerStream.WriteAsync(buffer, cancellationToken);
  52. CachedStream.Write(buffer.Span);
  53. }
  54. }
  • 在请求管道中替换流

    将如下中间件添加到请求管道的最开始
  1. app.Use(async (context, next) =>
  2. {
  3. var originRequestBody = context.Request.Body;
  4. var originResponseBody = context.Response.Body;
  5. var requestCacheStream = new ReadCacheProxyStream(originRequestBody);
  6. var responseCacheStream = new WriteCacheProxyStream(originResponseBody);
  7. context.Request.Body = requestCacheStream;
  8. context.Response.Body = responseCacheStream;
  9. try
  10. {
  11. await next();
  12. }
  13. finally
  14. {
  15. await context.Response.CompleteAsync();
  16. //要不要还回去不在这里进行讨论了
  17. context.Request.Body = originRequestBody;
  18. context.Response.Body = originResponseBody;
  19. var requestData = requestCacheStream.CachedStream.ToArray();
  20. var responseData = requestCacheStream.CachedStream.ToArray();
  21. }
  22. });
  • 接下来在 finally 块的最后打上断点,然后运行服务端和客户端,即可在中间件中通过 requestDataresponseData 观察数据交互

分析数据结构


理论上我们可以直接使用 Protobuf 进行解析,不过这里我们目的是为了手动实现一个超级简单的编码器。。。


客户端执行 Sample 方法,并在服务端获取 requestDataresponseData

分析requestData

这个样子太不直观了,由于我们的消息定义 Request 只有一个 string 类型的字段,那么如果之前猜测正确,这个数据里面必定有对应字符串。我们直接尝试拿来看看:

果然有对应的数据 Sample ,我们尝试去掉多余的数据看看:

那么前7个byte是干什么的呢,我们改一下请求的消息内容,将 Sample 修改为 Sample1 再次进行分析:

这样就比较明显了,稍做分析,我们可以先做个简单的总结,第5个字节为消息的总长度,第6个字节应该是字段描述之类的,当前消息体固定为10,第7个字节为Request.message字段的长度

不过这样有点草率,byte最大为255,我们再探索一下内容超过255时,是什么结构。将 Sample 修改为 50 个重复的 Sample 再次进行分析:

情况一下就复杂了。。。不过第6个字节仍然是10,那么前5个字节应该有描述消息总长度,[0,0,0,1,47] 和长度 303 (注:308-5)之间的关系是什么呢;稍微试了一下,数据的第1个字节目前假设固定为0,第2-5字节应该是一个大端序uint32,用来声明消息总长度但是第78个字节如何转换为300,就有点难琢磨了。。。算了,我们先不处理内容过大的情况吧(具体编码逻辑可参见 protocol-buffers-encoding

分析responseData

查看后发现结构和 requestData 是一样的(因为 RequestReply 消息声明的结构相同),这里就不多描述了,可以自行Debug查看。

分析流式请求的requestDataresponseData

分析后发现流式请求里面的多个消息每个都是单个消息的结构,然后顺序放到请求或响应流中,这里也不多描述了,可以自行Debug进行查看,直接上基于以上总结的解码器代码:

  1. public static IEnumerable<string> ReadMessages(byte[] originData)
  2. {
  3. var slice = originData.AsMemory();
  4. while (!slice.IsEmpty)
  5. {
  6. var messageLen = BinaryPrimitives.ReadInt32BigEndian(slice.Slice(1, 4).Span);
  7. var messageData = slice.Slice(5, messageLen);
  8. slice = slice.Slice(5 + messageLen);
  9. int len = messageData.Span[1];
  10. var content = Encoding.UTF8.GetString(messageData.Slice(2, len).Span);
  11. yield return content;
  12. }
  13. }

然后在中间件中展示内容

  1. TempMessageCodecUtil.DisplayMessages(requestData);
  2. TempMessageCodecUtil.DisplayMessages(responseData);

再次运行程序,能够正确看到控制台直接输出的请求和响应消息内容,形如:

四、使用 Controller 实现能够与 Grpc Client SDK 交互的服务端

基于之前的分析,理论上我们只需要满足:

  1. - 请求的协议使用的是 `HTTP/2`
  2. - 方法都为 `POST`
  3. - 所有grpc方法都映射到了对应的终结点 `/{package名}.{service名}/{方法名}`
  4. - 请求&响应的 `ContentType` 都为 `application/grpc`

然后正确的从请求流中解析数据结构,将正确的数据结构写入响应流,就可以响应 Grpc Client 的请求了。

  • 现在我们需要一个编码器,能够将字符串编码为 Reply 消息格式;以及一个解码器,从请求流中读取 Request 消息。直接上代码。编码器:
  1. public static byte[] BuildMessage(string message)
  2. {
  3. var contentData = Encoding.UTF8.GetBytes(message);
  4. if (contentData.Length > 127)
  5. {
  6. throw new ArgumentException();
  7. }
  8. var messageData = new byte[contentData.Length + 7];
  9. Array.Copy(contentData, 0, messageData, 7, contentData.Length);
  10. messageData[5] = 10;
  11. messageData[6] = (byte)contentData.Length;
  12. BinaryPrimitives.WriteInt32BigEndian(messageData.AsSpan().Slice(1), contentData.Length + 2);
  13. return messageData;
  14. }

解码器:

  1. private async IAsyncEnumerable<string> ReadMessageAsync([EnumeratorCancellation] CancellationToken cancellationToken)
  2. {
  3. var pipeReader = Request.BodyReader;
  4. while (!cancellationToken.IsCancellationRequested)
  5. {
  6. var readResult = await pipeReader.ReadAsync(cancellationToken);
  7. var buffer = readResult.Buffer;
  8. if (readResult.IsCompleted
  9. && buffer.IsEmpty)
  10. {
  11. yield break;
  12. }
  13. if (buffer.Length < 5)
  14. {
  15. pipeReader.AdvanceTo(buffer.Start, buffer.End);
  16. continue;
  17. }
  18. var messageBuffer = buffer.IsSingleSegment
  19. ? buffer.First
  20. : buffer.ToArray();
  21. var messageLen = BinaryPrimitives.ReadInt32BigEndian(messageBuffer.Slice(1, 4).Span);
  22. if (buffer.Length < messageLen + 5)
  23. {
  24. pipeReader.AdvanceTo(buffer.Start, buffer.End);
  25. continue;
  26. }
  27. messageBuffer = messageBuffer.Slice(5);
  28. int len = messageBuffer.Span[1];
  29. var content = Encoding.UTF8.GetString(messageBuffer.Slice(2, len).Span);
  30. yield return content;
  31. pipeReader.AdvanceTo(readResult.Buffer.GetPosition(7 + len));
  32. }
  33. }
  • 实现一个 ReverseController.cs ,映射 reverse.proto 中对应的方法,实现和 ReverseService.cs 中相同的执行逻辑。代码如下:
  1. [Route("reverse.Reverse")]
  2. [ApiController]
  3. public class ReverseController : ControllerBase
  4. {
  5. [HttpPost]
  6. [Route(nameof(Bidirectional))]
  7. public async Task Bidirectional()
  8. {
  9. await foreach (var item in ReadMessageAsync(HttpContext.RequestAborted))
  10. {
  11. DisplayReceivedMessage(item);
  12. await ReplayReverseAsync(item);
  13. }
  14. }
  15. [HttpPost]
  16. [Route(nameof(ClientSide))]
  17. public async Task ClientSide()
  18. {
  19. var total = 0;
  20. await foreach (var item in ReadMessageAsync(HttpContext.RequestAborted))
  21. {
  22. total++;
  23. DisplayReceivedMessage(item);
  24. }
  25. await ReplayAsync($"{nameof(ServerSide)} Received Over. Total: {total}");
  26. }
  27. [HttpPost]
  28. [Route(nameof(ServerSide))]
  29. public async Task ServerSide()
  30. {
  31. string message = null!;
  32. await foreach (var item in ReadMessageAsync(HttpContext.RequestAborted))
  33. {
  34. message = item;
  35. }
  36. DisplayReceivedMessage(message);
  37. for (int i = 0; i < 5; i++)
  38. {
  39. await ReplayReverseAsync(message);
  40. }
  41. }
  42. [HttpPost]
  43. [Route(nameof(Simple))]
  44. public async Task Simple()
  45. {
  46. string message = null!;
  47. await foreach (var item in ReadMessageAsync(HttpContext.RequestAborted))
  48. {
  49. message = item;
  50. }
  51. DisplayReceivedMessage(message);
  52. await ReplayReverseAsync(message);
  53. }
  54. private async Task ReplayAsync(string message)
  55. {
  56. if (!Response.HasStarted)
  57. {
  58. Response.Headers.ContentType = "application/grpc";
  59. Response.AppendTrailer("grpc-status", "0");
  60. await Response.StartAsync();
  61. }
  62. await Response.Body.WriteAsync(TempMessageCodecUtil.BuildMessage(message));
  63. }
  64. private Task ReplayReverseAsync(string rawMessage) => ReplayAsync(new string(rawMessage.Reverse().ToArray()));
  65. //省略其他信息
  66. }

最后记得 services.AddControllers()app.MapControllers() 并取消Grpc的ServiceMap;

此时分别使用 ControllerGrpcService 运行服务端,并查看客户端日志,可以看到运行结果相同,如图:

五、使用 HttpClient 实现能够与 Grpc Server 交互的客户端

在上面我们已经使用原生 Controller 实现了一个可以让客户端正常运行的服务端,现在我们不使用 Grpc SDK 来实现一个可以和服务端交互的客户端。

  • 服务端获取请求流和响应流比较简单,目前 HttpClient 没有直接获取请求流的办法,我们需要从 HttpContentSerializeToStreamAsync 方法中获取到真正的请求流。具体细节不在这里赘述,直接上代码:
  1. class LongAliveHttpContent : HttpContent
  2. {
  3. private readonly TaskCompletionSource<Stream> _streamGetCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
  4. private readonly TaskCompletionSource _taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
  5. public LongAliveHttpContent()
  6. {
  7. Headers.ContentType = new MediaTypeHeaderValue("application/grpc");
  8. }
  9. protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
  10. {
  11. _streamGetCompletionSource.SetResult(stream);
  12. return _taskCompletionSource.Task;
  13. }
  14. protected override bool TryComputeLength(out long length)
  15. {
  16. length = -1;
  17. return false;
  18. }
  19. public void Complete()
  20. {
  21. _taskCompletionSource.TrySetResult();
  22. }
  23. public Task<Stream> GetStreamAsync()
  24. {
  25. return _streamGetCompletionSource.Task;
  26. }
  27. }
  • 客户端同样需要满足对应的请求要求:
  1. - 请求的协议使用的是 `HTTP/2`
  2. - 方法都为 `POST`
  3. - 所有grpc方法都映射到了对应的终结点 `/{package名}.{service名}/{方法名}`
  4. - 请求&响应的 `ContentType` 都为 `application/grpc`

直接上代码,使用 HttpClient 发起请求,并获取 请求流 & 响应流

  1. private static (Task<Stream> RequestStreamGetTask, Task<Stream> ResponseStreamGetTask, LongAliveHttpContent HttpContent) CreateStreamGetTasksAsync(HttpClient client, string path)
  2. {
  3. var content = new LongAliveHttpContent();
  4. var httpRequestMessage = new HttpRequestMessage()
  5. {
  6. Method = HttpMethod.Post,
  7. RequestUri = new Uri(path, UriKind.Relative),
  8. Content = content,
  9. Version = HttpVersion.Version20,
  10. VersionPolicy = HttpVersionPolicy.RequestVersionExact,
  11. };
  12. var responseStreamGetTask = client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)
  13. .ContinueWith(m => m.Result.Content.ReadAsStreamAsync())
  14. .Unwrap();
  15. return (content.GetStreamAsync(), responseStreamGetTask, content);
  16. }
  • 实现和Grpc客户端相同的执行逻辑。代码如下:
  1. private static async Task BidirectionalWithOutSDK(HttpClient client)
  2. {
  3. var (requestStreamGetTask, responseStreamGetTask, httpContent) = CreateStreamGetTasksAsync(client, "reverse.Reverse/Bidirectional");
  4. var requestStream = await requestStreamGetTask;
  5. var sendTask = Task.Run(async () =>
  6. {
  7. for (int i = 0; i < 10; i++)
  8. {
  9. await requestStream.WriteAsync(TempMessageCodecUtil.BuildMessage($"{nameof(Bidirectional)}-{i}"));
  10. }
  11. httpContent.Complete();
  12. });
  13. var receiveTask = DisplayReceivedMessageAsync(responseStreamGetTask);
  14. await Task.WhenAll(sendTask, receiveTask);
  15. }
  16. private static async Task ClientSideWithOutSDK(HttpClient client)
  17. {
  18. var (requestStreamGetTask, responseStreamGetTask, httpContent) = CreateStreamGetTasksAsync(client, "reverse.Reverse/ClientSide");
  19. var requestStream = await requestStreamGetTask;
  20. for (int i = 0; i < 5; i++)
  21. {
  22. await requestStream.WriteAsync(TempMessageCodecUtil.BuildMessage($"{nameof(ClientSide)}-{i}"));
  23. await requestStream.FlushAsync();
  24. }
  25. httpContent.Complete();
  26. await DisplayReceivedMessageAsync(responseStreamGetTask);
  27. }
  28. private static async Task SampleWithOutSDK(HttpClient client)
  29. {
  30. var (requestStreamGetTask, responseStreamGetTask, httpContent) = CreateStreamGetTasksAsync(client, "reverse.Reverse/Simple");
  31. var requestStream = await requestStreamGetTask;
  32. await requestStream.WriteAsync(TempMessageCodecUtil.BuildMessage(nameof(Sample)));
  33. httpContent.Complete();
  34. await DisplayReceivedMessageAsync(responseStreamGetTask);
  35. }
  36. private static async Task ServerSideWithOutSDK(HttpClient client)
  37. {
  38. var (requestStreamGetTask, responseStreamGetTask, httpContent) = CreateStreamGetTasksAsync(client, "reverse.Reverse/ServerSide");
  39. var requestStream = await requestStreamGetTask;
  40. await requestStream.WriteAsync(TempMessageCodecUtil.BuildMessage(nameof(ServerSide)));
  41. httpContent.Complete();
  42. await DisplayReceivedMessageAsync(responseStreamGetTask);
  43. }

此时分别进行如下测试:

  • 使用 GrpcService 运行服务端,并分别使用sdk客户端HttpClient客户端进行请求;
  • 使用 Controller 运行服务端,并分别使用sdk客户端HttpClient客户端进行请求;

可以看到客户端运行结果相同,如下:

  1. Sample Received: elpmaS
  2. ClientSide Received: ServerSide Received Over. Total: 5
  3. ServerSide Received: ediSrevreS
  4. ServerSide Received: ediSrevreS
  5. ServerSide Received: ediSrevreS
  6. ServerSide Received: ediSrevreS
  7. ServerSide Received: ediSrevreS
  8. Bidirectional Received: 0-lanoitceridiB
  9. Bidirectional Received: 1-lanoitceridiB
  10. Bidirectional Received: 2-lanoitceridiB
  11. Bidirectional Received: 3-lanoitceridiB
  12. Bidirectional Received: 4-lanoitceridiB
  13. Bidirectional Received: 5-lanoitceridiB
  14. Bidirectional Received: 6-lanoitceridiB
  15. Bidirectional Received: 7-lanoitceridiB
  16. Bidirectional Received: 8-lanoitceridiB
  17. Bidirectional Received: 9-lanoitceridiB
  18. ----------------- WithOutSDK -----------------
  19. SampleWithOutSDK Received: elpmaS
  20. ClientSideWithOutSDK Received: ServerSide Received Over. Total: 5
  21. ServerSideWithOutSDK Received: ediSrevreS
  22. ServerSideWithOutSDK Received: ediSrevreS
  23. ServerSideWithOutSDK Received: ediSrevreS
  24. ServerSideWithOutSDK Received: ediSrevreS
  25. ServerSideWithOutSDK Received: ediSrevreS
  26. BidirectionalWithOutSDK Received: 0-lanoitceridiB
  27. BidirectionalWithOutSDK Received: 1-lanoitceridiB
  28. BidirectionalWithOutSDK Received: 2-lanoitceridiB
  29. BidirectionalWithOutSDK Received: 3-lanoitceridiB
  30. BidirectionalWithOutSDK Received: 4-lanoitceridiB
  31. BidirectionalWithOutSDK Received: 5-lanoitceridiB
  32. BidirectionalWithOutSDK Received: 6-lanoitceridiB
  33. BidirectionalWithOutSDK Received: 7-lanoitceridiB
  34. BidirectionalWithOutSDK Received: 8-lanoitceridiB
  35. BidirectionalWithOutSDK Received: 9-lanoitceridiB

六、结论

至此,我们稍作分析和总结,可以得出结论:

  • Grpc 所有类型的方法调用都是普通的Http请求,只是请求和响应的内容是经过 Protobuf 编码的数据;

我们再稍作拓展,可以得出更多结论:

  • 多路复用Header压缩 什么的,都是 Http2 带来的优化,不是和 Grpc 绑定的,使用 Http2 访问常规 WebAPI 也能享受到其带来的好处;
  • GrpcUnary 请求模式和和 WebAPI 逻辑是一样的;Server streamingClient streaming 请求模式都可以通过 Http1.1 进行实现(但不能多路复用,每个请求会独占一个连接);Bidirectional streaming 是基于 二进制分帧 的,只能在 Http2 及以上版本实现双向流通讯;

基于以上结论,我们总结一下 GrpcWebAPI 的优势在哪里:

  • 运行速度更快(一定情况下),Protobuf 基于二进制的编码,在数据量较多时,比 json 这种基于文本的编码效率更高;但丢失了直接的可阅读性;(没做性能测试,理论是这样,如果性能打不过 json 的话,那就没有存在价值了。理论上数据量越大,性能差距越大)
  • 传输数据更少,json 因为要自我描述,所有字段都有名字,在序列化 List 时这种浪费就比较多了,重复对象越多,浪费越多(但可阅读性也是这样来的);Protobuf 没有这方面的浪费,还有一些其它的优化,参见 protocol-buffers-encoding
  • 开发速度更快,SDK使用 proto 文件直接生成服务端和客户端,上手更快,跨语言也能快速生成客户端(这点其实见仁见智,WebAPI 也有类似的工具);

Grpc 比传统 WebAPI 的劣势有哪些呢:

  • 可阅读性;不借助工具 Grpc 的消息内容是没法直接阅读的;
  • HTTP2 强绑定;WebAPI 可以在低版本协议下运行,某些时候会方便一点;
  • 依赖 Grpc SDK;虽然 Grpc SDK 已经覆盖了很多主流语言,但如果恰好某个需求要使用的语言没有SDK,那就有点麻烦了;相比之下基于文本的 WebAPI 会更通用一点;
  • 类型不能完全覆盖某些语言的基础类型,需要额外的编码量(方法不能直接接收/返回基础类型、Nullable等);
  • Protobuf 要求严格的格式,字段增删
  • 额外的学习成本;

最后再基于结论,总结一些我认为有问题的 grpc 使用方法吧:

  • grpc 当作一个封包/拆包工具;在消息体中放一个 json 之类的东西,拿到消息之后在反序列化一次。。。这又是何必呢。。。直接基于原生 Http 写一个 基于消息头指定消息长度 的分包逻辑并花不了多少工作量,也不会额外引入grpc的相关东西;这个用法也和 grpc高性能 背道而驰,还多了一层 序列化/反序列化 操作;(我在这里没有说nacos)
  • 使用单独的认证逻辑;grpc 调用就是 Http 请求,那么 Header 的工作逻辑是和 WebAPI 完全一样的;那么 grpc 请求完全可以使用现有的 Http 认证 和 Header处理 代码甚至请求管道;额外再自定义消息实现相关功能不是多此一举吗?(我在这里也没有说nacos)

综上,个人认为,不是别人说 grpc 高性能,就认为它碾压传统 WebAPI,就去用它;还是需要了解原理后好好考虑的,确认它能否为你带来理想的效果;有时候或许自己手写一个变体的 Http 请求处理逻辑能更快更好的满足需求;


拓展

如果有闲心的话,理论上甚至可以做下列的玩具:

  • WebAPIgrpc 兼容层,使 Controller 既能以 grpc 工作又能处理普通请求;通过 Controller 定义,反向生成 DTOproto 消息定义,以及整个service的 proto 定义;
  • grpcWebAPI 兼容层,使 grpc 服务能工作的像 Controller 一样,对外输入输出 json

[gRPC via C#] gRPC本质的探究与实践的更多相关文章

  1. protobuffer、gRPC、restful gRPC的相互转化

    转自:https://studygolang.com/articles/12510 文档 grpc中文文档 grpc-gateway,restful和grpc转换库 protobuf 官网 proto ...

  2. 我的Go gRPC之旅、01 初识gRPC,感受gRPC的强大魅力

    微服务架构 微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的API 进行通信的小型独立服务组成. 这些服务由各个小型独立团队负责. 微服务架构使应用程序更易于扩展和更快地开发,从而加速创 ...

  3. attilax.java 注解的本质and 使用最佳实践(3)O7

    attilax.java 注解的本质and 使用最佳实践(3)O7 1. 定义pojo 1 2. 建立注解By eclipse tps 1 3. 注解参数的可支持数据类型: 2 4. 注解处理器 2 ...

  4. Golang gRPC学习(03): grpc官方示例程序route_guide简析

    代码主要来源于grpc的官方examples代码: route_guide https://github.com/grpc/grpc-go/tree/master/examples/route_gui ...

  5. 前端框架本质之探究——以Vue.js为例

    问:我们在使用Vue时,实际上干了什么?   答:实际上只干了一件事——new了一个Vue对象.后面的事,都交由这个对象自动去做.就好像按了下开关,机器跑起来了,剩下的事就不用我们再操心了.   各位 ...

  6. Spring:IOC本质分析探究

    IOC本质分析 分析实现 我们先用我们原来的方式写一段代码 . 先写一个UserDao接口 public interface UserDao { public void getUser(); } 再去 ...

  7. cocos2d-x触摸事件优先级的探究与实践

    如何让自定义Layer触发触摸事件? bool LayerXXX::init() { this->setTouchEnabled(true); CCTouchDispatcher* td = C ...

  8. java grpc实例分析

    一.Protocol Buffer 我们还是先给出一个在实际开发中经常会遇到的系统场景.比如:我们的客户端程序是使用Java开发的,可能运行自不同的平台,如:Linux.Windows或者是Andro ...

  9. 跟我一起学Go系列:gRPC 入门必备

    RPC 的定义这里就不再说,看文章的同学都是成熟的开发.gRPC 是 Google 开源的高性能跨语言的 RPC 方案,该框架的作者 Louis Ryan 阐述了设计这款框架的动机,有兴趣的同学可以看 ...

随机推荐

  1. IE8中li添加float属性,中英数字混合BUG

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  2. 「Python实用秘技04」为pdf文件批量添加文字水印

    本文完整示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/PythonPracticalSkills 这是我的系列文章「Python实用秘技」的第4期 ...

  3. 不难懂--------react笔记

      在jsx中不能使用class定义类名   因为class在js中是用来定义类的  定义类名的时候用className       label中的for必须写成htmlFor         Rea ...

  4. 安卓开发常见Bug-项目未升级到Androidx

    当项目未升级到androidx时,会出现某些项目文件资源不匹配的问题,建议在建立项目后就将项目升级到androidx 点击升级到androidx Migrate迁移然后点击左下角Dorefactor

  5. 布客&#183;ApacheCN 编程/大数据/数据科学/人工智能学习资源 2020.2

    特约赞助商 公告 我们愿意普及区块链技术,但前提是互利互惠.我们有大量技术类学习资源,也有大量的人需要这些资源.如果能借助区块链技术存储和分发,我们就能将它们普及给我们的受众. 我们正在招募项目负责人 ...

  6. atomic 原子自增工程用法案例

    案例 1 : 简单用法 atomic_int id; atomic_fetch_add(&id, 1) atomic_uint id; atomic_fetch_add(&id, 1) ...

  7. AI 智能写情诗、藏头诗

    一.AI 智能情诗.藏头诗展示 最近使用PyTorch的LSTM训练一个写情诗(七言)的模型,可以随机生成情诗.也可以生成藏头情诗. 在特殊的日子用AI生成一首这样的诗,是不是很酷!下面分享下AI 智 ...

  8. .exe文件自动重启

    echo  :杀死进程taskkill /f /im YYTWEB.exe  :等待10秒:ping 127.0.0.1 -n 10  start "" "D:\都江堰银 ...

  9. java中构造函数和一般函数的区别

    构造方法 特点: 1.方法名称和类名相同 2.不用定义返回值类型 3.不可以写return语句 作用: 给对象初始化 构造方法的细节: 当一个类中没有定义构造函数时,系统会默认添加一个无参的构造方法. ...

  10. 转载_认识C语言的32个关键字

    简单介绍: 1 auto : 声明自动变量 2 short :声明短整型变量或函数 3 int: 声明整型变量或函数 4 long :声明长整型变量或函数 5 float:声明浮点型变量或函数 6 d ...