基于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具有以下特点:

  1. 简单易用,不需要写描述文件生成代码;
  2. 友好协商,客户端服务端之间不需要共用接口代码,只需要方法名和参数名正确;
  3. 方便测试,RPC接口支持Http访问,便于测试;
  4. 高速通信,支持二进制通信,高性能要求的接口不必受制于Json;
  5. 分布式集群,支持负载均衡和故障转移;
  6. 坚如磐石,大企业日均100亿级调用量;
  7. 服务治理,接入星尘监控和星尘注册中心,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);
}