zoukankan      html  css  js  c++  java
  • StreamJsonRpc 是一个实现了 JSON-RPC 通信协议的 .NET 库

    StreamJsonRpc 是一个实现了 JSON-RPC 通信协议的 .NET 库

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

    StreamJsonRpc 是一个实现了 JSON-RPC 通信协议的开源 .NET 库,在介绍 StreamJsonRpc 之前,我们先来了解一下 JSON-RPC。

     

    JSON-RPC 介绍

     

    JSON-RPC 是一个无状态且轻量级的远程过程调用(RPC)协议,其使用 JSON(RFC 4627)作为数据格式。

     

    目前 JSON-RPC 的版本已发展到 2.0,JSON-RPC 2.0 与 1.0 的约定规范是不一样的。2.0 包含一个名为 jsonrpc 且值为 2.0 的成员,而 1.0 版本是不包含的。所以我们可以很容易在两个版本间区分出 2.0。

     

    JSON-RPC 在客户端与服务端之间交换的所有成员名应是区分大小写的,函数、方法、过程都认为是可互换的。客户端被定义为请求对象的来源及响应对象的处理程序;服务端被定义为响应对象的起源和请求对象的处理程序。

     

    请求对象

     

    发送一个请求对象至服务端代表一个 RPC 调用,JSON-RPC 2.0 规定一个请求对象包含下列成员:

     

    • jsonrpc:指定 JSON-RPC 协议版本的字符串,必须准确写为“2.0”。
    • method:包含所要调用方法名称的字符串,以 rpc 开头的方法名,用英文句号连接的为预留给 rpc 内部的方法名及扩展名,且不能在其他地方使用。
    • params:调用方法所需要的结构化参数值,该成员参数可以被省略。
    • id:已建立客户端的唯一标识,值必须包含一个字符串、数值或 NULL 空值。如果不包含该成员则被认定为是一个通知。该值一般不为 NULL,若为数值则不应该包含小数。

     

    没有包含 id 成员的请求对象为通知,作为通知的请求对象表明客户端对服务端响应不感兴趣,服务端可以不响应请求对象给客户端。

     

    下面是几个请求对象的 JSON 结构示例(“-->”表示发送,“<--”表示响应,下同):

     

    --> { "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1 }
    --> { "jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
    --> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]} // 通知
    

     

    响应对象

     

    当客户端发起一个 RPC 调用时,除通知之外,服务端都必须回复响应。响应也表示为一个 JSON 对象,使用以下成员:

     

    • jsonrpc:指定 JSON-RPC 协议版本的字符串,必须准确写为“2.0”。
    • result:调用成功时响应给客户端的结果,当调用发生错误时可以不包含该成员。
    • error:调用发生错误时返回给客户端的错误信息,在调用失败时必须包含该成员。
    • id:对应请求对象的“id”,其值必须与请求对象中的“id”值一致。

     

    响应对象必须包含 result 或 error 成员之一。

     

    响应对象的 error 成员的结构包含下列成员:

     

    • code:使用数值表示该异常的错误类型,必须为整数。、
    • message:对该错误的简单描述字符串,该描述应尽量限定在简短的一句话。
    • data:包含关于错误的附加信息,可忽略。

     

    其中 -32768 至 -32000 为保留的预定义错误代码,各保留错误代码的含义请查看文末参考链接[1]。

     

    下面是几个响应对象的 JSON 结构示例:

     

    <-- {"jsonrpc": "2.0", "result": 19, "id": 1}
    <-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
    <-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null} // 无效调用
    

     

    批量调用

     

    当需要同时发送多个请求对象时,客户端可以发送一个包含所有请求对象的数组。

     

    当批量调用的所有请求对象处理完成时,服务端则需要返回一个包含相对应的响应对象数组。每个响应对象都应对应每个请求对象,除非是通知的请求对象。服务端可以并发的,可以以任意顺序和任意宽度并行处理这些批量调用。而客户端应该是基于各个响应对象中的 id 成员来匹配对应的请求对象。

     

    若批量调用没有需要返回的响应对象,则服务端不需要返回任何结果。

     

    下面是一个批量请求及响应的 JSON 结构示例:

     

    --> [
          {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
          {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
          {"foo": "boo"},
          {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
          {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
        ]
    <-- [
          {"jsonrpc": "2.0", "result": 7, "id": "1"},
          {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
          {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
          {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
        ]
    

     

    当批量请求对象都是通知时,服务端不需要返回结果。

     

    StreamJsonRpc 库介绍

     

    StreamJsonRpc 是一个实现了 JSON-RPC 通信协议的 .NET 库,支持 .NET Core。它把 RPC 的调用封装为公开的 .NET API,可以很方便的进行 RPC 请求的发送和接收操作。StreamJsonRpc 是微软官方的一个开源库,目前 Star 数接近 300,貌似知道的人不多或者用的人不多。GitHub 地址:

     

    github.com/microsoft/vs-streamjsonrpc

     

    StreamJsonRpc 可以在 Stream、WebSocket 或 System.IO.Pipelines 管道上工作,独立于底层传输。除了包含 JSON-RPC 规范所需的特性外,它额外还有如下优点:

     

    • 请求取消
    • .NET 事件作为通知
    • 动态客户端代理生成
    • 支持紧凑的 MessagePack 二进制序列化
    • 易于实现插件式架构的消息处理和格式化

     

    使用 StreamJsonRpc 主要有四个基本步骤:建立 JSON-RPC 连接、发送 RPC 请求、接收 RPC 请求、断开连接。

     

    这一篇主要介绍一些预备知识,下一篇将通过示例演示并详细介绍 StreamJsonRpc 的使用,敬请期待!

     

    参考:
    [1].jsonrpc.org/specification
    [2].github.com/microsoft/vs-streamjsonrpc

    包括 JSON-RPC 介绍和实现了 JSON-RPC 的 StreamJsonRpc 介绍,讲到了 StreamJsonRpc 可以通过 .NET 的 Stream 类和 WebSocket 类实现 JSON-RPC 协议的通信。本篇就先选择其中的 Stream 类来讲解,通过具体的示例讲解如何使用 StreamJsonRpc 实现 RPC 调用。

    准备工作

    先新建两个 Console 应用,分别命名为 StreamSample.Client 和 StreamSample.Server,并均添加 StreamJsonRpc 包引用。

    mkdir StreamJsonRpcSamples                              # 创建目录
    cd StreamJsonRpcSamples                                 # 进入目录
    dotnet new sln -n StreamJsonRpcSamples                  # 新建解决方案
    dotnet new console -n StreamSample.Client               # 建新客户端应用
    dotnet new console -n StreamSample.Server               # 新建服务端应用
    dotnet sln add StreamSample.Client StreamSample.Server  # 将应用添加到解决方案
    dotnet add StreamSample.Client package StreamJsonRpc    # 为客户端安装 StreamJsonRpc 包
    dotnet add StreamSample.Server package StreamJsonRpc    # 为服务端安装 StreamJsonRpc 包
    

    上篇 提到了实现 JSON-RPC 通讯要经历四个步骤:建立连接、发送请求、接收请求、断开连接,其中发送请求和接收请求可以归为数据通讯,下面按照这几个步骤顺序来逐步讲解。

    建立连接

    使用 Stream 实现 JSON-RPC 协议的通讯,要求该 Stream 必须是一个全双工 Stream(可同时接收数据和发送数据)或是一对半双工 Stream(本文不作讨论)。实现了全双工的 Stream 类在 .NET 中有 PipeStreamNetworkStream 等,本示例用的是 NamedPipeClientStream 类和 NamedPipeServerStream,前者用于客户端,后者用于服务端。

    先看服务端代码示例:

    int clientId = 1;
    
    var stream = new NamedPipeServerStream("StringJsonRpc",
        PipeDirection.InOut,
        NamedPipeServerStream.MaxAllowedServerInstances,
        PipeTransmissionMode.Byte,
        PipeOptions.Asynchronous);
    
    Console.WriteLine("等待客户端连接...");
    await stream.WaitForConnectionAsync();
    Console.WriteLine($"已与客户端 #{clientId} 建立连接");
    

    这里使用了 NamedPipeServerStream 类,其第一个构造参数指定了该 Stream 管道的名称,方便客户端使用该名称查找。其它参数就不解释了,其各自的含义可以在你编写代码时通过智能提示了解。

    Stream 实例通过 WaitForConnectionAsync 来等待一个客户端连接。由于该服务端可以连接多个客户端,这里使用自增长的 clientId 来标识区分它们。

    再来看客户端代码示例:

    var stream = new NamedPipeClientStream(".",
        "StringJsonRpc",
        PipeDirection.InOut,
        PipeOptions.Asynchronous);
    
    Console.WriteLine("正在连接服务器...");
    await stream.ConnectAsync();
    Console.WriteLine("已建立连接!");
    

    和服务器类似,客户端使用的是 NamedPipeClientStream 类来建立连接,在其构造参数中需要指定服务端的地址(这里用了.代表本机)和通讯管道的名称。Stream 实例通过 ConnectAsync 方法主动向服务器请求连接。

    如果网络是通的,客户端和服务端就能成功建立连接。下面就要实现客户端和服务端之间的数据通讯了,即客户端发送请求和服务端接收并处理请求。

    数据通讯

    客户端与服务端建立连接后,数据不会无缘无故从一端流到另一端,要实现两端的数据通讯还需要先把通讯管道架设起来,在其两端设定对应的控制和处理程序。工程上这个听起来好像不简单,但对于 StreamJsonRpc 来说是件非常简单的事。最简单的方法是使用 JsonRpc 类的 Attach 静态方法来架设两端的 Stream 管道,该方法返回一个 JsonRpc 实例可以用来控制数据的通讯。

    对于服务端,架设管道的同时还要为管道上的请求添加监听和对应的处理程序,比如定义一个名为 GreeterServer 的类来处理“打招呼”的请求:

    public class GreeterServer
    {
        public string SayHello(string name)
        {
            Console.WriteLine($"收到【{name}】的问好,并回复了他");
            return $"您好,{name}!";
        }
    }
    

    然后实例化该类,把它传给 JsonRpc 类的 Attach 静态方法:

    static async Task Main(string[] args)
    {
        ...
        _ = ResponseAsync(stream, clientId);
        clientId++;
    }
    
    static Task ResponseAsync(NamedPipeServerStream stream, int clientId)
    {
        var jsonRpc = JsonRpc.Attach(stream, new GreeterServer());
        return jsonRpc.Completion;
    }
    

    这里我们单独定义了一个 ResponseAsync 方法用来处理客户端请求,在 Main 函数中我们不用关心该方法返回的 Task 任务,所以使用了弃元

    对于客户端也是类似的,使用 JsonRpc 类的 Attach 静态方法来完成管道架设,并调用 JsonRpc 实例的 InvokeAsync 方法向服务端发送指定请求。代码示例如下:

    ...
    Console.WriteLine("我是精致码农,开始向服务端问好...");
    var jsonRpc = JsonRpc.Attach(stream);
    var message = await jsonRpc.InvokeAsync<string>("SayHello", "精致码农");
    Console.WriteLine($"来自服务端的响应:{message}");
    

    这样就实现了客户端调用服务端的方法,但客户端需要知道服务端的方法签名。这里只是为示例演示,在实际情况中,客户端和服务端需要先约定好接口,这样客户端就可以面向接口实现强类型编程,不必关心服务端处理程序的具体信息。

    注意到没,从建立连接到实现数据通讯,客户端和服务端都是对应的,而且使用的类和方法都是相似的。

    断开连接

    当客户端或服务器端在不需要发送请求或响应请求时,则可以调用 JsonRpc 实例的 Dispose 方法断开并释放连接。

    jsonRpc.Dispose();
    

    如果需要断开连接,一般是由客户端这边发起,比如对于控制台应用按 Ctrl + C 结束任务便会断开与服务端的连接。那服务端如何知道某个客户端断开了连接呢?可以手动等待 JsonRpc 实例的 Completion 任务完成,比如:

    static async Task ResponseAsync(NamedPipeServerStream stream, int clientId)
    {
        var jsonRpc = JsonRpc.Attach(stream, new GreeterServer());
        await jsonRpc.Completion;
        Console.WriteLine($"客户端 #{clientId} 的已断开连接");
        jsonRpc.Dispose();
        await stream.DisposeAsync();
    }
    

    这里为了保险起见,我还手动把 stream 也释放掉了。

    除了主动断开连接,客户端或服务器抛出未 catch 的异常也会致使连接中断,在实际情况中针对这种异常的连接中断可能需要编写重试机制,这里就不展开讨论了。

    完整代码

    以上为了讲解方便,代码只贴了与上下文相关的部分,最后我再把完整代码贴一下吧。

    服务端 StreamSample.Server 下的 Program.cs:

    class Program
    {
        static async Task Main(string[] args)
        {
            int clientId = 1;
    
            while (true)
            {
                var stream = new NamedPipeServerStream("StringJsonRpc",
                    PipeDirection.InOut,
                    NamedPipeServerStream.MaxAllowedServerInstances,
                    PipeTransmissionMode.Byte,
                    PipeOptions.Asynchronous);
    
                Console.WriteLine("等待客户端连接...");
                await stream.WaitForConnectionAsync();
                Console.WriteLine($"已与客户端 #{clientId} 建立连接");
    
                _ = ResponseAsync(stream, clientId);
    
                clientId++;
            }
        }
    
        static async Task ResponseAsync(NamedPipeServerStream stream, int clientId)
        {
            var jsonRpc = JsonRpc.Attach(stream, new GreeterServer());
            await jsonRpc.Completion;
            Console.WriteLine($"客户端 #{clientId} 的已断开连接");
            jsonRpc.Dispose();
            await stream.DisposeAsync();
        }
    }
    
    public class GreeterServer
    {
        public string SayHello(string name)
        {
            Console.WriteLine($"收到【{name}】的问好,并回复了他");
            return $"您好,{name}!";
        }
    }
    

    客户端 StreamSample.Client 下的 Program.cs:

    class Program
    {
        static async Task Main(string[] args)
        {
            var stream = new NamedPipeClientStream(".",
                "StringJsonRpc",
                PipeDirection.InOut,
                PipeOptions.Asynchronous);
    
            Console.WriteLine("正在连接服务器...");
            await stream.ConnectAsync();
            Console.WriteLine("已建立连接!");
    
            Console.WriteLine("我是精致码农,开始向服务端问好...");
            var jsonRpc = JsonRpc.Attach(stream);
            var message = await jsonRpc.InvokeAsync<string>("SayHello", "精致码农");
            Console.WriteLine($"来自服务端的响应:{message}");
    
            Console.ReadKey();
        }
    }
    

    完整代码已放到 GitHub,地址为:

    github.com/liamwang/StreamJsonRpcSamples

    两个客户端和服务端一起运行的截图:

    本篇总结

    本文通过一个简单但完整的示例讲解了如何使用 StreamJsonRpc 来实现基于 JSON-RPC 协议的 RPC 调用。由于服务端和客户端都使用的是 StreamJsonRpc 库来实现的,所以在示例中感觉不到 JSON-RPC 协议带来的统一规范,也没看到具体的 JSON 格式的数据。这是因为 StreamJsonRpc 库都已经帮我们封装好了,两端都基于 C#,示例使用的也是简单的 Stream 方式,隐藏了我们不必关心的细节。其实只要符合 JSON-RPC 协议标准,C# 写的服务端也可以由其它语言实现的客户端来调用,反之亦然。

    关注我一段时间的朋友都知道,我的文章篇幅一般不会太长,主要是方便大家利用零碎时间把它一次性看完。StreamJsonRpc 的使用远不止本文讲的这些,比如还有基于 WebSocket 进行数据传输的方式。来想通过两篇讲完,但讲了一半就已经超出了预期的篇幅长度。所以我把本文定为[中篇],如果有时间我会继续写[下篇],下篇主要会讲 StreamJsonRpc + WebSocket 的使用,并会尽量以更贴合实际应用场景的示例来讲解。

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

    准备工作

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

    github.com/liamwang/StreamJsonRpcSamples

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

    你可以直接复制并执行下面的命令一键完成大部分准备工作:

    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 项目中添加三个类用来约定客户端和服务端通讯的数据结构和接口。

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

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

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

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

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

    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,这几篇文章包括示例代码应该有值得参考的地方。

  • 相关阅读:
    mac 环境下adb的安装
    iOS开发Swift版本
    UILabel-Swift
    Android应用上架
    Android应用开发
    andriod
    Android 6.0 更新包与已安装应用的签名不一致
    苹果手机怎么录屏 iOS12设置录屏
    Siri语音唤醒的开启与语音矫正
    Git的工作流程
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/13356321.html
Copyright © 2011-2022 走看看