.NET 开源项目 StreamJsonRpc 介绍[下篇]

阅读本文大概须要 9 分钟。git

你们好,这是 .NET 开源项目 StreamJsonRpc 介绍的最后一篇。上篇介绍了一些预备知识,包括 JSON-RPC 协议介绍,StreamJsonRpc 是一个实现了 JSON-RPC 协议的库,它基于 Stream、WebSocket 和自定义的全双工管道传输。中篇经过示例讲解了 StreamJsonRpc 如何使用全双工的 Stream 做为传输管道实现 RPC 通信。本篇(下篇)将继续经过示例讲解如何基于 WebSocket 传输管道实现 RPC 通信。github

准备工做

为了示例的完整性,本文示例继续在中篇建立的示例基础上进行。该示例的 GitHub 地址为:web

github.com/liamwang/StreamJsonRpcSamples编程

咱们继续添加三个项目,一个是名为 WebSocketSample.Client 的 Console 应用,一个是名为 WebSocketSample.Server 的 ASP.NET Core 应用,还有一个名为 Contract 的契约类库(和 gRPC 相似)。json

你能够直接复制并执行下面的命令一键完成大部分准备工做:api

dotnet new console -n WebSocketSample.Client # 建新客户端应用
dotnet new webapi -n WebSocketSample.Server # 新建服务端应用
dotnet new classlib -n Contract # 新建契约类库
dotnet sln add WebSocketSample.Client WebSocketSample.Server Contract # 将项目添加到解决方案
dotnet add WebSocketSample.Client package StreamJsonRpc # 为客户端安装 StreamJsonRpc 包
dotnet add WebSocketSample.Server package StreamJsonRpc # 为服务端安装 StreamJsonRpc 包
dotnet add WebSocketSample.Client reference Contract # 添加客户端引用 Common 引用
dotnet add WebSocketSample.Server reference Contract # 添加服务端引用 Common 引用

为了把重点放在实现上,此次咱们依然以一个简单的功能做为示例。该示例实现客户端向服务端发送一个问候数据,而后服务端响应一个消息。为了更贴合实际的场景,此次使用强类型进行操做。为此,咱们在 Contract 项目中添加三个类用来约定客户端和服务端通信的数据结构和接口。bash

用于客户端发送的数据的 HelloRequest 类:网络

public class HelloRequest
{
    public string Name { get; set; }
}

用于服务端响应的数据的 HelloResponse 类:数据结构

public class HelloResponse
{
    public string Message { get; set; }
}

用于约定服务端和客户端行为的 IGreeter 接口:app

public interface IGreeter
{
    Task<HelloResponse> SayHelloAsync(HelloRequest request);
}

接下来和中篇同样,经过创建链接、发送请求、接收请求、断开链接这四个步骤演示和讲解一个完整的基于 WebSocket 的 RPC 通信示例。

创建链接

上一篇讲到要实现 JSON-RPC 协议的通信,要求传输管道必须是全双工的。而 WebSocket 就是标准的全双工通信,因此天然能够用来实现 JSON-RPC 协议的通信。.NET 自己就有现成的 WebSocket 实现,因此在创建链接阶段和 StreamJsonRpc 没有关系。咱们只须要把 WebSocket 通信管道架设好,而后再使用 StreamJsonRpc 来发送和接收请求便可。

客户端使用 WebSocket 创建链接比较简单,使用 ClientWebSocket 来实现,代码以下:

using (var webSocket = new ClientWebSocket())
{
    Console.WriteLine("正在与服务端创建链接...");
    var uri = new Uri("ws://localhost:5000/rpc/greeter");
    await webSocket.ConnectAsync(uri, CancellationToken.None);
    Console.WriteLine("已创建链接");
}

服务端创建 WebSocket 链接最简单的方法就是使用 ASP.NET Core,借助 Kestrel 和 ASP.NET Core 的中间件机制能够轻松搭建基于 WebSocket 的 RPC 服务。只要简单的封装还能够实现同一套代码同时提供 RPC 服务和 Web API 服务。

首先在服务端项目的 Startup.cs 类的 Configure 方法中引入 WebSocket 中间件:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();

    app.UseWebSockets(); // 增长此行,引入 WebSocket 中间件

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

再新建一个 Controller 并定义一个 Action 用来路由映射 WebSocket 请求:

