NET Core微服務之路:讓我們對上一個Demo通訊進行修改,完成RPC通訊

最近一段時間有些事情耽擱了更新,抱歉各位了。
上一篇我們簡單的介紹了DotNetty通信框架,并簡單的介紹了基于DotNetty實現了回路(Echo)通信過程。
我們來回憶一下上一個項目的整個流程:
  1. 當服務端啟動后,綁定并監聽(READ)設定的端口,比如1889。
  2. 當客戶端啟動后,綁定指定端口,等待用戶輸入。
  3. 當用戶輸入任意字符串數據后,客戶端將這組數據進行轉碼為byte格式進行傳輸到服務端。
  4. 當服務端收到客戶端傳來的數據,進行轉碼后輸出控制臺,并將這組數據再次回傳到客戶端。
  5. 客戶端收到數據,也打印出來。

?

很簡單的實現了一個點對點的通信例子。接下來我們將對這個DEMO進行簡單的修改,模擬最簡單的gRPC通信的一個構造過程。
本篇很簡單,只要實現了上一個demo,稍作修改,就能實現gRPC了(當然實際構建gRPC根本不會這么簡單),本篇也是順帶一下這幾天搞出來的一個輕量級RPC框架,先接上一個例子。

服務端

增加兩個靜態方法SayHello和SayByebye,用于提供遠程調用,超級簡單,不解釋。

public static class Say
{public static string SayHello(string content){return $"hello {content}";}public static string SayByebye(string content){return $"byebye {content}";}
}

?

?

在我們原來的ChannelRead函數中,將原有的Echo回路傳輸,直接替換成如下內容。

 1 public override void ChannelRead(IChannelHandlerContext context, object message)
 2 {
 3     if (message is IByteBuffer buffer)
 4     {
 5         Console.WriteLine($"message length is {buffer.Capacity}");
 6         var obj = JsonConvert.DeserializeObject<Dictionary<string, string>>(buffer.ToString(Encoding.UTF8).Replace(")", "")); // (1)
 7 
 8         byte[] msg = null;
 9         if (obj["func"].Contains("sayHello"))  // (2)
10         {
11             msg = Encoding.UTF8.GetBytes(Say.SayHello(json["username"]));
12         }
13 
14         if (obj["func"].Contains("sayByebye")) // (2)
15         {
16             msg = Encoding.UTF8.GetBytes(Say.SayByebye(json["username"]));
17         }
18 
19         if (msg == null) return;
20         // 設置Buffer大小
21         var b = Unpooled.Buffer(msg.Length, msg.Length); // (3)
22         IByteBuffer byteBuffer = b.WriteBytes(msg); // (4)
23         context.WriteAsync(byteBuffer); // (5)
24     }
25 }

?

?

(1):有這樣一句話Replace(")", ""),筆者不知為何每次傳送過來從buffer里轉義出來的字符串,始終會有一個左括號在里面,也許是消息頭,也許是protobuf-net的標記頭,因為都是byte格式,在服務端偷懶就沒有再進行一次protobuf的反序列化了。
為何要用Dictionary來作為中間對象轉換,因為序列化需要實體對象作為類型,為了簡單的介紹RPC,目前也就這么干了,例如上面代碼所示。
(2):通過判斷“func”字段中的內容進行方法調用,并將調用過程的返回結果轉為BYTE格式。
(3):設置本次傳輸中的Buffer大小。
(4):將消息(數據)寫入到DotNetty的Buffer。
(5):最終將Buffer寫入到當前上下文(包含通道,傳輸對象,連接對象等等)。

客戶端

我們將上一個demo中的EchoClientHandler做如下修改,以完成一個簡單的請求

 1 public EchoClientHandler()
 2 {
 3     var hello = new Dictionary<string, string> // (1)
 4     {
 5         {"func", "sayHello"},
 6         {"username", "stevelee"}
 7     };
 8     SendMessage(ToStream(JsonConvert.SerializeObject(hello)));
 9 }
