netcore 中的動態代理與RPC實現(微服務專題)
一、關於RPC的呼叫
1. 呼叫者(客戶端Client)以本地呼叫的方式發起呼叫;
2. Client stub(客戶端存根)收到呼叫後,負責將被呼叫的方法名、引數等打包編碼成特定格式的能進行網路傳輸的訊息體;
3. Client stub將訊息體通過網路傳送給服務端;
4. Server stub(服務端存根)收到通過網路接收到訊息後按照相應格式進行拆包解碼,獲取方法名和引數;
5. Server stub根據方法名和引數進行本地呼叫;
6. 被呼叫者(Server)本地呼叫執行後將結果返回給server stub;
7. Server stub將返回值打包編碼成訊息,並通過網路傳送給客戶端;
9. Client得到本次RPC呼叫的最終結果。
參考https://www.cnblogs.com/FG123/p/10261676.html
參考https://www.jianshu.com/p/bb9beca7f7bc 第四節
二、關於RPC呼叫方式的思考(為什麼要用代理類)
RPC的方便之處我們已經看到了,
假設在系統中要呼叫多個服務,如果寫一個函式,每次將這個服務的名字,引數,和其他資訊通過一個方法來呼叫遠端服務,假設這個方法叫做getService(methodname,object[],引數3,引數4)
我們在各個消費類中來呼叫這個方法似乎也能得到結果。
在每個呼叫遠端服務的地方都要反射出 類的方法名稱,引數等其他資訊以能傳給getService 是不是很麻煩?
要知道遠端服務每個服務返回的結果不會是一樣的型別,那我們在客戶端還要每次都轉換getService的結果,是不是很麻煩?
有沒有好的解決方案?
--請使用代理類,我們在代理類中反射代理介面得到這個方法的各種屬性(名稱&引數&其他),遠端呼叫傳遞給遠端服務,並轉換得到的結果。看起來這種方法和上文的getService 差不多嘛!那我們為什麼要使用代理類呢?(我也不知道,但看起來很吊的樣子。)這看起來並沒有很好的樣子,況且如果有多個類要呼叫遠端服務,那豈不是要寫很多代理類?
思考:呼叫getService 後每次都要在消費類中轉換結果,使用代理類後將這個轉換過程放入了代理類中,這樣消費類就不用關注RPC的呼叫結果的型別的轉換了。
於是人們發明了動態代理 --來自《魯迅先生說革命》
人們發現每個類都要寫個代理。現在小明要在專案中寫1000個代理類,直接氣炸了,對!炸了!。
經過了N代的小明客戶鑽研和發現,總結了一套可以很省力氣的方法。--動態代理
簡單的來說:動態建立代理類(https://www.cnblogs.com/netqq/p/11452374.html),這樣就不用給每個消費類都寫一個代理類了,是不很爽
三、動態代理與RPC
在網上找到了一個簡單的RPC 示例,非常適合初學者學習 https://github.com/Coldairarrow/DotNettyRPC
下載專案後先執行 Server 專案,再執行client專案
看到再server的控制檯上輸出了hello 字串。這是客戶端程式呼叫了server的IHello.SayHello()的服務輸出的。
我們來看下作者的客戶端呼叫
RPCClientFactory原始碼如下
namespace Coldairarrow.DotNettyRPC { /// <summary> /// 客戶端工廠 /// </summary> public class RPCClientFactory { private static ConcurrentDictionary<string, object> _services { get; } = new ConcurrentDictionary<string, object>(); /// <summary> /// 獲取客戶端 /// 注:預設服務名為介面名 /// </summary> /// <typeparam name="T">介面定義型別</typeparam> /// <param name="serverIp">遠端服務IP</param> /// <param name="port">遠端服務埠</param> /// <returns></returns> public static T GetClient<T>(string serverIp, int port) where T : class { return GetClient<T>(serverIp, port, typeof(T).Name); } /// <summary> /// 獲取客戶端 /// 注:自定義服務名 /// </summary> /// <typeparam name="T">介面定義型別</typeparam> /// <param name="serverIp">遠端服務IP</param> /// <param name="port">遠端服務埠</param> /// <param name="serviceName">服務名</param> /// <returns></returns> public static T GetClient<T>(string serverIp, int port, string serviceName) where T : class { T service = null; string key = $"{serviceName}-{serverIp}-{port}"; try { service = (T)_services[key]; } catch { var clientProxy = new RPCClientProxy { ServerIp = serverIp, ServerPort = port, ServiceType = typeof(T), ServiceName = serviceName }; service = clientProxy.ActLike<T>(); //動態代理? _services[key] = service; } return service; } } }View Code
在示例中,程式呼叫的GetClient
實際上也是動態生成的代理類,返回了IHello型別的物件。
我們先拋開該作者的程式,用我們自己的動態代理類來實現相同的效果。
在DotNettyRPC專案中新增ProxyDecorator<T> 類。 需要nuget下載System.Reflection.DispatchProxy.dll
在新增ProxyDecorator 和編譯的時候會遇到問題,我們將server、 client專案和DotNettyRPC 轉為NETCORE專案才能正常執行 ,因為 System.Reflection.DispatchProxy.dll 只NETCORE 類庫,不支援NET Framework專案
ProxyDecorator<T> 原始碼
1 public class ProxyDecorator<T> : DispatchProxy 2 { 3 public string ServerIp { get; set; } 4 public int ServerPort { get; set; } 5 public string ServiceName { get; set; } 6 static Bootstrap _bootstrap { get; } 7 static ClientWait _clientWait { get; } = new ClientWait(); 8 9 static ProxyDecorator() 10 { 11 _bootstrap = new Bootstrap() 12 .Group(new MultithreadEventLoopGroup()) 13 .Channel<TcpSocketChannel>() 14 .Option(ChannelOption.TcpNodelay, true) 15 .Handler(new ActionChannelInitializer<ISocketChannel>(channel => 16 { 17 IChannelPipeline pipeline = channel.Pipeline; 18 pipeline.AddLast("framing-enc", new LengthFieldPrepender(8)); 19 pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 8, 0, 8)); 20 21 pipeline.AddLast(new ClientHandler(_clientWait)); 22 })); 23 } 24 25 public ProxyDecorator() 26 { 27 28 } 29 30 ///// <summary> 31 ///// 建立代理例項 32 ///// </summary> 33 ///// <param name="decorated">代理的介面型別</param> 34 ///// <returns></returns> 35 public T Create(string serverIp, int port, string serviceName) 36 { 37 38 object proxy = Create<T, ProxyDecorator<T>>(); //呼叫DispatchProxy 的Create 建立一個新的T 39 ((ProxyDecorator<T>)proxy).ServerIp = serverIp; 40 ((ProxyDecorator<T>)proxy).ServerPort = port; 41 ((ProxyDecorator<T>)proxy).ServiceName = serviceName; 42 return (T)proxy; 43 } 44 45 protected override object Invoke(MethodInfo targetMethod, object[] args) 46 { 47 if (targetMethod == null) throw new Exception("無效的方法"); 48 49 try 50 { 51 52 ResponseModel response = null; 53 IChannel client = null; 54 try 55 { 56 client = AsyncHelpers.RunSync(() => _bootstrap.ConnectAsync($"{ServerIp}:{ServerPort}".ToIPEndPoint())); 57 } 58 catch 59 { 60 throw new Exception("連線到服務端失敗!"); 61 } 62 if (client != null) 63 { 64 _clientWait.Start(client.Id.AsShortText()); 65 RequestModel requestModel = new RequestModel 66 { 67 ServiceName = ServiceName, 68 MethodName = targetMethod.Name, 69 Paramters = args.ToList() 70 }; 71 var sendBuffer = Unpooled.WrappedBuffer(requestModel.ToJson().ToBytes(Encoding.UTF8)); 72 73 client.WriteAndFlushAsync(sendBuffer); 74 var responseStr = _clientWait.Wait(client.Id.AsShortText()).ResponseString; 75 response = responseStr.ToObject<ResponseModel>(); 76 } 77 else 78 { 79 throw new Exception("連線到服務端失敗!"); 80 } 81 82 if (response == null) 83 throw new Exception("伺服器超時未響應"); 84 else if (response.Success) 85 { 86 Type returnType = targetMethod.ReturnType; 87 if (returnType == typeof(void)) 88 return null; 89 else 90 return response.Data; 91 } 92 else 93 throw new Exception($"伺服器異常,錯誤訊息:{response.Msg}"); 94 95 } 96 catch (Exception ex) 97 { 98 if (ex is TargetInvocationException) 99 { 100 LogException(ex.InnerException ?? ex, targetMethod); 101 throw ex.InnerException ?? ex; 102 } 103 else 104 { 105 throw ex; 106 } 107 } 108 } 109 110 111 /// <summary> 112 /// aop異常的處理 113 /// </summary> 114 /// <param name="exception"></param> 115 /// <param name="methodInfo"></param> 116 private void LogException(Exception exception, MethodInfo methodInfo = null) 117 { 118 try 119 { 120 var errorMessage = new StringBuilder(); 121 errorMessage.AppendLine($"Class {methodInfo.IsAbstract.GetType().FullName}"); 122 errorMessage.AppendLine($"Method {methodInfo?.Name} threw exception"); 123 errorMessage.AppendLine(exception.Message); 124 125 //_logError?.Invoke(errorMessage.ToString()); 記錄到檔案系統 126 } 127 catch (Exception) 128 { 129 // ignored 130 //Method should return original exception 131 } 132 }View Code
這個類的原始碼與上一篇反向代理文章中所講的核心區別是 Invoke 的實現,上篇文章中其呼叫的是本地的一個類實體的方法,本文中其呼叫的是遠端服務中的類實體的方法
client呼叫程式碼如下
static void Main(string[] args) { //IHello client = RPCClientFactory.GetClient<IHello>("127.0.0.1", 39999); var serviceProxy = new ProxyDecorator<IHello>(); IHello client = serviceProxy.Create("127.0.0.1", 39999, "IHello"); client.SayHello("Hello"); Console.WriteLine("完成"); Console.ReadLine(); }
重新啟動Server 和Client 執行效果如下
和原作者的執行結果一致,那麼我們換個介面來試試:建立IMail 和Mail兩個類,幷包含一個成員string Send(string name)//IMail和Mail的成員
public string Send(string name) {
Console.WriteLine(name); return $"你的名字是{name}"; }
Client端呼叫程式碼
static void Main(string[] args) { //IHello client = RPCClientFactory.GetClient<IHello>("127.0.0.1", 39999); //var serviceProxy = new ProxyDecorator<IHello>(); //IHello client = serviceProxy.Create("127.0.0.1", 39999, "IHello"); var serviceProxy = new ProxyDecorator<IMail>(); IMail client = serviceProxy.Create("127.0.0.1", 39999, "IMail"); string msg= client.Send("張三丰"); Console.WriteLine(msg); Console.WriteLine("完成"); Console.ReadLine(); }
服務端新增服務監控
static void Main(string[] args) { RPCServer rPCServer = new RPCServer(39999); rPCServer.RegisterService<IHello, Hello>(); rPCServer.RegisterService<IMail, Mail>(); rPCServer.Start(); Console.ReadLine(); }
預計客戶端輸出:
你的名字是張三丰
完成
服務端輸出是:
張三丰
我們先後啟動server 和 client 兩個端來看看
至此動態代理的應用示例已經演示完畢。
在檢視 寒空飛箭 git 原始碼時候我們發現 RPCClientProxy 類和我們的ProxyDecorator<T> 類 實現了相同的效果,寒空飛箭的實現方式也是很令人振奮,獨闢蹊徑,非常值得學習。下篇文章將會分析他的用法。感興趣的可以自行檢視作者的原始碼。
參考文獻:
https://www.cnblogs.com/coldairarrow/p/10193765.html
說明:
RPC功能的實現是直接引用作者 寒空飛箭 的程式碼,對此向 寒空飛箭 表示感謝
&n