Redis系列(五):Redis的RESP協議詳解
一、什麼是RESP
Redis是Redis序列化協議,Redis客戶端RESP協議與Redis伺服器通訊。Redis協議在以下幾點之間做出了折衷:
- 簡單的實現
- 快速地被計算機解析
- 簡單得可以能被人工解析
二、RESP協議描述
RESP協議在Redis 1.2中引入,但在Redis 2.0中成為與Redis伺服器通訊的標準方式。這個通訊方式就是Redis客戶端實現的協議。RESP實際上是一個序列化協議,它支援以下資料型別:簡單字串、錯誤、整數、大容量字串和陣列。
1、RESP在Redis中用作請求-響應協議的方式如下:
- 客戶端將命令以批量字串的RESP陣列的形式傳送到Redis伺服器,如下:
SET mykey myvalue *3 $3 SET $5 mykey $7 myvalue *3:SET mykey myvalue 這陣列的長度 $3:表示下面的字元長度是3,這裡是SET長度是 $5:表示下面的字元的長度是5,這裡是mykey的長度 $7:表示下面的字元的長度是7,這裡是myvalue的長度
- 伺服器根據命令實現使用其中一種RESP型別進行響應
2、在RESP中,某些資料的型別取決於第一個位元組:
- For Simple Strings the first byte of the reply is "+" 簡單字串回覆的第一個位元組將是“+”
比如:向伺服器傳送"set toby xu"命令,實際上伺服器的返回是:"+OK\r\n"
- For Errors the first byte of the reply is "-" 錯誤訊息,回覆的第一個位元組將是“-”
比如:向伺服器傳送"add toby xu"命令,實際上伺服器的返回是:"-ERR unknown command `add`, with args beginning with: `toby`, `xu`,\r\n"
- For Integers the first byte of the reply is ":" 整型數字,回覆的第一個位元組將是“:”
比如:向伺服器傳送"incr count"命令,實際上伺服器的返回是:":6\r\n"
- For Bulk Strings the first byte of the reply is "$" 批量回復,回覆的第一個位元組將是“$”
比如:向伺服器傳送"get toby"命令,實際上伺服器的返回是:"$2\r\nxu\r\n"
- For Arrays the first byte of the reply is "*" 陣列回覆的第一個位元組將是“*”
比如:向伺服器傳送"hgetall toby_h"命令,實際上伺服器的返回是:"*4\r\n$4\r\njava\r\n$3\r\n100\r\n$3\r\nc++\r\n$2\r\n80\r\n"
示例RedisServerReplyTest程式碼如下:
/** * @desc: 測試伺服器返回 * @author: toby * @date: 2019/12/5 23:07 */ public class RedisServerReplyTest { public static void main(String[] args) { try(Socket socket = new Socket("192.168.160.146",6379); OutputStream os = socket.getOutputStream(); InputStream is = socket.getInputStream()){ os.write(assemblyCommandForArrays().getBytes()); byte[] bytes=new byte[4096]; is.read(bytes); System.out.println("伺服器真實返回:" + new String(bytes)); } catch (IOException e) { e.printStackTrace(); } } /** * For Simple Strings the first byte of the reply is "+" * @return */ private static String assemblyCommandForSimpleStrings() { StringBuilder sb=new StringBuilder(); sb.append("*3").append("\r\n"); sb.append("$").append("set".length()).append("\r\n"); sb.append("set").append("\r\n"); sb.append("$").append("toby".length()).append("\r\n"); sb.append("toby").append("\r\n"); sb.append("$").append("xu".length()).append("\r\n"); sb.append("xu").append("\r\n"); return sb.toString(); } /** * For Errors the first byte of the reply is "-" * @return */ private static String assemblyCommandForErrors() { StringBuilder sb=new StringBuilder(); sb.append("*3").append("\r\n"); sb.append("$").append("set".length()).append("\r\n"); sb.append("add").append("\r\n"); sb.append("$").append("toby".length()).append("\r\n"); sb.append("toby").append("\r\n"); sb.append("$").append("xu".length()).append("\r\n"); sb.append("xu").append("\r\n"); return sb.toString(); } /** * For Integers the first byte of the reply is ":" * @return */ private static String assemblyCommandForIntegers() { StringBuilder sb=new StringBuilder(); sb.append("*2").append("\r\n"); sb.append("$").append("incr".length()).append("\r\n"); sb.append("incr").append("\r\n"); sb.append("$").append("count".length()).append("\r\n"); sb.append("count").append("\r\n"); return sb.toString(); } /** * For Bulk Strings the first byte of the reply is "$" * @return */ private static String assemblyCommandForBulkStrings() { StringBuilder sb=new StringBuilder(); sb.append("*2").append("\r\n"); sb.append("$").append("get".length()).append("\r\n"); sb.append("get").append("\r\n"); sb.append("$").append("toby".length()).append("\r\n"); sb.append("toby").append("\r\n"); return sb.toString(); } /** * For Arrays the first byte of the reply is "*" * @return */ private static String assemblyCommandForArrays() { StringBuilder sb=new StringBuilder(); sb.append("*2").append("\r\n"); sb.append("$").append("hgetall".length()).append("\r\n"); sb.append("hgetall").append("\r\n"); sb.append("$").append("toby_h".length()).append("\r\n"); sb.append("toby_h").append("\r\n"); return sb.toString(); } }
三、自定義簡單的Redis Client
我們現在瞭解了Redis的RESP協議,並且知道網路層上Redis在TCP埠6379上監聽到來的連線,客戶端連線到來時,Redis伺服器為此建立一個TCP連線。在客戶端與伺服器端之間傳輸的每個Redis命令或者資料都以\r\n結尾,那麼接下來我們自定義一個簡單的Client。
(1)編解碼器Coder:
/** * @desc: 編解碼器 * @author: toby * @date: 2019/12/6 19:33 */ public class Coder { public static byte[] encode(final String str) { try { if (str == null) { throw new IllegalArgumentException("value sent to redis cannot be null"); } return str.getBytes(RedisProtocol.CHARSET); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String decode(final byte[] data) { try { return new String(data, RedisProtocol.CHARSET); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }
(2)Redis協議RedisProtocol:
/** * @desc: Redis協議 * @author: toby * @date: 2019/12/6 19:33 */ public class RedisProtocol {
public static final String CHARSET = "UTF-8"; public static final byte DOLLAR_BYTE = '$'; public static final byte ASTERISK_BYTE = '*'; public static final byte PLUS_BYTE = '+'; public static final byte MINUS_BYTE = '-'; public static final byte COLON_BYTE = ':'; public static final byte CR_BYTE = '\r'; public static final byte LF_BYTE = '\n'; /** * *3 * $3 * SET * $4 * toby * $2 * xu * @param os * @param command * @param args */ public static void sendCommand(final OutputStream os, final Command command, final byte[]... args) { try { os.write(ASTERISK_BYTE); os.write(Coder.encode(String.valueOf(args.length + 1))); os.write(CR_BYTE); os.write(LF_BYTE); os.write(DOLLAR_BYTE); os.write(Coder.encode(String.valueOf(command.name().length()))); os.write(CR_BYTE); os.write(LF_BYTE); os.write(Coder.encode(command.name())); os.write(CR_BYTE); os.write(LF_BYTE); for (final byte[] arg : args) { os.write(DOLLAR_BYTE); os.write(Coder.encode(String.valueOf(arg.length))); os.write(CR_BYTE); os.write(LF_BYTE); os.write(arg); os.write(CR_BYTE); os.write(LF_BYTE); } } catch (IOException e) { throw new RuntimeException(e); } } public enum Command{ SET, GET } }
(3)自定義Client RedisClient:
/** * @desc: 自定義Client * @author: toby * @date: 2019/12/6 19:31 */ public class RedisClient { private String host; private int port; public RedisClient(String host,int port){ this.host = host; this.port = port; } public String set(String key,String value){ try (Socket socket = new Socket(this.host,this.port); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream()){ RedisProtocol.sendCommand(os,RedisProtocol.Command.SET,Coder.encode(key),Coder.encode(value)); return getReply(is); }catch (Exception e) { return e.getMessage(); } } public String get(String key){ try (Socket socket = new Socket(this.host,this.port); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream()){ RedisProtocol.sendCommand(os,RedisProtocol.Command.GET,Coder.encode(key)); return getReply(is); }catch (Exception e) { return e.getMessage(); } } private String getReply(InputStream is){ try { byte[] bytes = new byte[4096]; is.read(bytes); return Coder.decode(bytes); } catch (IOException e) { e.printStackTrace(); } return null; } }
(4)Redis Client 測試 RedisClientTest:
/** * @desc: Redis Client 測試 * @author: toby * @date: 2019/12/6 19:35 */ public class RedisClientTest { public static void main(String[] args) { RedisClient client = new RedisClient("192.168.160.146",6379); System.out.println(client.set("toby_2","xu_2")); System.out.println(client.get("toby_2")); } }
執行結果如下:
至此自定義的簡單的Redis Client完成!!!!!!
四、總結
通過本章的學習,瞭解了什麼是Redis的RESP協議?Redis協議幾個特點:簡單的實現;快速地被計算機解析;簡單得可以能被人工解析。有了協議,我們就可以通過自定義的Client想Redis服務端發起請求,從而進行操作Redis。對後面理解Redis客戶端Jedis的實現原理有很大的幫