10 
11 private byte[] ToStream(string msg)
12 {
13     Console.WriteLine($"string length is {msg.Length}");
14     using (var stream = new MemoryStream()) // (2)
15     {
16         Serializer.Serialize(stream, msg);
17         return stream.ToArray();
18     }
19 }
20 
21 private void SendMessage(byte[] msg)
22 {
23     Console.WriteLine($"byte length is {msg.Length}");
24     _initialMessage = Unpooled.Buffer(msg.Length, msg.Length);
25     _initialMessage.WriteBytes(msg); // (3)
26 }

?

(1):建立與服務端相關的通信數據。
(2):將數據序列化為二進制流。
(3):將數據寫入到ByteBuffer中。

啟動一下

由于在客戶端明文標注了使用sayHello這個方法,客戶端會收到服務端返回的"hello stevelee"。

  這樣一個最簡單的RPC遠程調用就完成了(其實上一篇就也屬于RPC,只是這里用方法和過濾來指定調用)。

?

?問題

  1. 服務端不可能都通過這樣笨拙的過濾方式來調用方法吧?是的,這只是DEMO,為了演示和理解基礎概念而已,而是要動過動態代理來實現方法Invoke。
  2. 這個DEMO只是一個點對點的遠程調用,不會涉及到任何服務路由和轉發等高級特性。
  3. 有新的接口的時候時候,需要重新編譯和暴露,如果有上萬個新的接口,這樣的重復工作豈不是瘋了。
  4. ...etc
這里推薦一下最近構建的一個小框架:Easy.Rpc(連接點我),實現了路由,轉發,代理,動態編譯的特性。這里也幫朋友們推薦一個同樣基于DotNetty的RPC框架(連接點我)張隊推薦我加入他們,可我不知道怎么加入他們的團隊,悲催啊...
簡單介紹一下使用方法,本篇不詳細介紹這個框架是如何實現的,估計會好幾十萬字,單獨擰出來做個系列會更好,框架設計需要哪些原則,需要考慮到的問題,包含設計模式、依賴注入、動態代理、動態編譯、路由轉發等等特性。

Esay.Rpc

正如上面提到問題,需要解決這些問題,就需要修改諸多內容,
例如把函數改為接口,把接口的定義放置服務端并對外開放相應端口,把接口的實現同樣放置服務端,提供接口的調用,客戶端通過類似API的方式進行遠程接口調用,因此這個接口的定義必須單列的一個項目;
如何將接口自動部署(暴露)出來,可以通過中間協調器(也叫服務注冊中心,如ETCD,consul,zookeeper),如何將這些接口自動注冊到服務中心呢,需要實現反射自動掃描并添加到注冊中心。
我們添加一個Rpc.Common的中間通用庫,當然Easy.Rpc的框架源碼也在這個里面(框架目前不探討),添加IUserService接口,UserModel實體類,UserServiceImpl實現類。其實通用類庫只需要接口和實體就行,接口實現完全放置服務端,這樣這個庫也能完全分離出來。(不過筆者偷懶都寫到Rpc.Common庫中去了,實際生產決不能這么膜,分離,分離,分離,這也是微服務的主要概念之一)
DEMO結構如下(Easy.Rpc源碼目前也包含在這個里面,過兩天單獨拎出來做成框架,方便調用)

?

?

先看看接口定義了些什么:

 1 /// <summary>
 2 /// 接口UserService的定義
 3 /// </summary>
 4 [RpcTagBundle]
 5 public interface IUserService
 6 {
 7     Task<string> GetUserName(int id);
 8 
 9     Task<int> GetUserId(string userName);
10 
11     Task<DateTime> GetUserLastSignInTime(int id);
12 
13     Task<UserModel> GetUser(int id);
14 
15     Task<bool> Update(int id, UserModel model);
16 
17     Task<IDictionary<string, string>> GetDictionary();
18 
19     Task Try();
20 
21     Task TryThrowException();
22 }
8個接口,幾乎囊括了目前RPC調用測試的所有方法場景。接口實現就不貼了,你完全可以自定義接口的任何實現,或者就一句Console.Write("哇涼哇涼完啦")都可以。
接口參數中有個UserModel的實體對象,這里也貼上來。
1 [ProtoContract]
2 public class UserModel
3 {
4     [ProtoMember(1)] public string Name { get; set; }
5 
6     [ProtoMember(2)] public int Age { get; set; }
7 }

