Redis序列化协议

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

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序列化协议_第1张图片

我们使用redis客户端看到的数据都是解析过后的数据,可以windows可以使用telnet,Linux可以使用nc来处理。

telnet

Redis序列化协议_第2张图片

使用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。

Redis序列化协议_第3张图片

我们可以看到有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的包是一样的。

Redis序列化协议_第4张图片

其实这就是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下载

转载于:https://my.oschina.net/u/2474629/blog/913805

你可能感兴趣的:(Redis序列化协议)