基于SRMP协议的RPC通信框架,固定4字节二进制协议头,默认json传递参数和返回值,高速场景可使用二进制参数和返回值,避免json序列化损耗。
Nuget包:NewLife.Remoting
源码地址:https://github.com/NewLifeX/NewLife.Remoting/blob/master/NewLife.Remoting/ApiServer.cs
ApiServer底层基于4字节头的《SRMP协议》实现,再辅以Json编码器(可自定义)实现方法和出入参的编码。
功能特性
RPC框架ApiServer具有以下特点:
- 简单易用,不需要写描述文件生成代码;
- 友好协商,客户端服务端之间不需要共用接口代码,只需要方法名和参数名正确;
- 方便测试,RPC接口支持Http访问,便于测试;
- 高速通信,支持二进制通信,高性能要求的接口不必受制于Json;
- 分布式集群,支持负载均衡和故障转移;
- 坚如磐石,大企业日均100亿级调用量;
- 服务治理,接入星尘监控和星尘注册中心,NewLife全家桶成员;
ApiServer的最强案例是一个Redis网关,8个网关节点抗住背后48个Redis实例的百万级ops吞吐,向50多个高性能应用提供RPC服务,抵消这些应用对Redis集群的数十万个连接。
快速入门
新建控制台项目,写入以下代码:
using NewLife.Data; using NewLife.Log; using NewLife.Net; using NewLife.Remoting; using System; using System.Linq; using System.Net; namespace NewLife.RPC { class Program { static void Main(string[] args) { XTrace.UseConsole(); var netUri = new NetUri(NetType.Tcp, IPAddress.Any, 5001); using var server = new ApiServer(netUri) { Log = XTrace.Log, EncoderLog = XTrace.Log, ShowError = true }; server.Register<BigController>(); server.Start(); ClientTest(); Console.ReadKey(); } static void ClientTest() { var client = new ApiClient("tcp://127.0.0.1:5001") { Log = XTrace.Log, EncoderLog = XTrace.Log }; var rs = client.Invoke<Int32>("Big/Sum", new { a = 123, b = 456 }); XTrace.WriteLine("{0}+{1}={2}", 123, 456, rs); } class BigController { public int Sum(int a, int b) => a + b; public string ToUpper(string str) => str.ToUpper(); public Packet Test(Packet pk) { var buf = pk.ReadBytes().Select(e => (Byte)(e ^ 'x')).ToArray(); return buf; } } } }
F5跑起来
这是一个标准例程,实例化ApiServer后注册一个控制器BigController,把它内部的几个方法公开成为接口。然后客户端直接Invoke调用,指定方法名 Big/Sum 和参数(匿名对象),成功调用目标接口并返回。
从日志中可以看到,客户端调用 Big/Sum 接口,参数是Json格式的 {"a":123,"b":456} 。服务端收到后映射到接口参数,处理后封装返回值(这里是整型看不出来)。
因此,RPC调用本质上就是封装了方法、入参、出参的远程调用!
基础功能
ApiServer最重要的三个成员,分别是端口、控制器、开始方法。
前面示例的最简代码如下:
using var server = new ApiServer(5001); server.Register<BigController>(); server.Start();
实例化时指定端口,然后注册控制器接口,在Start开始服务。一切都是那么简单!
日志
Log属性,指定服务端日志,一般指定为 XTrace.Log,让它跟全局一致,默认输出控制台和日志文件;
EncoderLog属性,编码器日志,仅用于开发调试,一定一定不要在生产环境打开,否则会写爆日志;
20:26:30.228 1 N - 启动ApiServer,服务器 Api 20:26:30.228 1 N - 编码:NewLife.Remoting.JsonEncoder 20:26:30.229 1 N - 处理:NewLife.Remoting.ApiHandler 20:26:30.353 1 N - Api 准备开始监听2个服务器 20:26:30.369 1 N - Api 开始监听 Tcp://0.0.0.0:5001 20:26:30.370 1 N - Api 开始监听 Tcp://[::]:5001 20:26:30.371 1 N - Api 准备就绪! 20:26:30.373 1 N - 可用服务6个: 20:26:30.377 1 N - Api/All String[] All() NewLife.Remoting.ApiController 20:26:30.378 1 N - Api/Info Object Info(String state) NewLife.Remoting.ApiController 20:26:30.378 1 N - Api/Info2 Packet Info2(Packet state) NewLife.Remoting.ApiController 20:26:30.378 1 N - Big/Sum Int32 Sum(Int32 a, Int32 b) NewLife.RPC.Program+BigController 20:26:30.378 1 N - Big/ToUpper String ToUpper(String str) NewLife.RPC.Program+BigController 20:26:30.379 1 N - Big/Test Packet Test(Packet pk) NewLife.RPC.Program+BigController 20:26:30.392 6 Y 1 Api 集群:NewLife.Remoting.ClientSingleCluster 20:26:30.438 6 Y 1 集群转移:tcp://127.0.0.1:5001
如上是服务端Log日志,数量有限,以下是EncoderLog编码器日志,每一次请求响应都有:
20:26:30.429 6 Y 1 Big/Sum=>{"a":123,"b":456} 20:26:30.612 4 Y - Big/Sum[01]<={"a":123,"b":456} 20:26:30.618 4 Y - Big/Sum[01]=>579 20:26:30.622 4 Y - Big/Sum[01]<=579
服务注册
ApiServer监听端口后,需要根据方法名路由到具体接口。
void Register(object controller, string method); void Register<TService>() where TService : class, new();
前者注册控制器对象的指定方法,method为空时注册所有公开实例方法作为接口,客户端调用时,多次调用共用这个控制器对象;
后者注册控制器类型,注册所有公开实例方法作为接口,客户端调用时,每一次调用都重新实例化控制器对象,该逻辑跟Asp.Net一致;
服务控制
服务控制只有启动和停止,停止时需要制定原因,记录到日志。Dispose释放对象时也会调用Stop停止。
void Start(); void Stop(string reason);
ApiServer默认将会在UDP/TCP/IPv4/IPv6上同时监听指定端口,内网使用时,UDP+IPv6拥有最佳吞吐,公网使用时TCP+IPv4更稳定。
RPC框架自身采用请求响应机制,在可靠网络中无需TCP的ACK确认,且UDP能够避免底层TCP粘包处理压力。
超时设置
Timeout属性,调用超时时间。请求发出后,等待响应的最大时间,默认15_000ms。
慢调用
SlowTrace属性,慢追踪。远程调用或处理时间超过该值时,输出慢调用日志,默认5000ms。
高级功能
ApiServer还有着许多很有用的高级功能。
性能追踪
Tracer属性,指定APM追踪器,结合星尘监控使用时,可用于追踪每一个接口的调用次数、错误次数、耗时等,并作出灵活告警。
蚂蚁调度系统由ApiServer提供高性能通信支持,以下是它的APM数据:
其中 rps: 开头的三个接口,就是蚂蚁调度的核心接口!
星尘监控传送门:http://star.newlifex.com/Monitors/appDayStat?appId=34
ApiServer和ApiClient支持ITracer穿透,把客户端服务端使用同一个TraceId关联起来!
连接会话
不管是TCP还是UDP,客户端的调用都具有一个会话连接的概念。NewLife.Net网络库把UDP中来自同一个远程地址端口的客户端归到一个连接会话中。
在业务开发过程中,常常遇到需要在会话上下文共用数据的需求。可以给控制器实现IApi接口,其中的Session属性,将会在调用接口之前得到赋值。
/// <summary>Api接口</summary> /// <remarks> /// 在基于令牌Token的无状态验证模式中,可以借助Token重写IApiHandler.Prepare,来达到同一个Token共用相同的IApiSession.Items /// </remarks> public interface IApi { /// <summary>会话</summary> IApiSession Session { get; set; } }
例子
var rs = new { Server = asmx?.Name, asmx?.Version, asmx?.Compile, OS = _OS, MachineName = _MachineName, UserName = _UserName, ApiVersion = asmx2?.Version, LocalIP = _LocalIP, Remote = ns?.Remote?.EndPoint + "", State = state, LastState = Session?["State"], Time = DateTime.Now, }; // 记录上一次状态 if (Session != null) Session["State"] = state;
先后访问以下两个地址,将看到State和LastState的不同
http://ant.newlifex.com:9999/api/info?state=stone
http://ant.newlifex.com:9999/api/info?state=newlife
Session除了可用于存储会话数据以外,还可访问到服务器的其它会话连接,做你想做的事情!
下行指令
IApiSession还可以由服务端主动向客户端发送消息,调用客户端指定接口。
/// <summary>单向远程调用,无需等待返回</summary> /// <param name="action">服务操作</param> /// <param name="args">参数</param> /// <param name="flag">标识</param> /// <returns></returns> Int32 InvokeOneWay(String action, Object args = null, Byte flag = 0);
InvokeOneWay只发送请求包,客户端无需发送响应,起到通知客户端的效果。
修改前面的示例,加上服务端主动下发代码:
class BigController : IApi { public IApiSession Session { get; set; } public int Sum(int a, int b) { Task.Run(async () => { await Task.Delay(1000); Session.InvokeOneWay("test", new { name = "Stone", company = "NewLife" }, 3); }); return a + b; } }
在Sum接口内,调用了 Session.InvokeOneWay。客户端也需要修改一下:
static void ClientTest() { var client = new MyClient("tcp://127.0.0.1:5001") { Log = XTrace.Log, EncoderLog = XTrace.Log }; var rs = client.Invoke<Int32>("Big/Sum", new { a = 123, b = 456 }); XTrace.WriteLine("{0}+{1}={2}", 123, 456, rs); } class MyClient : ApiClient { public MyClient(String urls) : base(urls) { } protected override void OnReceive(IMessage message) { if (Encoder.Decode(message, out var action, out _, out var args)) { XTrace.WriteLine("通知:{0} 参数:{1}", action, args.ToStr()); } } }
MyClient类继承自ApiClient,重写了 OnReceive 方法,所有未经处理的消息,都将进入这里。使用编码器解码后即可得到想要的数据。
动作过滤器
支持动作过滤器IActionFilter,控制器类可实现该接口,用于控制器内接口方法执行前后的业务处理。使用方法跟ASP.Net保持一致。
/// <summary>定义操作筛选器中使用的方法。</summary> public interface IActionFilter { /// <summary>在执行操作方法之前调用。</summary> /// <param name="filterContext"></param> void OnActionExecuting(ControllerContext filterContext); /// <summary>在执行操作方法后调用。</summary> /// <param name="filterContext"></param> void OnActionExecuted(ControllerContext filterContext); }
执行前方法,一般用于授权校验,执行后方法,一般用于异常处理。
如果多个控制器需要使用相同的授权校验逻辑,可以搞一个公共基类,其中实现该接口。
自定义编码器
Encoder属性,默认指定Json编码器,可以通过实现IEncoder接口实现自己的编码器。
SRMP协议中详细讲解了RPC负载数据格式。
请求格式:
响应格式:
编码器IEncoder决定了请求响应的Body负载数据部分。默认的Json编码器在中小数据传输时,性能和便捷性达到了平衡。
自2015年以来,就没有真正使用过Json以外的编码器。虽然实现过其它编码器,最终屈服于Json的便捷性和通用性。
超高速通信
Json编码出入参毕竟是一种损耗,压测发现,在高吞吐系统中,Json序列化耗时75%以上,远超过网络上的损耗。因此,有必要设计一种支持高吞吐的编码方式,考虑到Json以其便捷性成为编码器事实上的标准,我们无法再造一个二进制编码器,那样对低吞吐接口是一种伤害。(ApiServer只能指定一种编码器供所有接口使用,且必须跟客户端保持一致)
最终在ApiServer底层直接支持二进制传输!
超高速通信规则:接口函数只有一个参数,且参数类型和返回类型都是Packet,RPC将跳过编解码和反射,直接调用接口函数,完全由用户自己处理未拷贝的二进制数据!
注:Packet类似于Span<Byte>,本质上是对字节数组一定范围区域的封装。
例如内置ApiController的Info2就是一个高速接口:
/// <summary>服务器信息,用户健康检测,二进制压测</summary> /// <param name="state">状态信息</param> /// <returns></returns> public Packet Info2(Packet state);
这种接口,可以让用户实现任意功能,例如文件分片传输等。
借助二进制序列化,可以灵活传输自定义消息,参考《二进制序列化》。
自定义接口注册
Manager属性,IApiManager接口,管理着所有的路由注册与查找,可以根据需要重写。
// // 摘要: // 接口管理器 public interface IApiManager { // // 摘要: // 可提供服务的方法 IDictionary<string, ApiAction> Services { get; } // // 摘要: // 查找服务 // // 参数: // action: ApiAction Find(string action); // // 摘要: // 注册服务提供类。该类的所有公开方法将直接暴露 // // 类型参数: // TService: void Register<TService>() where TService : class, new(); // // 摘要: // 注册服务 // // 参数: // controller: // 控制器对象 // // method: // 动作名称。为空时遍历控制器所有公有成员方法 void Register(object controller, string method); }