?

上面有兩個不一樣的標記,也是protobuf-net中獨有的特性。

ProtoContract標記:該類是參與序列化內容的數據類。
ProtoMember標題:該類需要序列化的字段和順序。

?

protobuf-net的坑

  1. 默認例子中該類沒有任何繼承,因此不會存在一個妖孽問題,但如果UserModel是一個子類,他繼承于一個父類,而這個父類也同樣擁有多個子類,直接ProtoContract參與序列化將會報錯,需要在特性上增加DataMemberOffset = x,此處的x不是字母,而是這個子類的一個序列化順序。比如有3個子類繼承同一個父類,前面兩個子類的偏移量分別是1和2,那么這個類的偏移量將設置為3,以此類推。
  2. 默認的數據類型中,系統定義的標準類型沒問題,但有個妖孽的int[]這樣的數組類型,那也將是個噩夢,官網團隊沒有解釋為何不支持數組的序列化,我猜測估計是因為數組的不規則性(比如多維數組、甚至不規則的多維數組)而放棄了這個類型的序列化,畢竟序列化是不能影響性能的。

接下來繼續服務端的代碼

 1 static void Main()
 2 {
 3     var bTime = DateTime.Now;
 4 
 5     // 實現自動裝配
 6     var serviceCollection = new ServiceCollection();
 7     {
 8         serviceCollection
 9             .AddLogging()
10             .AddRpcCore()
11             .AddService()
12             .UseSharedFileRouteManager("d:\\routes.txt")
13             .UseDotNettyTransport();
14 
15         // ** 注入本地測試類
16         serviceCollection.AddSingleton<IUserService, UserServiceImpl>();
17     }
18 
19     // 構建當前容器
20     var buildServiceProvider = serviceCollection.BuildServiceProvider();
21 
22     // 獲取服務管理實體類
23     var serviceEntryManager = buildServiceProvider.GetRequiredService<IServiceEntryManager>();
24     var addressDescriptors = serviceEntryManager.GetEntries().Select(i => new ServiceRoute
25     {
26         Address = new[]
27         {
28             new IpAddressModel {Ip = "127.0.0.1", Port = 9881}
29         },
30         ServiceDescriptor = i.Descriptor
31     });
32     var serviceRouteManager = buildServiceProvider.GetRequiredService<IServiceRouteManager>();
33     serviceRouteManager.SetRoutesAsync(addressDescriptors).Wait();
34 
35     // 構建內部日志處理
36     buildServiceProvider.GetRequiredService<ILoggerFactory>().AddConsole((console, logLevel) => (int) logLevel >= 0);
37 
38     // 獲取服務宿主
39     var serviceHost = buildServiceProvider.GetRequiredService<IServiceHost>();
40 
41     Task.Factory.StartNew(async () =>
42     {
43         //啟動主機
44         await serviceHost.StartAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9881));
45     });
46 
47     Console.ReadLine();
48 }
全程基于serviceCollection實現自動裝配和構造,相信用過Ioc容器都能明白這上面幾條依賴注入和自動構建服務的含義。
再添加客戶端代碼:
 1 static void Main()
 2 {
 3     var serviceCollection = new ServiceCollection();
 4     {
 5         serviceCollection
 6             .AddLogging()                                // 添加日志
 7             .AddClient()                                 // 添加客戶端
 8             .UseSharedFileRouteManager(@"d:\routes.txt") // 添加共享路由
 9             .UseDotNettyTransport();                     // 添加DotNetty通信傳輸
10     }
11 
12     var serviceProvider = serviceCollection.BuildServiceProvider();
13 
14     serviceProvider.GetRequiredService<ILoggerFactory>().AddConsole((console, logLevel) => (int) logLevel >= 0);
15 
16     var services = serviceProvider.GetRequiredService<IServiceProxyGenerater>()
17         .GenerateProxys(new[] {typeof(IUserService)}).ToArray();
18 
19     var userService = serviceProvider.GetRequiredService<IServiceProxyFactory>().CreateProxy<IUserService>(
20         services.Single(typeof(IUserService).GetTypeInfo().IsAssignableFrom)
21     );
22 
23     while (true)
24     {
25         Task.Run(async () =>
26         {
27             Console.WriteLine($"userService.GetUserName:{await userService.GetUserName(1)}");
28             Console.WriteLine($"userService.GetUserId:{await userService.GetUserId("rabbit")}");
29             Console.WriteLine($"userService.GetUserLastSignInTime:{await userService.GetUserLastSignInTime(1)}");
30             var user = await userService.GetUser(1);
31             Console.WriteLine($"userService.GetUser:name={user.Name},age={user.Age}");
32             Console.WriteLine($"userService.Update:{await userService.Update(1, user)}");
33             Console.WriteLine($"userService.GetDictionary:{(await userService.GetDictionary())["key"]}");
34             await userService.Try();
35             Console.WriteLine("client function completed!");
36         }).Wait();
37         Console.ReadKey();
38     }
39 }

?

我想看到這里,明白上面代碼的作用,也就明白了這個框架的作用,客戶端能像調用本地方法一樣去調用遠程方法,并且中間過程是完全透明的,分離,分離,分離。
微服務的作用不再介紹,呵呵。
感謝閱讀!

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450294.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450294.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450294.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Centos7防火墻設置

查看防火墻狀態 or rootlocalhost ~]# systemctl status firewalld / firewall-cmd --state 啟動防火墻 [rootlocalhost ~]# systemctl start firewalld 關閉防火墻 [rootlocalhost ~]# systemctl stop firewalld 設置開機啟動 [rootlocalhost ~]# systemctl enable fi…

HTTP協議中POST、GET、HEAD、PUT等請求方法及相應值得含義

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 請求方法是請求一定的Web頁面的程序或用于特定的URL。可選用下列幾種&#xff1a; GET&#xff1a; 請求指定的頁面信息&#xff0c;并…

java面試題文檔(QA)

– 基礎篇 1、 Java語言有哪些特點2、面向對象和面向過程的區別3 、八種基本數據類型的大小&#xff0c;以及他們的封裝類4、標識符的命名規則。5、instanceof 關鍵字的作用6、Java自動裝箱與拆箱7、 重載和重寫的區別8、 equals與的區別9、 Hashcode的作用10、String、String …

第四次軟件工程作業

關于 石墨文檔客戶端 的案例分析 作業地址&#xff1a; https://edu.cnblogs.com/campus/nenu/2016CS/homework/2505 第一部分 調研&#xff0c; 評測 1.下載并使用&#xff0c;按照描述的bug定義&#xff0c;找3~5個功能性的比較嚴重的bug。請用專業的語言描述&#xff08;每個…

深入剖析C++中的string類

一&#xff0c;C語言的字符串 在C語言里&#xff0c;對字符串的處理一項都是一件比較痛苦的事情&#xff0c;因為通常在實現字符串的操作的時候都會用到最不容易駕馭的類型——指針。 比如下面這個例子&#xff1a; //example 1: char str[12] "Hello"; char *…

Apple System: Error: ENFILE: file table overflow

2019獨角獸企業重金招聘Python工程師標準>>> 在MAC上跑nodejs&#xff0c;遇到了一個問題&#xff1a;file table overflow 主要意思就是說文件打開太多了&#xff0c;超過了限制&#xff0c;產生這個問題主要是蘋果操作系統的限制。 echo kern.maxfiles65536 | sud…

springboot的緩存技術

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我門知道一個程序的瓶頸在于數據庫&#xff0c;我門也知道內存的速度是大大快于硬盤的速度的。當我門需要重復的獲取相同的數據的時候&a…

深度優先遍歷解決連通域求解問題-python實現

問題描述 在一個矩形網格中每一個格子的顏色或者為白色或者為黑色。任意或上、或下、或左、或右相鄰同為黑色的格子組成一個家族。家族中所有格子的數量反映家族的大小。要求找出最大家族的家族大小&#xff08;組成最大家族的格子的數量&#xff09;并統計出哪些點屬于哪一族。…

