本帖最后由 1139365029 于 2022-9-18 13:56 编辑
本贴已默认你拥有Redis基础,若没有请先前往此处:https://www.mcbbs.net/thread-861073-1-1.html
本贴的主要目的是教大家在不添加任何驱动、前置库、外部依赖等的情况下使用Redis
值得注意的是,本贴的相关操作偏向于底层逻辑,需要一定的编程基础
众所周知,Redis的通讯协议为resp协议,
因此,对应的操作步骤如下
回到正题,用户输入的指令与resp协议的数据有什么对应关系呢?
举个例子复制代码需要转换为下列resp协议格式复制代码我们一行一行来看,
首先第一行的*3,表示这个指令由3个部分/参数组成,
接下来是每个参数的数据,
第一个$3表示第一个参数的长度(字符串转换为byte[]后的数组成员数),
紧接着就是第一个参数的内容,
第二个$3表示第二个参数的长度,
紧接着就是第二个参数的内容,
同理,接下来的$5就是第三个参数的长度,然后是内容,
注意,包括来自Redis返回的数据,换行符均为\r\n,并且数据由\r\n结尾,
并且传输的数据应使用byte数组,而不是String字符串,
接下来再举个例子复制代码转换后的结果为复制代码最后再举个例子复制代码转换后的结果为复制代码好了,接下来是解析来自Redis的resp协议格式的数据,
这里我们需要知道Redis会发送什么样的数据,
首先是错误信息,指令异常、执行错误、查询错误等情况,Redis会返回此信息,
这类信息的格式由减号开头,后面跟报错信息,例如复制代码接着是执行成功的信息,set指令、ping指令等回返回此信息,
这类信息的格式由加号开头,后面跟相关信息,例如复制代码接着是整数信息,查询数量、列表操作等回返回此信息,
这类信息的格式由冒号开头,后面跟整数数字,例如复制代码最后是一般的查询数据时返回的内容,
这类信息的格式与我们发送到Redis的数据的格式是相同的,
例如有一个包含3个数据的列表,内容分别是“abc、defg、hijkl”,全部返回时,Redis会返回复制代码值得注意的是,哈希、字符串等返回的数据只有1个的情况下,Redis会省略掉数据数量,
例如某个字符串的内容是“12345”,则Redis会直接返回复制代码特殊情况:未查询到数据时,列表等多个数据会返回*0,字符串等一个数据会返回$-1
看到这里,想必大家已经知道如何操作了吧,以下为参考代码:
注意此代码的执行效率并非最优,之所以这样写是为了帮助理解
另外此代码只是大致测试了一下,基本没什么问题,不过还是仅供参考
麻了,泥潭的code模块居然会屏蔽掉“$'”,改用quote模块吧
部分Redis指令参考:
登录指令(仅Redis存在连接密码时使用,连接成功后需要先发送此指令才能使用其它指令)复制代码切换到指定数据库(编号从0开始)复制代码获取Redis的信息(包括版本、CPU、内存等)复制代码
参考文献:resp协议
本贴已默认你拥有Redis基础,若没有请先前往此处:https://www.mcbbs.net/thread-861073-1-1.html
本贴的主要目的是教大家在不添加任何驱动、前置库、外部依赖等的情况下使用Redis
值得注意的是,本贴的相关操作偏向于底层逻辑,需要一定的编程基础
众所周知,Redis的通讯协议为resp协议,
因此,对应的操作步骤如下
- 用户输入Redis指令
- 将指令转换为resp协议格式的数据
- 将数据发送至Redis,并且接收Redis返回的数据
- 解析并处理Redis返回的resp协议格式的数据
回到正题,用户输入的指令与resp协议的数据有什么对应关系呢?
举个例子
- set key value
- *3
- $3
- set
- $3
- key
- $5
- value
首先第一行的*3,表示这个指令由3个部分/参数组成,
接下来是每个参数的数据,
第一个$3表示第一个参数的长度(字符串转换为byte[]后的数组成员数),
紧接着就是第一个参数的内容,
第二个$3表示第二个参数的长度,
紧接着就是第二个参数的内容,
同理,接下来的$5就是第三个参数的长度,然后是内容,
注意,包括来自Redis返回的数据,换行符均为\r\n,并且数据由\r\n结尾,
并且传输的数据应使用byte数组,而不是String字符串,
接下来再举个例子
- ping
- *1
- $4
- ping
- keys *
- *2
- $4
- keys
- $1
- *
这里我们需要知道Redis会发送什么样的数据,
首先是错误信息,指令异常、执行错误、查询错误等情况,Redis会返回此信息,
这类信息的格式由减号开头,后面跟报错信息,例如
- -BaLaBaLa
这类信息的格式由加号开头,后面跟相关信息,例如
- +OK
这类信息的格式由冒号开头,后面跟整数数字,例如
- :114514
这类信息的格式与我们发送到Redis的数据的格式是相同的,
例如有一个包含3个数据的列表,内容分别是“abc、defg、hijkl”,全部返回时,Redis会返回
- *3
- $3
- abc
- $4
- defg
- $5
- hijkl
例如某个字符串的内容是“12345”,则Redis会直接返回
- $5
- 12345
看到这里,想必大家已经知道如何操作了吧,以下为参考代码:
注意此代码的执行效率并非最优,之所以这样写是为了帮助理解
另外此代码只是大致测试了一下,基本没什么问题,不过还是仅供参考
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class test {
private static Socket socket=null;
//连接redis
private static void login() {
try {
socket=new Socket("127.0.0.1", 6379);
//设置超时时间
socket.setSoTimeout(3000);
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//断开redis
private static void close() {
if(socket!=null) {
try {
socket.close();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
socket=null;
}
//将两个数组拼接成一个数组
public static byte[] addByte(byte[] b1, byte[] b2){
byte[] re=new byte[b1.length+b2.length];
System.arraycopy(b1, 0, re, 0, b1.length);
System.arraycopy(b2, 0, re, b1.length, b2.length);
return re;
}
//redis操作
private static List<byte[]> runCommand(List<byte[]> command) {
//拼接指令
//拼接参数个数
byte[] info=("*"+command.size()+"\r\n").getBytes();
for(byte[] b:command) {
//拼接参数长度
info=addByte(info,("$"+b.length+"\r\n").getBytes());
//拼接参数内容
info=addByte(info,b);
//拼接换行符
info=addByte(info,"\r\n".getBytes());
}
List<byte[]> data=null;
try {
//发送数据
socket.getOutputStream().write(info);
//准备读取数据
InputStream is=socket.getInputStream();
//读取并解析数据
data=parseData(is);
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
//返回读取到的内容
return data;
}
//读取并解析redis返回的数据
//返回解析后的结果
private static List<byte[]> parseData(InputStream is) throws IOException {
List<byte[]> re=new ArrayList<byte[]>();
byte[] b=new byte[1];
//读取第一个字符
is.read(b);
if(b[0]=='-') {
//redis指令执行错误
StringBuilder sb=new StringBuilder();
sb.append(new String(b));
while(true) {
is.read(b);
if(b[0]=='\r') {
//将最后的\n读取完,以便下次使用InputStream
is.read(b);
break;
}
sb.append(new String(b));
}
re.add(sb.toString().getBytes());
}else if(b[0]=='+') {
//redis指令执行成功
StringBuilder sb=new StringBuilder();
sb.append(new String(b));
while(true) {
is.read(b);
if(b[0]=='\r') {
//将最后的\n读取完,以便下次使用InputStream
is.read(b);
break;
}
sb.append(new String(b));
}
re.add(sb.toString().getBytes());
}else if(b[0]==':'){
//返回了一个整数
StringBuilder sb=new StringBuilder();
//第一个字符是冒号,需不需要一起返回随你
//sb.append(new String(b));
while(true) {
is.read(b);
if(b[0]=='\r') {
//将最后的\n读取完,以便下次使用InputStream
is.read(b);
break;
}
sb.append(new String(b));
}
re.add(sb.toString().getBytes());
}else if(b[0]=='$') {
//返回了正在查询的数据,并且该数据的数量只有1个
StringBuilder sb=new StringBuilder();
//读取对应数据的长度
while(true) {
is.read(b);
if(b[0]=='\r') {
break;
}
sb.append(new String(b));
}
//长度
int len=Integer.parseInt(sb.toString());
//下一个字符是\n,跳过
is.read(b);
if(len==-1) {
//没有数据
return re;
}
//按照刚才读取的长度,读取数据内容
byte[] info=new byte[len];
is.read(info, 0, len);
re.add(info);
//将最后的\r\n读取完,以便下次使用InputStream
is.read(b);
is.read(b);
}else if(b[0]=='*') {
//返回了正在查询的数据,并且该数据的数量有复数个
StringBuilder sb=new StringBuilder();
//读取数据的数量
while(true) {
is.read(b);
if(b[0]=='\r') {
break;
}
sb.append(new String(b));
}
//数量
int num=Integer.parseInt(sb.toString());
//下一个字符是\n,跳过
is.read(b);
if(num==0) {
//没有数据
return re;
}
//按照刚才读取的数量,依次读取数据内容
for(int i=0;i<num;i++) {
//读取数据的长度
//第1个字符是$,跳过
is.read(b);
StringBuilder sb2=new StringBuilder();
while(true) {
is.read(b);
if(b[0]=='\r') {
break;
}
sb2.append(new String(b));
}
//长度
int len=Integer.parseInt(sb2.toString());
//下一个字符是\n,跳过
is.read(b);
//按照刚才读取的长度,读取数据内容
byte[] info=new byte[len];
is.read(info, 0, len);
re.add(info);
//下一个字符是\r,跳过
is.read(b);
//下一个字符是\n,跳过
is.read(b);
}
}else {
//未知,应该是网络错误或其它什么异常导致的,亦或者是高版本的新特性
StringBuilder sb=new StringBuilder();
sb.append(new String(b));
while(true) {
is.read(b);
if(b[0]=='\r') {
//将最后的\n读取完,以便下次使用InputStream
is.read(b);
break;
}
sb.append(new String(b));
}
re.add(sb.toString().getBytes());
}
return re;
}
//测试连接
public static void main(String[] args) {
login();
showByte(test0());
showByte(test1());
showByte(test2());
showByte(test3());
showByte(test4());
close();
}
//将Redis返回的数据输出到控制台
private static void showByte(List<byte[]> res) {
if(res!=null) {
for(byte[] b:res) {
System.out.println(new String(b));
}
}
}
//测试ping指令
private static List<byte[]> test0(){
//ping
System.out.println("[debug] ping");
List<byte[]> res=new ArrayList<byte[]>();
res.add("ping".getBytes());
return runCommand(res);
}
//测试set指令
private static List<byte[]> test1(){
//set key1 abcd
System.out.println("[debug] set key1 abcd");
List<byte[]> res=new ArrayList<byte[]>();
res.add("set".getBytes());
res.add("key1".getBytes());
res.add("abcd".getBytes());
return runCommand(res);
}
//测试get指令
private static List<byte[]> test2(){
//get key1
System.out.println("[debug] get key1");
List<byte[]> res=new ArrayList<byte[]>();
res.add("get".getBytes());
res.add("key1".getBytes());
return runCommand(res);
}
//测试lrange指令
private static List<byte[]> test3(){
//lrange list1 0 10
System.out.println("[debug] lrange list1 0 10");
List<byte[]> res=new ArrayList<byte[]>();
res.add("lrange".getBytes());
res.add("list1".getBytes());
res.add("0".getBytes());
res.add("10".getBytes());
return runCommand(res);
}
//测试hdel指令
private static List<byte[]> test4(){
//hdel hashname hashkey
System.out.println("[debug] hdel hashname hashkey");
List<byte[]> res=new ArrayList<byte[]>();
res.add("hdel".getBytes());
res.add("hashname".getBytes());
res.add("hashkey".getBytes());
return runCommand(res);
}
}
部分Redis指令参考:
登录指令(仅Redis存在连接密码时使用,连接成功后需要先发送此指令才能使用其它指令)
- auth 密码
- select 编号
- info all
参考文献:resp协议
可以