NetCore版RPC框架NewLife.ApiServer


微服務和消息隊列的基礎都是RPC框架,比較有名的有WCF、gRPC、Dubbo等,我們的NewLife.ApiServer建立在網絡庫NewLife.Net之上,支持.Net Core,追求輕量級和高性能,只有最簡單的遠程調用功能。

現在是網絡系列文章的第五篇,前面四篇快速過了一遍網絡庫基本用法,也做了壓力測試並給出數字 2266萬tps

本章正式進入應用層面,並且采用.Net Core作為例程,說明我們一開始就支持.Net Core,也算是回答了很多支持者的疑問。

 

老規矩,先上代碼:https://github.com/NewLifeX/NewLife.Net (例程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主要特點如下:

  1. 支持.Net Core/Net40/Net45,這個最近太熱門了,其實X組件絕大部分功能都支持.Net Core
  2. 多年積累。從2014年起,遇到並解決了很多問題,也去掉了很多可選功能,只保留必要功能
  3. 性能尚可。網絡庫2266tps,ApiServer在40核服務器上單客戶端帶業務測試得到16萬tps
  4. 簡單易用。高仿MVC的Controller風格,支持上下文和執行前后過濾器,客戶端直接Invoke,無需生成Stub代碼,參數無需完全一致,便於多版本兼容
  5. 容易調試。默認通信參數和返回采用Json封送,打開編碼器日志后,遠程調用的收發一目了然。(網絡庫的高性能就是用來給Json浪費的……)
  6. 大包請求。支持收發大數據包(如1M~1000M),特殊服務接口避開Json序列化,直接走二進制。
  7. 支持異常。服務接口拋出的異常,能夠封裝傳遞到客戶端

 

三、服務端例程

新建.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#大數據!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM