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


    阅读本文大概需要 11 分钟。

    上一篇介绍了一些预备知识,包括 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 的使用,并会尽量以更贴合实际应用场景的示例来讲解。

  • 相关阅读:
    前端 CSS 基础
    前端 HTML基础
    前端 JavaScript基础
    GoldenGate 复制进程报错"OGG-01296 Error mapping",丢弃文件报错“Mapping problem with delete record (target format)”,且实际条目存在
    SaltStack 与 Python 程序的结合
    SUSE-11 本地 zypper 配置
    Centos-7 + Docker-1.12 中 devicemapper + direct_lvm 的 Docker 存储配置
    Docker-v17 的层级(layer)概念
    Oracle-11g 中使用表空间透明数据加密(TDE)
    SaltStack 的远程执行机制
  • 原文地址:https://www.cnblogs.com/willick/p/13253344.html
Copyright © 2020-2023  润新知