2019独角兽企业重金招聘Python工程师标准>>>
RESP 发送命令格式
发送命令格式 RESP的规定一条命令的格式如下,CRLF代表"\r\n":
*<参数数量> CRLF
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF
以set hello world为例,发送的就是
*3
$3
SET
$5
hello
$5
world
第一行*3表示有3个参数,$3表示接下来的一个参数有3个字节,接下来是参数,$5表示下一个参数有5个字节,接下来是参数,$5表示下一个参数有5个字节,接下来是参数。
所以set hello world最终发送给redis服务器的命令是:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
RESP 响应命令格式
Redis的返回结果类型分为以下五种:
正确回复:在RESP中第一个字节为"+"
错误回复:在RESP中第一个字节为"-"
整数回复:在RESP中第一个字节为":"
字符串回复:在RESP中第一个字节为"$"
多条字符串回复:在RESP中第一个字节为"*"
(+) 表示一个正确的状态信息,具体信息是当前行+后面的字符。
(-) 表示一个错误信息,具体信息是当前行-后面的字符。
(*) 表示消息体总共有多少行,不包括当前行,*后面是具体的行数。
($) 表示下一行数据长度,不包括换行符长度\r\n,$后面则是对应的长度的数据。
(:) 表示返回一个数值,:后面是相应的数字节符。
所以set hello world收到的响应是+OK
我们使用redis客户端看到的数据都是解析过后的数据,可以windows可以使用telnet,Linux可以使用nc来处理。
使用Java Socket连接Redis服务器
既然知道了redis使用的是RESP协议,而RESP的底层使用的是TCP协议,当然可以利用Java的socket来连接redis服务器。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
public class RespStart {
private static final String host = "127.0.0.1";
private static final int port = 6379;
private static final String cmd = "*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n";
public static void main(String[] args) {
byte [] b = new byte[8192];
Socket socket = initSocket();
try {
OutputStream outputStream = socket.getOutputStream();
outputStream.write(cmd.getBytes());
} catch (IOException e1) {
e1.printStackTrace();
}
try {
InputStream inputStream = socket.getInputStream();
inputStream.read(b);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(new String(b));
}
private static Socket initSocket(){
Socket socket = new Socket();
try {
// close方法关闭Socket连接后,Socket对象所绑定的端口并不一定马上释放
// 系统有时在Socket连接关闭才会再确认一下是否有因为延迟面未到达的数据包
//避免重启时,有时候的port already bind
socket.setReuseAddress(true);
socket.setKeepAlive(true);//保持TCP连接,不释放资源
socket.setTcpNoDelay(true);//立即发送数据,不合并数据包
socket.setSoLinger(true, 0);//强制关闭连接,不阻塞close(阻塞0s)
socket.connect(new InetSocketAddress(host, port), 3000);
socket.setSoTimeout(3000);//读取数据阻塞超时时间3s(0是一直阻塞)
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return socket;
}
}
上面的例子就是利用socket连接redis服务器,并且获取到redis服务器的返回值+OK
我们可以通过Wireshark来抓取数据包看一下。Wireshark使用WinPcap是不能抓取到本地包的,可以在npcap下载一个npcap。
我们可以看到有8个TCP包。前3个TCP包是3次握手,第4次是发送命令数据,后4次是TCP关闭的4次握手。
下面使用jedis来执行set hello world:
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
public class SimpleRedisTest {
private Jedis jedis ;
@Before
public void setUp(){
jedis = new Jedis("127.0.0.1");
}
@Test
public void testSet(){
jedis.set("hello", "world");
}
}
可以发现使用jedis抓包和上面我们自己使用socket的包是一样的。
其实这就是Jedis的原理,jedis对RESP进行了较好的封装。下面就看一下jedis是如何封装RESP的。
Jedis对RESP的封装
redis.clients.jedis.Protocol(RESP协议封装) redis.clients.jedis.Connection(连接管理) redis.clients.util.RedisOutputStream(继承FilterOutputStream) redis.clients.util.RedisInputStream(继承FilterInputStream) 发送命令是封装在Protocol中,利用的方法是sendCommand
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
os.write(ASTERISK_BYTE);//*
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);//$
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
处理也是封装在Protocol中,利用的方式是方法是process
private static Object process(final RedisInputStream is) {
final byte b = is.readByte();
if (b == PLUS_BYTE) {//+
return processStatusCodeReply(is);
} else if (b == DOLLAR_BYTE) {//$
return processBulkReply(is);
} else if (b == ASTERISK_BYTE) {//*
return processMultiBulkReply(is);
} else if (b == COLON_BYTE) {//:
return processInteger(is);
} else if (b == MINUS_BYTE) {//-
processError(is);
return null;
} else {
throw new JedisConnectionException("Unknown reply: " + (char) b);
}
}
sendCommand就是将命令转换为RESP协议格式,process是根据响应的第一个字节进行不同处理的。对比上面的RESP协议图来看就非常清除了。
RedisOutputStream和RedisInputStream是在Connection设置代的,RedisOutputStream和RedisInputStream分别继承FilterOutputStream和FilterInputStream,继承这2个类的典型的装饰模式。是对socket获取的InputStream和OutputStream的装饰。
jedis把命令和数据读写分开了,一个命令对应一次响应。然而jedis不是线程安全的,所以多线程下很容易出现Unknown reply: x和ERR Protocol error: invalid bulk length这样的错误。一个是读缓冲区并发引起的错误,一个是写缓冲区并发引起的错误。这个具体的分析放在下一次来介绍。
参考
npcap下载