淺議NetMQ常見模式和訊息加密機制
概述
在傳統企業級開發中,訊息佇列機制已經成為一種非常常見的技術實現手段,而基於NetMQ則看起來有點像一朵“奇葩”,看起來從名字似乎是一個訊息佇列(Message Quene),但事實上更多的卻是一個類似於socket機制的訊息庫。它雖然提供了訊息佇列的能力,但又與傳統訊息佇列中介軟體如kafka、rabbitmq等有一定的區別。
不過,不管它是啥,它提供的一些類似於訊息佇列的機制,使得開發者能夠快速在專案中使用起來,例如類似於釋出訂閱模式、推拉模式等機制,接入簡便,功能也挺強大。而且當如果我們要實現訊息加密時,還可能通過一些簡單的操作實現,例如我們可以選擇對內容進行Rsa加密,或者也許還有其他的實現方法?
TL;DR:本文首先介紹NETMQ及其常用的使用模式,進而討論如何基於NETMQ實現訊息的加密傳輸機制。
NetMq簡介和基本特性
ZeroMQ
NETMQ是一種輕量級的訊息佇列元件,是著名的ZeroMQ的重要成員。2010年,AMQP的最初設計者Pieter Hintjens帶領其團隊退出了該開源專案,併發起成立了ZeroMQ這個新的訊息庫,並發展至今。 Pieter Hintjens 後由於膽管癌復發,於2016年接受了安樂死。
在ZeroMQ的官方網站中,其介紹到ZeroMQ看起來似乎是一個訊息佇列框架,實際上更像一個併發處理框架。它除了提供了多種訊息佇列機制(如Pub-Sub、Pull-Push、Dealer、XPub-XSub機制)外,更是為開發者提供了跨多種傳輸能力的套接字,它不僅適用於程式間的訊息傳輸,也同樣適用於程式內、TCP和多播的傳輸機制,基於其提供的框架,開發者能快速的實現原子訊息的傳輸能力。ZeroMQ的輕量級體現在其框架靈活簡單,效能優異,無需依賴外部元件,即可輕鬆實現優秀的效能。它也支援非同步I/O的傳輸機制,可為多核應用程式提供擴充套件,且能成為叢集部署的核心傳輸元件。
ZeroMQ提供了多種語言實現,參見其官方網站,包括C語言,C#,Java等主流後端語言,都支援良好,同樣,也支援包括Go、Node.JS等最近比較熱門的新興語言。ZeroMQ自然也支援不同語言間的資料傳輸,使其可以成為跨語言傳輸的一種訊息協議。
ZeroMQ的Zero,代表一種極簡文化,可以代表零代理層(與Mqtt等佇列機制不同,ZeroMQ提供的是一種無代理層的佇列機制),零延遲,零成本和零管理。ZeroMQ致力於打造極簡的通訊元件,通過消除元件的複雜性來提升其功能應用效果。
NETMQ和ClrZmq
對於C#開發者來說,可以使用NetMQ和ClrZmq兩種不同的方式來獲得ZeroMQ的魔力,前者是基於C#語言原生實現的ZeroMQ通訊協議,後者則是通過C#呼叫基於C語言實現 的Libzmq庫來使用。
相對而言,前者可能更受歡迎。NETMQ也同樣繼承了ZeroMQ的優雅效能和輕量化,開發者可通過Nuget下載NetMQ的的元件,通過幾行程式碼就可以整合訊息佇列和套接字傳輸能力。 如圖所示,NetMQ獲得了約175w的下載量,算是一個比較受歡迎的基礎元件。
而同樣在nuget上,ClrZmq的下載量則遠遠少於NetMQ,僅僅8w多的下載量,可能說明它只是一種小眾框架吧。值得一提的是,ClrZmq需要根據構建平臺來選擇不同的架構。
NETMQ的組成部分
截止本文撰寫時,NETMQ的版本為4.0.1.6,作為輕量級元件的一個評判標準,依賴項複雜度也是個重要指標,而NetMQ只依賴了AsyncIO、NaCI.NET、System.ServiceModel.Primitives、System.Threading.Tasks.Extension、System.ValueTuple五個元件,算是名副其實,此處重點介紹兩個非System開頭的元件。
AsyncIO:該元件是一個高效能的非同步的訊息套接字型檔,事實上在Nuget上,該訊息庫比NetMQ更受歡迎,基於該元件,可減少套接字開發的成本。
NaCI:該元件是一個加密元件,實現了包括Curve25519x、Salsa20、Poly1305加密演算法。Curve25519是一種橢球曲線加密演算法,被設計用於橢圓曲線迪菲-赫爾曼(ECDH)金鑰交換方法。Salsa20則是一種流加密演算法。Poly1305則是一種訊息認證碼,可用於檢測訊息的完整性和驗證訊息的真實性,現常在網路安全協議(SSL/TLS)中與salsa20或ChaCha20流密碼結合使用。這三種演算法都是由密碼專家丹尼爾·J·伯恩斯坦設計的加密演算法。
這兩個元件都是由NETMQ的建立者somdoron(Doron Somech)建立,並引入到NETMQ中。
官方網站
NETMQ官方網站地址為https://netmq.readthedocs.io/,該網站提供了較為完整的學習示例,開發者可參考該示例快速學會該元件的用法。
常見模式實現
NETMQ提供了多種訊息通訊機制,例如釋出訂閱模式,推拉模式,
釋出訂閱模式(Pulish-Subscriber Pattern)
簡介
釋出-訂閱是一種訊息傳遞模式,其中訊息的傳送者(稱為釋出者)不會將訊息程式設計為直接傳送給特定的接收者(稱為訂閱者)。釋出的訊息按照主題進行特徵化,作為釋出者事先不用知道可能有哪些訂閱者(如果有)。
類似地,訂閱者可訂閱多個主題,也可只訂閱一個主題。訂閱者也同樣無需關注釋出者是否真實存在,不過由於ZeroMQ本身沒有代理層,且需要繫結服務端埠,事實上看起來似乎必須給定釋出者。但由於ZeroMQ本身也可以作為一種微服務架構的基礎設施,實際上也可以通過一些機制,例如訊息代理,地址代理,DNS閘道器如ZeroConf,Gossip協議等機制,將釋出者隱藏在訊息閘道器背後,從而使得訂閱者無需關注釋出者具體在哪裡。
程式碼示例
該需要首先建立一個釋出者,並通過主題的形式釋出訊息。
class Program
{
private static string _address = "";
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
_address = "tcp://127.0.0.1:5556"; //設定埠
var task = Task.Factory.StartNew(async() =>
{
await BeginPublisherAsync();
});
var taskSubScriber = Task.Factory.StartNew(() =>
{
BeginSubscriberSocket();
});
while(Console.ReadKey().Key!=ConsoleKey.Escape);
}
/// <summary>
/// 啟動訊息釋出
/// </summary>
/// <returns></returns>
private static async Task BeginPublisherAsync()
{
using (var publisher = new PublisherSocket())
{
publisher.Bind(_address); //繫結埠
while (true)
{
publisher
.SendMoreFrame("DotNET技術圈") // Topic
.SendFrame("test"); // Message
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
}
/// <summary>
/// 訂閱訊息
/// </summary>
private static void BeginSubscriberSocket()
{
using (var subscriber = new SubscriberSocket())
{
subscriber.Connect(_address);
subscriber.SubscribeToAnyTopic();
while (true)
{
var topic = subscriber.ReceiveFrameString();
var msg = subscriber.ReceiveFrameString();
Console.WriteLine("收到訊息: {0} {1}", topic, msg);
}
}
}
}
在上述程式碼中,釋出者繫結了tcp://127.0.0.1:5556
埠,並通過同步阻塞的方式,釋出主題為Topic的訊息內容。也可以指定主機的固定ip地址來進行訊息釋出,還能通過inproc://inproc-demo
的方式進行程式內通訊。
using var subscriber = new SubscriberSocket()
subscriber.Connect("tcp://127.0.0.1:5556");
subscriber.Subscribe("TopicA"); //訂閱到TopicA主題,也可通過SubscribeToAnyTopic訂閱所有主題,也可通過UnSubcribe取消訂閱相關主題
while (true)
{
var topic = subscriber.ReceiveFrameString();
var msg = subscriber.ReceiveFrameString();
Console.WriteLine("From Publisher: {0} {1}", topic, msg);
}
請求響應模式(Request-Response Pattern)
請求響應模式也是NETMQ眾多訊息模式中最為簡單的一種模式,這種模式實際上有點像http協議,可通過一問一答的同步阻塞的模式進行訊息的應答,當然,傳送HTTP請求我們也可以不必接收響應,NETMQ的請求響應模式也同樣如此。
示意圖
private static void BeginResponseSocket()
{
using var responseSocket = new ResponseSocket(_address);
string request=responseSocket.ReceiveFrameString();
responseSocket.SendFrame("Hello DotNET技術圈");
}
private static async Task BeginRequestSocketAsync()
{
using var requestSocket = new RequestSocket();
requestSocket.Connect(_address);
while (requestSocket.TrySignalOK())
{
try
{
requestSocket.TrySendFrame("Hallo I am DotNET技術圈碼農");
requestSocket.TrySendFrame("Hallo I am DotNET技術圈碼農"); ---這裡會引發錯誤。。
}
catch(Exception ex)
{
Console.Out.WriteLine(ex);
}
await Task.Delay(1000);
}
}
由於該模式的同步阻塞特性,如果同時傳送兩條訊息,可能會觸發NETMQ重複傳送異常,如:
推拉模式
推拉模式與我們傳統意義上理解的類似於手機推送的模式有一些區別,ZeroMQ中說該模式主要將訊息下發到提供了一組Push-Pull的套接字,實現訊息下發。
值得一體的是,即便的同為ZeroMQ模式下不同語言的版本,對於相同模式的說明,文字描述也不盡相同,例如,在NetMQ的開發者文件中,
Well a
PushSocket
is normally used to push to aPullSocket
, whilst thePullSocket
will pull from aPushSocket
. Sounds obvious right!PushSocket 負責把訊息推給PullSocket,同樣PullSocket負責從PushSocket 拉訊息。
這樣的說明似乎什麼都說了,但又似乎啥都沒說,看看其他語言的實現,例如基於Python的PyZmq中,其描述為這樣:
Push and Pull sockets let you distribute messages to multiple workers, arranged in a pipeline. A Push socket will distribute sent messages to its Pull clients evenly. This is equivalent to producer/consumer model but the results computed by consumer are not sent upstream but downstream to another pull/consumer socket.
推拉模式允許你基於通過管道的機制實現訊息分發給多個工作者。單個PushSocket分發會將訊息均勻的分發給其Pull客戶端。這樣的操作等效於生產者-消費者模型,但消費者計算的結果不是向上傳送,而是向下遊傳送到另一個拉/消費者套接字。
兩種不同的實現,在描述上區別還是顯著不同,通過兩者的對比,我們可以這樣理解:Push-Pull模式下,兩者都可以互為服務端或客戶端,但無論如何,其訊息都是單向傳輸的。訊息總是沿著管道向下流動,沿著我們設計的方向傳輸,實現訊息在不同節點間的負載均衡。
例如,可以實現如下的效果,通過一個Ventilator來生產資料,通過多個Pull來拉取資料,進而實現資料向下流動,可以參考NetMq官方文件來實現該程式碼。
基於推拉模式,可以設計非常負責的業務模型,例如類似於MapReduce的資料處理器就是一個這樣的教學工具。(當然,該工具只是一個演示ZeroMQ模式實現的分散式計算的Demo,可能不適合作為生產用途)。
程式碼示意
本示例中,僅僅簡單介紹Push-Pull的用法,暫不涉及複雜的模式。
private static async Task BeginPushSocketAsync()
{
using var pushSocket = new PushSocket(_address);
while (true)
{
pushSocket.SendFrame("Hello Clients");
await Task.Delay(1000);
}
}
private static async Task BeginPullSocketAsync()
{
using var pullSocket = new PullSocket(_address);
while (true)
{
string message = pullSocket.ReceiveFrameString();
Console.WriteLine(message);
await Task.Delay(1000);
}
}
netmq加密傳輸機制實現
當我們使用NetMQ進行訊息傳輸時,上述示例均沒有對訊息進行任何加密處理,這種策略可能導致一些不可控的安全性風險,例如在開發基於NetMQ的聊天室功能時,釋出的資訊若未採取任何加密措施,事實上可能意味著訊息是以廣播的形式對外發布,從而會造成某些隱私資訊洩漏。或者,如果你需要向外Publish某些訊息,未授權的訂閱者訂閱了你的資料,雖然可能資料中不包含直接的隱私資料,但同樣可能會引起你的不適。
因此,從安全性的角度來說,無論你計劃基於NetMQ實現何種場景,事實上可能都得考慮以儘可能安全的形式“釋出”你的訊息。目前我們可通過三種方式來實現訊息的加密傳輸功能。第一種是使用基於Tls協議的NetMQ.Security元件,一種是基於非對稱金鑰演算法,如RSA加密演算法,還有一種是基於ZeroMQ所提供的兩種加密方式,ECC橢球曲線加密演算法和Z85加密演算法,以對稱金鑰的方式。
基於Tls的NetMq.Security?
NetMQ.Security也是由NetMQ的主要貢獻者somdoron開發的元件,目前該元件處於不活躍的狀態,截至目前僅有5次更新,上一次更新依然是4年前,通過一些早期帖子,作者Doron Somech也同樣不認為該元件可以在生產環境下使用(?),所以事實上可能不太適合作為專業團隊的技術選型。
目前比較詳細的介紹來自傑哥很忙,且優秀的傑哥對fork了該元件的程式碼,並開發了許多功能,由於主幹倉庫已經塵封太久了,開發者有興趣可以參詳參詳。
使用時,我們可通過Nuget下載由NetMQ官方釋出的元件,不過,似乎下載量有點慘淡,那麼,此處就不再贅述了。。。。
非對稱金鑰演算法-Rsa加密
對於文字來說,使用Rsa這種非對稱演算法族進行加密是一種非常常見的選擇,RSA是由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)在1977年一起提出的,當時他們三人都在麻省理工學院工作。RSA 就是他們三人姓氏開頭字母拼在一起組成的。
RSA演算法的核心是極大整數做因數分解,換言之,對一極大整數做因式分解越困難,RSA演算法越可靠。目前傳統計算機只能破解較為簡單的RSA金鑰,如果使用的金鑰長度足夠長,理論上用RSA加密的資訊也很難以被破解。在RSA演算法中,金鑰由私鑰和公鑰組成。由私鑰負責對內容進行解密,並用公鑰進行加密。分配公鑰的過程必須足夠安全,若被中間人攻擊,則可能導致公鑰失效。
影響RSA金鑰安全性的要素首先是其金鑰長度,目前推薦的RSA演算法公鑰長度為2048位。其次是RSA金鑰的填充模式,共有三種填充模式,RSA_PKCS1_PADDING, RSA_PKCS1_OAEP_PADDING, RSA_NO_PADDING。填充技術實現的不好,RSA也不會安全,應儘量選擇最安全的填充模式,例如RSA_PKCS1_PADDING。
原因如下:
- RSA加密是確定的,即給定一個金鑰,特定明文總會對映到特定的密文。攻擊者可以根據密文中統計資訊獲取明文的一些資訊。
- 填充技術如果比較弱,那麼較小的明文和小型公開指數e將易於受到攻擊。
- RSA有個特性叫做延展性,如果攻擊者可以將一種密文轉換為另一種密文,這種新密文會導致對明文的轉換變得可知,這種特性並沒有解密明文,而是以一種可預測的方式操縱了明文,比如:銀行交易系統中,攻擊者根據新密文,直接去修改原密文中金額的資料,可以在使用者和接受方無法感知的情況下進行修改。
在RSA演算法中提供了以下功能提供:
- 金鑰對生成:生成隨機私鑰(通常大小為 1024-4096 位)和相應的公鑰。
- 加密:使用公鑰加密祕密訊息(範圍為 [0...key_length] 的整數),並使用祕密金鑰將其解密。
- 數字簽名:簽署訊息(使用私鑰)並驗證訊息簽名(使用公鑰)。
- 金鑰交換:安全地傳輸一個祕密金鑰,用於以後的加密通訊。
RSA 可以使用不同長度的金鑰:1024、2048、3072、4096、8129、16384 甚至更多位的金鑰。3072 位及以上的金鑰長度被認為是安全的。更長的金鑰提供更高的安全性,但消耗更多的計算時間,因此需要在安全性和速度之間進行權衡。很長的 RSA 金鑰(例如 50000 位或 65536 位)對於實際使用來說可能太慢,例如金鑰生成可能需要幾分鐘到幾個小時。
網上也有基於RSA進行NetMQ進行訊息加密的示例,可供參考。其核心流程為,在進行訊息傳送時,使用RSA公鑰進行加密,
MsgObject sendmsg = EventQueue.Dequeue ( ) ;
sendmsg.Content = RSAEncryption.RSAEncrypt(sendmsg.Content);
sendmsg.MachineName= msg.MachineName;
SendMessageQueue.Enqueue(sendmsg) ;
並在客戶端接收到訊息後,對正文進行RSA解密,解密程式碼略。
使用對稱金鑰加密演算法-Ecc加密演算法進行訊息加密
RSA演算法雖好,但由於私鑰由客戶端管理,公鑰由服務端管理,且RSA必須金鑰位數足夠長才安全,例如2048位,使用這麼長的金鑰進行加密時間開銷也令人吃不消的,有沒有一種更簡單、快速的演算法來實現呢?
使用AES演算法?
我們或許會想到AES演算法,例如AES256演算法這種“對稱金鑰加密演算法”。在“兌成金鑰加密演算法”中,加密和解密使用祕密金鑰或密碼短語(從中派生出金鑰)。該祕密金鑰用於加密和解密資料,通常是128位或256位,並被稱為“加密金鑰”。有時它以十六進位制或 base64 編碼的整數形式給出,或者通過密碼到金鑰派生方案派生,當輸入資料被加密時,它被轉換為加密的密文,當密文被解密時,它被轉換回原始輸入資料。
通常,對稱加密過程使用一系列步驟,涉及不同的加密演算法:
密碼到金鑰派生演算法(如 Scrypt 或 Argon2):允許使用密碼而不是金鑰,並使密碼破解變得困難而緩慢。
塊到流密碼轉換演算法(塊密碼模式如CBC或CTR )+訊息填充演算法如PKCS7 (在某些模式下):允許使用塊密碼演算法(如AES)加密任意大小的資料。
塊密碼演算法(如AES ):使用金鑰安全地加密固定長度的資料塊。
訊息認證演算法(如HMAC ):檢查解密後得到的結果是否與加密前的原始訊息匹配。
NETMQ的原生解決方案?
不過上述AES加密演算法實質上也需要開發者手工處理訊息體,存在的記憶體開銷和時間可能對於使用者來說依然無法接受,或許最好的辦法依然是基於NETMQ框架來入手看看是否有什麼“原生”的解決方案。
所幸ZeroMQ在設計之初就已經將安全作為其認為非常重要的一個方面,在這篇部落格中,ZeroMQ提到了其對於安全層的目標,包括:
- 它使用起來必須非常簡單,而且不可能出錯。複雜性是密碼學的第一大風險和第一大漏洞。每一個額外的選項都是一種出錯的方式,最終導致一個不安全的系統。
- 對於實際工作,它必須足夠快。如果安全性使系統變得太慢而無法使用,人們就會將其關閉,因為今天能夠工作的務實需求勝過明天被入侵的風險。
- 它必須基於標準化協議,以便任何團隊都可以重新實施、獨立驗證並在軟體堆疊之外進行改進。
- 等等。
並從2013年起,在ZeroMQ版本(4.0.0)中就已經引入了安全架構設計,包括:
- 一種新的有線協議ZMTP 3.0,為所有 ZeroMQ 連線新增了安全握手。
- 一種新的安全協議CurveZMQ,它通過 TCP 連線在兩個 ZeroMQ 對等點之間實現“完美的前向安全”。我將在下面解釋 CurveZMQ。
- ZMTP 的一組安全機制:NULL、PLAIN 和 CURVE,每個機制都由它們自己的 RFC 描述。NULL 本質上是我們之前所擁有的。PLAIN 允許簡單的使用者名稱和密碼驗證。CURVE 實現了 CurveZMQ 協議。、
- 等等。
在ZeroMQ中整合的橢球曲線演算法為Curve25519 ,目前,在我們所使用的NetMQ中也同樣整合了該演算法。在搞清楚原理後,我們再來使用該演算法,發現一切就變得非常簡單明瞭了。
var serverPair = NetMQ.NetMQCertificate.CreateFromSecretKey(UTF8Encoding.UTF8.GetBytes(”這裡是金鑰“));;
using var server = new PublishSocket();
server.Options.CurveServer = true;
server.Options.CurveCertificate = serverPair;
server.Bind($"tcp://127.0.0.1:55367");
using (var server = new SubscriberSocket())
{
var cert = NetMQ.NetMQCertificate.CreateFromSecretKey(UTF8Encoding.UTF8.GetBytes(”這裡是金鑰“));
var curveServerCertificate = serverPair;
var clientCertificate = new NetMQCertificate(); ---這裡是客戶端金鑰,
server.Options.CurveServerCertificate = curveServerCertificate; ---這裡是使用服務端金鑰
server.Options.CurveCertificate = clientCertificate; ---這裡是客戶端金鑰
}
結語
本文對NetMQ進行了簡單的概述,包括其常見模式和加密傳輸機制,開發者若有興趣,可通過NetMQ官網獲得更多學習資料。 如果開發者加密演算法感興趣,還可以通過這個網站(https://cryptobook.nakov.com)讀到許多有關加密的基礎知識。