字符串進階

C風格字符串 1、字符串是用字符型數組存儲的&#xff0c;字符串要求其尾部以’\0’作為結束標志。如&#xff1a; char string[ ]”C programming language”; 用sizeof來測string長度為25個字節&#xff0c;而實際串本身長度(含空格)為24個字節&#xff0c;多出來的一個就是…

flask上傳excel文件,無須存儲,直接讀取內容

運行環境python3.6 import xlrd from flask import Flask, requestapp Flask(__name__)app.route("/", methods[POST, GET]) def filelist1():print(request.files)file request.files[file]print(file, type(file), file)print(file.filename) # 打印文件名f …

分布式 ID的 9 種生成方式

一、為什么要用分布式 ID&#xff1f; 在說分布式 ID 的具體實現之前&#xff0c;我們來簡單分析一下為什么用分布式 ID&#xff1f;分布式 ID 應該滿足哪些特征&#xff1f; 1、什么是分布式 ID&#xff1f; 拿 MySQL 數據庫舉個栗子&#xff1a; 在我們業務數據量不大的時…

spring boot Redis集成—RedisTemplate

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Spring boot 基于Spring, Redis集成與Spring大同小異。 文章示例代碼均以前篇筆記為基礎增加修改&#xff0c;直接上代碼&#xff1a;…

QtCreator無法編輯源文件

在Qt Creator中新建工程&#xff0c;添加現有C源文件&#xff0c;有的源文件可以編輯&#xff0c;有的源文件編輯不了&#xff0c;發現無法編輯的源文件有一個共同特點&#xff0c;即其中都包含中文&#xff0c;且中文出現亂碼&#xff0c;于是&#xff0c;點擊Qt Creator菜單欄…

Unicode簡介和使用

一、Unicode簡介 在第一章中&#xff0c;我已經預告&#xff0c;C語言中在Microsoft Windows程序設計中扮演著重要角色的任何部分都會講述到&#xff0c;您也許在傳統文字模式程序設計中還尚未遇到過這些問題。寬字符集和Unicode差不多就是這樣的問題。 簡單地說&#xff0c;…

webpack4.x 模塊化淺析-CommonJS

先看下webpack官方文檔中對模塊的描述&#xff1a; 在模塊化編程中&#xff0c;開發者將程序分解成離散功能塊(discrete chunks of functionality)&#xff0c;并稱之為模塊。每個模塊具有比完整程序更小的接觸面&#xff0c;使得校驗、調試、測試輕而易舉。 精心編寫的模塊提供…

設計模式--抽象工廠(個人筆記)

一、抽象工廠的應用場景以及優缺點 1 應用場景&#xff1a; 如果系統需要多套的代碼解決方案&#xff0c;并且每套的代碼解決方案中又有很多相互關聯的產品類型&#xff0c;并且在系統中我們可以相互替換的使用一套產品的時候可以使用該模式&#xff0c;客戶端不需要依賴具體的…

利用阿里云OSS對文件進行存儲,上傳等操作

--pom.xml加入阿里OSS存儲依賴 <!--阿里云OSS存儲--> <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>2.8.3</version> </dependency> --配置阿里云oss相關常量參數 /…

Java并發編程之ThreadGroup

ThreadGroup是Java提供的一種對線程進行分組管理的手段&#xff0c;可以對所有線程以組為單位進行操作&#xff0c;如設置優先級、守護線程等。 線程組也有父子的概念&#xff0c;如下圖&#xff1a; 線程組的創建 1 public class ThreadGroupCreator {2 3 public static v…

springboot 緩存ehcache的簡單使用

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 步驟&#xff1a; 1. pom文件中加 maven jar包&#xff1a; <!-- ehcache 緩存 --><dependency><groupId>net.sf.eh…

Spring boot + mybatis plus 快速構建項目,生成基本業務操作代碼。

---進行業務建表&#xff0c;這邊根據個人業務分析&#xff0c;不具體操作 --加入mybatis plus pom依賴 <!-- mybatis-plus 3.0.5--> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId>&l…