Redis服务器与客户端通过RESP(REdis Serialization Protocol)协议通信。
RESP协议支持的数据类型
Simple String
第一个字节以+
开头,随后紧跟内容字符串(不能包含CR
LF
),最后以CRLF
结束。很多Redis命令执行成功时会返回"OK","OK"就是一个Simple String
:
"+OK\\r\\n"
Error
的结构与Simple String
很像,但是第一个字节以-
开头:
"-ERR unknown command 'foobar'"
-
符号后的第一个单词代表错误类型,ERR代表一般错误,WRONGTYPE代表在某种数据结构上执行了不支持的操作。
Integer
第一个字节以:
开头,随后紧跟数字,以CRLF
结束:
":1000\\r\\n"
很多Redis命令会返回Integer
,例如INCR
LLEN
等。
Bulk String
是一种二进制安全的字符串结构,整个结构包含两部分。第一部分以$
开头,后面紧跟字符串的字节长度,CRLF
结尾。第二部分是真正的字符串内容,CRLF
结尾,最大长度限制为512MB。一个Bulk String
结构的"Hello World!"是:
"$12\\r\\nHello World!\\r\\n"
空字符串是:
"$0\\r\\n\\r\\n"
nil是:
"$-1\\r\\n"
Array
也可以看成由两部分组成,第一部分以*
开头,后面紧跟一个数字代表Array
的长度,以CRLF
结束。第二部分是每个元素的具体值,可能是Integer
,可能是Bulk String
。Array
结构的["hello", "world"]是:
"*2\\r\\n$5\\r\\nhello\\r\\n$5\\r\\nworld\\r\\n"
空Array
:
"*-1\\r\\n"
通过RESP协议与服务端通信
了解了Redis协议的基本数据类型,就可以通过Socket与Redis Server进行通信:
package me.likeyao.yingzong.example;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.Charset;
/**
* Created by yingzong on 16/6/15.
*/
public class SimpleProtocol {
public static void main(String[] args) throws Exception{
Socket socket = new Socket();
//TIME_WAIT状态下可以复用端口
socket.setReuseAddress(true);
//空闲时发送数据包,确认服务端状态
socket.setKeepAlive(true);
//关闭Nagle算法,尽快发送
socket.setTcpNoDelay(true);
//调用close方法立即关闭socket,丢弃所有未发送的数据包
socket.setSoLinger(true, 0);
//连接server
socket.connect(new InetSocketAddress("localhost", 6379), 3000);
//设置读取时超时时间
socket.setSoTimeout(3000);
OutputStream os = socket.getOutputStream();
/**
* SET 命令
* 协议: array 3个元素 SET simpleKey simpleValue
*/
os.write(getBytes("*3\\r\\n$3\\r\\nSET\\r\\n$9\\r\\nsimpleKey\\r\\n$11\\r\\nsimpleValue\\r\\n"));
os.flush();
InputStream is = socket.getInputStream();
/**
* 解析SET命令的返回结果
*/
String result = analysisResult(is);
System.out.println("SET command response : " + result);
System.out.println();
/**
* GET 命令
* 协议: array 2个元素 GET simpleKey
*/
os.write(getBytes("*2\\r\\n$3\\r\\nGET\\r\\n$9\\r\\nsimpleKey\\r\\n"));
os.flush();
/**
* 解析GET命令返回结果
*/
String value = analysisResult(is);
System.out.println("GET command response : " + value);
is.close();
os.close();
socket.close();
}
/**
* 解析返回结果
* @param is
* @return
* @throws Exception
*/
private static String analysisResult(InputStream is) throws Exception{
/**
* 第一个字节指定返回的数据结构类型
*/
byte type = (byte)is.read();
System.out.println("response type is : " + (char)type);
if(type == '+'){
//Simple String类型
return readCRLF(is);
}else if(type == '$'){
//Bulk String类型
int len = readIntCRLF(is);
System.out.println("$ value len : " + len);
return readFixedLen(is, len);
}
return null;
}
/**
* 读取int值,直到遇到CRLF
* @param is
* @return
* @throws Exception
*/
private static int readIntCRLF(InputStream is) throws Exception{
return Integer.parseInt(readCRLF(is));
}
/**
* 读取字符串,直到遇到CRLF
* @param is
* @return
* @throws Exception
*/
private static String readCRLF(InputStream is) throws Exception{
byte b = (byte)is.read();
StringBuilder sb = new StringBuilder();
//不是最后一个输入字节时
while(b != -1){
//判断是否是CR,如果不是加入sb中
if(b != '\\r'){
sb.append((char)b);
}else{
//如果是CR,继续读取一个字节,如果不是LF,报错
byte oneMore = (byte)is.read();
if(oneMore != '\\n'){
throw new RuntimeException("CRLF error!");
}else{
break;
}
}
b = (byte)is.read();
}
return sb.toString();
}
/**
* 读取固定字节长度的字符串
* @param is
* @param len
* @return
* @throws Exception
*/
private static String readFixedLen(InputStream is, int len) throws Exception{
byte[] bytes = new byte[len];
for(int i = 0; i < len; i++){
bytes[i] = (byte)is.read();
}
//CR
is.read();
//LF
is.read();
return new String(bytes, "UTF-8");
}
private static byte[] getBytes(String str) throws Exception{
return str.getBytes(Charset.forName("UTF-8"));
}
}
=>
response type is : +
SET command response : OK
response type is : $
$ value len : 11
GET command response : simpleValue
代码执行了两个命令:
SET simpleKey "simpleValue" => "OK"
GET simpleKey => "simpleValue"
analysisResult方法目前只支持了+
$
两种格式的解析,其他格式解析方式类似,就不一一实现了。