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主要特點如下:
- 支持.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#大數據!

浙公網安備 33010602011771號