public class RpcController : ControllerBase
{
    ...
    [Route("/rpc/greeter")]
    public async Task<IActionResult> Greeter()
    {
        if (!HttpContext.WebSockets.IsWebSocketRequest)
        {
            return new BadRequestResult();
        }

        var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();

        ...
    }
}

这里的 Greeter 提供的服务既能接收 HTTP 请求也能接收 WebSocket 请求。HttpContext 中的 WebSockets 属性是一个 WebSocketManager 对象,它能够用来判断当前请求是否为一个 WebSocket 请求,也能够用来等待和接收 WebSocket 链接,即上面代码中的 AcceptWebSocketAsync 方法。另外客户端的 WebSocket 的 Uri 路径须要与 Router 指定的路径对应。

链接已经创建,如今到了 StreamJsonRpc 发挥做用的时候了。

发送请求

客户端经过 WebSocket 发送请求的方式和前一篇讲的 Stream 方式是同样的。还记得前一篇讲到的 JsonRpc 类的 Attach 静态方法吗?它告诉 StreamJsonRpc 如何传输数据,并返回一个用于调用 RPC 的客户端,它除了能够接收 Stream 参数外还有多个重载方法。好比:

public static T Attach<T>(Stream stream);
public static T Attach<T>(IJsonRpcMessageHandler handler);

第二个重载方法能够实现更灵活的 Attach 方式,你能够 Attach 一个交由 WebSocket 传输数据的管道,也能够 Attach 给一个自定义实现的 TCP 全双工传输管道(此方式本文不讲,但文末会直接给出示例)。如今咱们须要一个实现了 IJsonRpcMessageHandler 接口的处理程序,StreamJsonRpc 已经实现好了,它是 WebSocketMessageHandler 类。经过 Attach 该实例,能够拿到一个用于调用 RPC 服务的对象。代码示例以下:

Console.WriteLine("开始向服务端发送消息...");
var messageHandler = new WebSocketMessageHandler(webSocket);
var greeterClient = JsonRpc.Attach<IGreeter>(messageHandler);
var request = new HelloRequest { Name = "精致码农" };
var response = await greeterClient.SayHelloAsync(request);
Console.WriteLine($"收到来自服务端的响应:{response.Message}");

你会发现,定义客户端和服务端契约的好处是能够实现强类型编程。接下来看服务端如何接收并处理客户端发送的消息。

接收请求

和前一篇同样,咱们先定义一个 GreeterServer 类用来处理接收到的客户端消息。

public class GreeterServer : IGreeter
{
    private readonly ILogger<GreeterServer> _logger;
    public GreeterServer(ILogger<GreeterServer> logger)
    {
        _logger = logger;
    }

    public Task<HelloResponse> SayHelloAsync(HelloRequest request)
    {
        _logger.LogInformation("收到并回复了客户端消息");
        return Task.FromResult(new HelloResponse
        {
            Message = $"您好, {request.Name}!"
        });
    }
}

一样,WebSocket 服务端也须要使用 Attach 来告诉 StreamJsonRpc 数据如何通信,并且使用的也是 WebSocketMessageHandler 类,方法与客户端相似。在前一篇中,咱们 Attach 一个 Stream 调用的方法是:

public static JsonRpc Attach(Stream stream, object? target = null);

同理,咱们推测应该也有一个这样的静态重载方法:

public static JsonRpc Attach(IJsonRpcMessageHandler handler, object? target = null);

惋惜,StreamJsonRpc 并无提供这个静态方法。既然 Attach 方法返回的是一个 JsonRpc 对象,那咱们是否能够直接实例化该对象呢?查看该类的定义,咱们发现是能够的,并且有咱们须要的构造函数:

public JsonRpc(IJsonRpcMessageHandler messageHandler, object? target);

接下来就简单了,一切和前一篇的 Stream 示例都差很少。在 RpcController 的 Greeter Action 中实例化一个 JsonRpc,而后开启消息监听。

public class RpcController : ControllerBase
{
    private readonly ILogger<RpcController> _logger;
    private readonly GreeterServer _greeterServer;

    public RpcController(ILogger<RpcController> logger, GreeterServer greeterServer)
    {
        _logger = logger;
        _greeterServer = greeterServer;
    }

    [Route("/rpc/greeter")]
    public async Task<IActionResult> Greeter()
    {
        if (!HttpContext.WebSockets.IsWebSocketRequest)
        {
            return new BadRequestResult();
        }

        _logger.LogInformation("等待客户端链接...");
        var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
        _logger.LogInformation("已与客户端创建链接");

        var handler = new WebSocketMessageHandler(socket);

        using (var jsonRpc = new JsonRpc(handler, _greeterServer))
        {
            _logger.LogInformation("开始监听客户端消息...");
            jsonRpc.StartListening();
            await jsonRpc.Completion;
            _logger.LogInformation("客户端断开了链接");
        }

        return new EmptyResult();
    }
}

看起来和咱们平时写 Web API 差很少,区别仅仅是对请求的处理方式。但须要注意的是,WebSocket 是长链接,若是客户端没有事情能够处理了,最好主动断开与服务端的链接。若是客户客户没有断开链接,执行的上下文就会停在 await jsonRpc.Completion 处。

断开链接

一般断开链接是由客户端主动发起的,因此服务端不须要作什么处理。服务端响应完消息后,只需使用 jsonRpc.Completion 等待客户端断开链接便可,上一节的代码示例中已经包含了这部分代码,就再也不累述了。若是特殊状况下服务端须要断开链接,调用 JsonRpc 对象的 Dispose 方法便可。

不论是 Stream 仍是 WebSocket,其客户端对象都提供了 Close 或 Dispose 方法,链接会随着对象的释放自动断开。但最好仍是主动调用 Close 方法断开链接,以确保服务端收到断开的请求。对于 ClientWebSocket,须要调用 CloseAsync 方法。客户端完整示例代码以下:

static async Task Main(string[] args)
{
    using (var webSocket = new ClientWebSocket())
    {
        Console.WriteLine("正在与服务端创建链接...");
        var uri = new Uri("ws://localhost:5000/rpc/greeter");
        await webSocket.ConnectAsync(uri, CancellationToken.None);
        Console.WriteLine("已创建链接");

        Console.WriteLine("开始向服务端发送消息...");
        var messageHandler = new WebSocketMessageHandler(webSocket);
        var greeterClient = JsonRpc.Attach<IGreeter>(messageHandler);
        var request = new HelloRequest { Name = "精致码农" };
        var response = await greeterClient.SayHelloAsync(request);
        Console.WriteLine($"收到来自服务端的响应:{response.Message}");

        Console.WriteLine("正在断开链接...");
        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "断开链接", CancellationToken.None);
        Console.WriteLine("已断开链接");
    }

    Console.ReadKey();
}

在实际项目中可能还须要因异常而断开链接的状况作处理,好比网络不稳定可能致使链接中断,这种状况可能须要加入重试机制。

运行示例

因为服务端使用的是 ASP.NET Core 模板,VS 默认使用 IIS Express 启动,启动后会自动打开网页,这样看不到 Console 的日志信息。因此须要把服务端项目 WebSocketSample.Server 的启动方式改为自启动。

另外,为了更方便地同时运行客户端和服务端应用,能够把解决方案设置成多启动。右键解决方案,选择“Properties”,把对应的项目设置“Start”便可。

若是你用的是 VS Code,也是支持多启动调试的,具体方法你自行 Google。若是你用的是 dotnet run 命令运行项目可忽略以上设置。

项目运行后的截图以下:

你也能够自定义实现 TCP 全双工通信管道,但比较复杂并且也不多这么作,因此就略过不讲了。但我在 GitHub 的示例代码也放了一个自定义全双工管道实现的示例,感兴趣的话你能够克隆下来研究一下。

该示例运行截图:

本篇总结

本文经过示例演示了如何使用 StreamJsonRpc 基于 WebSocket 数据传输实现 JSON-RPC 协议的 RPC 通信。其中客户端和服务端有共同的契约部分,实现了强类型编程。经过示例咱们也清楚了 StreamJsonRpc 这个库为了实现 RPC 通信作了哪些工做,其实它就是在现有传输管道(Stream、WebSocket 和 自定义 TCP 链接)上进行数据通信。正如前一篇所说,因为 StreamJsonRpc 把大部分咱们没必要要知道的细节作了封装,因此在示例中感受不到 JSON-RPC 协议带来的统一规范,也没看到具体的 JSON 格式的数据。其实只要遵循了 JSON-RPC 协议实现的客户端或服务端,不论是用什么语言实现,都是能够互相通信的。

但愿这三篇关于 StreamJsonRpc 的介绍能让你有所收获,若是你在工做中计划使用 StreamJsonRpc,这几篇文章包括示例代码应该有值得参考的地方。

相关文章
相关标签/搜索