基于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集群的数十万个连接。
快速入门
强烈推荐安装NewLife项目模板,以下命令可直接运行RpcServer示例项目:
dotnet new install NewLife.Templates
dotnet new rpcserver -n testserver
cd testserver
dotnet run
新建控制台项目,写入以下代码:
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);
}