一、RESP协议
首先需要明白,Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub):
- 客户端(client)向服务端(server)发送一条命令
- 服务端解析并执行命令,返回响应结果给客户端 因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。
而在Redis中采用的是RESP(Redis Serialization Protocol)协议:
- Redis 1.2版本引入了RESP协议
- Redis 2.0版本中成为与Redis服务端通信的标准,称为
- RESP2 Redis 6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型并且支持6.0的新特性--客户端缓存?
????????在RESP中,通过首字节的字符来区分不同数据类型,常用的数据类型包括5种:
- 单行字符串:首字节是 ‘+’ ,后面跟上单行字符串,以CRLF( "\r\n" )结尾。例如返回"OK": "+OK\r\n"
- 错误(Errors):首字节是 ‘-’ ,与单行字符串格式一样,只是字符串是异常信息,例如:"-Error message\r\n"
- 数值:首字节是 ‘:’ ,后面跟上数字格式的字符串,以CRLF结尾。例如:":10\r\n"
- 多行字符串:首字节是 ‘$’ ,表示二进制安全的字符串,最大支持512MB:如果大小为0,则代表空字符串:"$0\r\n\r\n" 如果大小为-1,则代表不存在:"$-1\r\n"
- 数组:首字节是 ‘*’,后面跟上数组元素个数,再跟上元素,元素数据类型不限:例如
?
?????????
?二、模拟redis客户端实现
【响应解析请求模块】
/**
* 解析响应请求信息
*
* @return 解析结果
*/
private static Object handleResponse() throws IOException {
//五种情况读取数据
int opt = READER.read();
switch (opt) {
case '+'://单行字符串,读取单行信息
return READER.readLine();
case '-'://异常信息,读取单行信息返回异常
return READER.readLine();
case ':'://数值类型,读取单行
return Long.parseLong(READER.readLine());
case '*':
return readBulkString();
case '$'://读取多行字符串
int len = Integer.parseInt(READER.readLine());
if (len == -1) {
return null;
} else if (len == 0) {
return "";
} else {
return READER.readLine();
}
default:
throw new RuntimeException("错误的数据格式!");
}
}
/**
* 数组结果解析
*
* @return
* @throws IOException
*/
private static Object readBulkString() throws IOException {
//获取数组大小
int size = Integer.parseInt(READER.readLine());
if (size <= 0) {
return null;
} else {
List<Object> result = new ArrayList<>();
for (int i = 0; i < size; i++) {
result.add(handleResponse());
}
return result;
}
}
【完整代码】
/**
* @Author 蜂蜜柚子茶
* @Date 2022/6/14 20:34
*/
public class MyRedisClient {
private static Socket socket;
private static PrintWriter WRITER;
private static BufferedReader READER;
private static BufferedReader KEYBOARD_INPUT;
private static final String INFO = "127.0.0.1:6379> ";
public static void main(String[] args) throws Exception {
try {
//建立连接
//虚拟机IP地址:192.168.29.128
socket = new Socket("192.168.29.128", 6379);
//获取输入流、输出流
WRITER = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
READER = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
//键盘输入命令
KEYBOARD_INPUT = new BufferedReader(new InputStreamReader(System.in));
//执行命令,同时结果解析
execute();
} catch (IOException e) {
e.printStackTrace();
} finally {
//释放连接
try {
if (READER != null)
READER.close();
if (WRITER != null)
WRITER.close();
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取键盘输入
*
* @return
* @throws Exception
*/
public static String getInput() throws Exception { // 键盘信息输入
System.out.print(INFO);
return KEYBOARD_INPUT.readLine();
}
/**
* 执行命令
*
* @throws IOException
*/
private static void execute() throws Exception {
while (true) {
//获取输入命令,去除首位空格
String string = getInput().trim();
//解析命令,去除所有空格
String replace = string.replaceAll("\\s{1,}", "/");
//System.out.println(replace);
String[] strings = replace.split("/");
//发送请求
sendRequest(strings);
//解析响应信息
Object result = handleResponse();
if (result == null) {
System.out.println(getFormatResult("null", "warning"));
} else if (result.toString().startsWith("ERR")) {
System.out.println(getFormatResult(result.toString(), "error"));
} else {
System.out.println(getFormatResult(result.toString(), "info"));
}
}
}
/**
* 格式化输出结果
*
* @param content 结果
* @param type 类型
* @return 格式化输出结果
*/
private static String getFormatResult(String content, String type) {
if (type.equals("error")) {
return String.format("\033[%dm%s\033[0m", 31, content);
} else if (type.equals("info")) {
return String.format("\033[%dm%s\033[0m", 34, content);
} else if (type.equals("warning")) {
return String.format("\033[%dm%s\033[0m", 33, content);
} else {
return content;
}
}
/**
* 解析响应请求信息
*
* @return 解析结果
*/
private static Object handleResponse() throws IOException {
//五种情况读取数据
int prefix = READER.read();
switch (prefix) {
case '+'://单行字符串,读取单行信息
return READER.readLine();
case '-'://异常信息,读取单行信息返回异常
return READER.readLine();
case ':'://数值类型,读取单行
return Long.parseLong(READER.readLine());
case '*':
return readBulkString();
case '$'://读取多行字符串
int len = Integer.parseInt(READER.readLine());
if (len == -1) {
return null;
} else if (len == 0) {
return "";
} else {
return READER.readLine();
}
default:
throw new RuntimeException("错误的数据格式!");
}
}
/**
* 数组结果解析
*
* @return
* @throws IOException
*/
private static Object readBulkString() throws IOException {
//获取数组大小
int size = Integer.parseInt(READER.readLine());
if (size <= 0) {
return null;
} else {
List<Object> result = new ArrayList<>();
for (int i = 0; i < size; i++) {
result.add(handleResponse());
}
return result;
}
}
/**
* 发送请求信息
*
* @param args
*/
private static void sendRequest(String... args) {
//本质上是命令--> set name XXXX
WRITER.println("*" + args.length);
for (String arg : args) {
WRITER.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
WRITER.println(arg);
}
//清空缓冲区
WRITER.flush();
}
}
三、效果展示
【模拟redis-cli】idear窗口?
win的控制台输出颜色乱码,不支持颜色的转义。
如果文章对你有用,狠狠地三连支持一下吧!!!
|