使用 DotNetty 實現 Redis 的一個控制臺應用程序
零: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 /// 以 : 開頭的整型數據INTEGER 14 /// </summary> 15 Integer = 58, 16 /// <summary> 17 /// 以 $ 開頭的多行字符串 18 /// </summary> 19 BulkString = 36, 20 21 /// <summary> 22 /// 以 * 開頭的數組 23 /// </summary> 24 ArrayHeader = 42 25 }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\n 18 //const string headstr = "*{0}\r\n"; 19 //參數信息 $<number of bytes of argument N>\r\n<argument data>\r\n 20 //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\n 28 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); } } }
結語:附上源碼地址
https://gitee.com/hesson/Dotnetty.Redis.Demo
感謝 @蛀牙 對本文的審閱,並提出修改的建議
使用 DotNetty 實現 Redis 的一個控制臺應用程序