零:Demo 跑出來的結果說明
圖中左邊藍色的命令列介面,是用windows powershell 命令列連結的。
1.開啟powershell命令列介面,輸入命令【telnet 127.0.0.1 6379】。
如果沒有powershell,使用cmd 命令列介面也是可以達到測試redis 命令的效果的。
輸入PING 命令,redis 接收到,它將返回一個PONG字串。命令的作用通常是測試與伺服器的串連是否仍然生效。PING命令
輸入Info 命令,redis 會返回一大串的redis 服務端的資訊。這個命令,主要用來測試拆包的情況,下面會講到拆包如何處理。
圖中右邊黑色的命令列介面,是Demo 跑出來的控制台應用程式。
兩個結果一對比,測試出來,我們的Demo已經得到了正確的結果。
Ok,下面開始進入正戲。
一 DotNetty 是什麼
DotNetty 是netty 一個C#版本。
Netty是由JBOSS提供的一個java開源架構。Netty提供非同步、事件驅動的網路應用程式架構和工具,用以快速開發高效能、高可靠性的網路伺服器和用戶端程式。【摘自百度百科】
筆者認為 Netty是Java生態圈的一個重要組件。
原生Socket編程,學習成本高,使用原生的Socket做項目,那就是開著一輛綠皮火車,動次打次。。。。
使用Netty,開做項目,那開發效率無疑是高鐵般的存在。
而且使用原生的socket 編程是很困難的
二,寫這個Demo 的起因
學習DotNetty很久。從DotNetty 0.4版本。到現在的0.48版本。自己實現一個C/S端的例子。還沒有太好的想法去實現。
今天看到haifeiWu 的高作《Netty 源碼中對 Redis 協議的實現》,遂想跟著實現一個。
所以,才有了今天的Demo.
是的,它還只是一個Demo.並不能取代StackExchange.Redis。
三,瞭解一下redis的協議
RESP 是 Redis 序列化協議的簡寫。它是一種直觀的文本協議,優勢在於實現非常簡單,解析效能極好。
Redis 協議將傳輸的結構資料分為 5 種最小單元類型,單元結束時統一加上斷行符號分行符號號\r\n,來表示該單元的結束。
單行字串 以 + 符號開頭。
多行字串 以 $ 符號開頭,後跟字串長度。
整數值 以 : 符號開頭,後跟整數的字串形式。
錯誤訊息 以 - 符號開頭。
數組 以 * 號開頭,後跟數組的長度。
關於 RESP 協議的具體介紹感興趣的小夥伴請移步 haifeiWu 的另一篇文章Redis協議規範(譯文)
以上第二點是摘抄自 haifeiWu中的介紹
四 Demo 代碼
1,定義枚舉 RedisMessageType
1 internal enum RedisMessageType:byte 2 { 3 /// <summary> 4 /// 以 + 開頭的單行字串 5 /// </summary> 6 SimpleString = 43, 7 8 /// <summary> 9 /// 以 - 開頭的錯誤資訊10 /// </summary>11 Error = 45,12 /// <summary>13 /// 以 : 開頭的整型資料INTEGER14 /// </summary>15 Integer = 58,16 /// <summary>17 /// 以 $ 開頭的多行字串18 /// </summary>19 BulkString = 36,20 21 /// <summary>22 /// 以 * 開頭的數組23 /// </summary>24 ArrayHeader = 4225 }View Code
2,定義RedisObject 並定義了虛擬方法 WriteBuffer
1 public class RedisObject 2 { 3 public virtual void WriteBuffer(IByteBuffer output) 4 { 5 } 6 } 7 8 public class RedisCommon : RedisObject 9 {10 public RedisCommon()11 {12 Commond = new List<string>();13 }14 public List<string> Commond { get; set; }15 public override void WriteBuffer(IByteBuffer output)16 {17 //要求標頭部格式, *<number of arguments>\r\n18 //const string headstr = "*{0}\r\n";19 //參數資訊 $<number of bytes of argument N>\r\n<argument data>\r\n20 //const string bulkstr = "${0}\r\n{1}\r\n";21 StringBuilder stringBuilder = new StringBuilder();22 stringBuilder.AppendFormat("*{0}\r\n",Commond.Count);23 foreach (var item in Commond)24 {25 stringBuilder.AppendFormat("${0}\r\n{1}\r\n",item.Length,item);26 }27 //*1\r\n$4\r\nPING\r\n28 byte[] bytes = Encoding.UTF8.GetBytes(stringBuilder.ToString());29 output.WriteBytes(bytes);30 }31 }View Code
3,定義RedisEncoder 編碼器, 它整合了MessageToByteEncoder<T>方法。主要是將RedisObject,寫到IByteBuffer裡面。
public class RedisEncoder:DotNetty.Codecs.MessageToByteEncoder<RedisObject> { protected override void Encode(IChannelHandlerContext context, RedisObject message, IByteBuffer output) { message.WriteBuffer(output); //context.WriteAndFlushAsync(output); } }
4,定義 RedisDecoder 解碼器,它繼承了 ByteToMessageDecoder。
ByteToMessageDecoder 是需要自己實現解決粘包,拆包的。比較低層級,但是靈活。
DotNetty 還有其他比較進階的解碼器。
比如 MessageToMessageDecoder, DatagramPacketDecoder,LengthFieldBasedFrameDecoder,LineBasedFrameDecoder,ReplayingDecoder,DelimiterBasedFrameDecoder,StringDecoder。
在李林鋒老師的《Netty權威指南》一書中,都能學習到。
通過測試,我們知道了info 命令返回的是一個多行字串
以 $ 符號開頭,後跟字串長度。假設redis 服務端要返回一個多行字串,它的返回格式為: ${字串長度}\r\n{字串}\r\n
解析多行字串的代碼為
private string ReadMultiLine(IByteBuffer input) { Int64 strLength = ReadInteger(input); Int64 packLength = input.ReaderIndex + strLength + 2; //包的長度,比實際包還要大,跳過他,防止堆積 if ( input.WriterIndex> packLength) { input.SkipBytes(input.ReadableBytes); } if (strLength == -1) { return null; } //包的長度,比實際包還小 拆包 if (packLength > input.WriterIndex) { throw new Exception(""); } int count = 0; int whildCount = 0; StringBuilder stringBuilder = new StringBuilder(); while (input.IsReadable()) { string str= this.ReadString(input); count += str.Length; stringBuilder.AppendLine(str); whildCount++; }
return stringBuilder.ToString(); }
6.定義 RedisHandle Handler ,他繼承了SimpleChannelInboundHandler 的方法。用來接收解碼器之後解出來的RedisObJect對象。
public class RedisHandle : SimpleChannelInboundHandler<RedisObject> { protected override void ChannelRead0(IChannelHandlerContext ctx, RedisObject msg) { if (msg is ReidsString) { ReidsString reidsString = (ReidsString)msg; Console.WriteLine(reidsString.Content); } } }
結語:附上源碼地址
gitee.com/hesson/Dotnetty.Redis.Demo
感謝 @蛀牙 對本文的審閱,並提出修改的建議