微服务和消息队列的基础都是 RPC 框架, 比较有名的有 WCF,gRPC,Dubbo 等, 我们的 NewLife.ApiServer 建立在网络库 NewLife.Net 之上, 支持. Net Core, 追求轻量级和高性能, 只有最简单的远程调用功能.
现在是网络系列文章的第五篇, 前面四篇快速过了一遍网络库基本用法, 也做了压力测试并给出数字 2266 万 tps.
本章正式进入应用层面, 并且采用. Net Core 作为例程, 说明我们一开始就支持. Net Core, 也算是回答了很多支持者的疑问.
老规矩, 先上代码: https://github.com/nnhy/NewLife.Net.Tests (例程 RpcTest)
ApiServer 源码: https://github.com/NewLifeX/X/tree/master/NewLife.Core/Remoting
ApiServer 实在太小了, 就让它和 Net 一起分别作为 X 组件核心库的一个目录.
一, 背景
ApiServer 开始于 2014 年, 我们为了建立物联网云平台, 解决云端, 硬件设备端, 手机端, 网页端相互通信, 而建立的一套完整的通信体系.
公司业务需要, 在 ApiServer 上建立了包括服务治理, 注册发现, 负载均衡, 设备鉴权, 通信加密, 压缩, P2SP 网络, webSocket 等等一系列模块.
这一套物联网云平台已经用在很多家公司上, 根据 NewLife 两年解封惯例, 大概在 2019 年开源放出大部分源码.
本文所指的 ApiServer, 仅指开源的 RPC 部分.
2017 年 4 月 1 日晚, 我们想知道 ApiServer 的表现, 做了一次最大并发数测试, 目标是单节点支持 100 万设备接入.
租用 60 台阿里云 ECS, 实际测试单节点最大支持 84.5 万模拟设备接入, 设备端的心跳包(5~60s) 拖垮了 32 核服务端.
二, 功能特点
先看看例程最终效果:
ApiServer 主要特点如下:
支持. Net Core/Net40/Net45, 这个最近太热门了, 其实 X 组件绝大部分功能都支持. Net Core
多年积累. 从 2014 年起, 遇到并解决了很多问题, 也去掉了很多可选功能, 只保留必要功能
性能尚可. 网络库 2266tps,ApiServer 在 40 核服务器上单客户端带业务测试得到 16 万 tps
简单易用. 高仿 MVC 的 Controller 风格, 支持上下文和执行前后过滤器, 客户端直接 Invoke, 无需生成 Stub 代码, 参数无需完全一致, 便于多版本兼容
容易调试. 默认通信参数和返回采用 Json 封送, 打开编码器日志后, 远程调用的收发一目了然.(网络库的高性能就是用来给 Json 浪费的......)
大包请求. 支持收发大数据包(如 1M~1000M), 特殊服务接口避开 Json 序列化, 直接走二进制.
支持异常. 服务接口抛出的异常, 能够封装传递到客户端
三, 服务端例程
新建. Net Core 2.0 项目 RpcTest, 我们把服务端客户端代码写到一起.
服务暴露高仿 MVC, 一个控制器内可以暴露多个服务方法
- /// <summary > 自定义控制器. 包含多个服务</summary>
- class MyController
- {
- /// <summary > 添加, 标准业务服务, 走 Json 序列化</summary>
- /// <param name="x"></param>
- /// <param name="y"></param>
- /// <returns></returns>
- public Int32 Add(Int32 x, Int32 y) => x + y;
- /// <summary>RC4 加解密, 高速业务服务, 二进制收发不经序列化</summary>
- /// <param name="pk"></param>
- /// <returns></returns>
- public Packet RC4(Packet pk)
- {
- var data = pk.ToArray();
- var pass = "NewLife".GetBytes();
- return data.RC4(pass);
- }
- }
这里暴露了两个服务, 分别是 加法 My/Add 和 加密 My/RC4 , 控制器名称加上方法名, 作为寻址路径.
不使用 Api 特性时, 控制器类的所有共有方法都将暴露成为服务.
返回值比较简单支持, 该什么类型就什么类型. 理论上来说, 支持 Json 序列化的类型, 都可以作为参数和返回类型.
服务方法也可以指定名称, 支持方法过滤接口
- /// <summary > 用户控制器. 会话获取, 请求过滤</summary>
- [Api("User")]
- class UserController : IApi, IActionFilter
- {
- /// <summary > 会话. 同一 Tcp/Udp 会话多次请求共用, 执行服务方法前赋值</summary>
- public IApiSession Session { get; set; }
- [Api(nameof(FindByID))]
- public User FindByID(Int32 uid, Boolean deleted)
- {
- // Session 用法同 Web
- var times = Session["Times"].ToInt();
- times++;
- Session["Times"] = times;
- // 故意制造异常
- if (times>= 2)
- {
- // 取得当前上下文
- var ctx = ControllerContext.Current;
- throw new ApiException(507, "[{0}]调用次数过多! Times={1}".F(ctx.ActionName, times));
- }
- var user = new User
- {
- ID = uid,
- Name = Rand.NextString(8),
- Enable = deleted,
- CreateTime = DateTime.Now,
- };
- return user;
- }
- /// <summary > 本控制器执行前</summary>
- /// <param name="filterContext"></param>
- public void OnActionExecuting(ControllerContext filterContext)
- {
- // 请求参数
- var ps = filterContext.Parameters;
- // 服务参数
- var cs = filterContext.ActionParameters;
- foreach (var item in ps)
- {
- if (cs != null && !cs.ContainsKey(item.Key))
- XTrace.WriteLine("服务 [{0}] 未能找到匹配参数 {1}={2}", filterContext.ActionName, item.Key, item.Value);
- }
- }
- /// <summary > 本控制器执行后, 包括异常发生</summary>
- /// <param name="filterContext"></param>
- public void OnActionExecuted(ControllerContext filterContext)
- {
- var ex = filterContext.Exception;
- if (ex != null && !filterContext.ExceptionHandled)
- {
- XTrace.WriteLine("控制器拦截到异常:{0}", ex.Message);
- }
- }
- }
这里控制器和方法都加上了 Api 特性, 特别指定了名称, 公开服务 User/FindByID.
这里有个硬伤, 如果不加 Api 特性, 默认会把 OnActionExecuting/OnActionExecuted 两个方法也暴露成为服务.
实现 Api 接口, 是为了得到 Session, 这个不是必须的, 因为控制器上下文 ControllerContext.Current 也可以得到这个 Session.
这个 Session 代表着网络会话, 可以取得各种跟网络相关的东西, 甚至包括直接向客户端发送数据.
当然, 也可以当做 Web 的 Session 来使用, 内置有一个字典.
同一客户端的 Api 多次请求, 都共用同一个 Session 对象, 可用于做身份验证, 从某种层面上来讲, ApiServer 是 "有状态" 的.
动作过滤接口 IActionFilter, 让我们能够在本控制器所有服务执行前后进行拦截, 包括参数预处理和异常拦截.
服务参数采用 Json 序列化封送, 所以客户端服务端可以不必要求严格一致, 跟 Http 类似, 这一点在多版本管理上非常重要, 不会说你加了个参数就强制要求所有客户端跟着升级.
服务方法内的各种异常, 都将会被拦截并送到客户端, ApiException 异常将会得到特殊处理, 它包括了一个异常代码, 也送到客户端.
没有异常代码的各种异常, 都将使用默认错误代码 500.
最后实例化 ApiServer
- static void TestServer()
- {
- // 实例化 RPC 服务端, 指定端口, 同时在 Tcp/Udp/IPv4/IPv6 上监听
- var svr = new ApiServer(1234);
- // 注册服务控制器
- svr.Register<MyController>();
- svr.Register<UserController>();
- // 指定编码器
- svr.Encoder = new JsonEncoder();
- svr.EncoderLog = XTrace.Log;
- // 打开原始数据日志
- var ns = svr.Server as NetServer;
- ns.Log = XTrace.Log;
- ns.LogSend = true;
- ns.LogReceive = true;
- svr.Log = XTrace.Log;
- svr.Start();
- _server = svr;
- // 定时显示性能数据
- _timer = new TimerX(ShowStat, ns, 100, 1000);
- }
中间打开的各种日志, 纯属为了便于展示通信过程, 实际应用中务必去除!
ApiServer 采用手工注册控制器的方式, 避免了复杂的 MVC 路由系统.
内置有一个控制器 ApiController, 它的 All 服务用于向客户端返回所有可用服务列表.
服务端建立起来后, 可以用码神工具的 Api 工具调试,( https://github.com/NewLifeX/X/tree/master/XCoder )
四, 客户端例程
为了便于使用, 封装一个客户端类
- /// <summary > 自定义业务客户端</summary>
- class MyClient : ApiClient
- {
- public MyClient(String uri) : base(uri) { }
- /// <summary > 添加, 标准业务服务, 走 Json 序列化</summary>
- /// <param name="x"></param>
- /// <param name="y"></param>
- /// <returns></returns>
- public async Task<Int32> AddAsync(Int32 x, Int32 y)
- {
- return await InvokeAsync<Int32>("My/Add", new { x, y });
- }
- /// <summary>RC4 加解密, 高速业务服务, 二进制收发不经序列化</summary>
- /// <param name="pk"></param>
- /// <returns></returns>
- public async Task<Packet> RC4Async(Packet pk)
- {
- return await InvokeAsync<Packet>("My/RC4", pk);
- }
- public async Task<User> FindUserAsync(Int32 uid, Boolean enable)
- {
- return await InvokeAsync<User>("User/FindByID", new { uid, enable });
- }
- }
其实这个类不是必须的, 看个人喜好吧.
- static async void TestClient()
- {
- var client = new MyClient("tcp://127.0.0.1:1234");
- // 指定编码器
- client.Encoder = new JsonEncoder();
- client.EncoderLog = XTrace.Log;
- // 打开原始数据日志
- var ns = client.Client;
- ns.Log = XTrace.Log;
- ns.LogSend = true;
- ns.LogReceive = true;
- client.Log = XTrace.Log;
- client.Open();
- // 定时显示性能数据
- _timer = new TimerX(ShowStat, ns, 100, 1000);
- // 标准服务, Json
- var n = await client.AddAsync(1245, 3456);
- XTrace.WriteLine("Add: {0}", n);
- // 高速服务, 二进制
- var buf = "Hello".GetBytes();
- var pk = await client.RC4Async(buf);
- XTrace.WriteLine("RC4: {0}", pk.ToHex());
- // 返回对象
- var user = await client.FindUserAsync(123, true);
- XTrace.WriteLine("FindUser: ID={0} Name={1} Enable={2} CreateTime={3}", user.ID, user.Name, user.Enable, user.CreateTime);
- // 拦截异常
- try
- {
- user = await client.FindUserAsync(123, true);
- }
- catch (ApiException ex)
- {
- XTrace.WriteLine("FindUser 出错, 错误码 ={0}, 内容 ={1}", ex.Code, ex.Message);
- }
- }
这里做了 4 次不同调用, 模拟了常见场景.
五, 总结
编译后跑起来就是开头的效果, 感兴趣的同学还可以到 Linux 上试试, 也可以新建 Net40/Net45 项目, 同样可用.
并且, Net40 项目还可以在树莓派上跑, 基于 Mono, 码神工具 (WinForm) 也支持.
RpcTest 例程概括性讲解了 ApiServer 的用法, 大家可以去尝试, 扩展.
实际工作中, 我们正准备用于建立一个每天数十亿次调用的微服务系统.
我是大石头, 打 1999 年起, 19 年老码农. 目前在物流行业从事数据分析架构工作, 日常工作都是亿万数据的读写使用. 欢迎大家一起 C# 大数据!
来源: https://www.cnblogs.com/nnhy/p/newlife_apiserver.html