Redis serialization protocol

Keywords: socket Jedis Redis Java

RESP Send Command Format

The sending command format RESP specifies the following format for a command. CRLF stands for "r\n":

* < Number of Parameters > CRLF
 Number of bytes in parameter 1 > CRLF
 Parameter 1 > CRLF
...
Number of bytes of parameter N > CRLF
 Parameter N > CRLF

In the case of set hello world, what you send is

*3
$3
SET
$5
hello
$5
world

The first line * 3 denotes three parameters, $3 denotes the next parameter has three bytes, followed by the parameter, $5 denotes the next parameter has five bytes, followed by the parameter, $5 denotes the next parameter has five bytes, followed by the parameter.

So the command set hello world eventually sends to the redis server is:

*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n

RESP response command format

Redis returns five types of results:
        Correct response: The first byte in RESP is "+"
        Error response: The first byte in RESP is "-"
        Integer reply: The first byte in RESP is ":".
        String reply: The first byte in RESP is "$"
        Multiple string replies: The first byte in RESP is "*"

(+) represents a correct state information, the specific information is the current line + after the character.
(-) Represents an error message, the specific information is the current line-after character.
(*) denotes the total number of rows in the message body, excluding the current row, followed by the specific number of rows.
($) denotes the length of the next line of data, excluding the length of the newline character r\n, $followed by the corresponding length of data.
(:) Represents returning a numeric value, followed by the corresponding numeric stanzas.

So the response set hello world receives is + OK

The data we see using redis client are all parsed data, which can be processed by telnet in windows and nc in Linux.

Connecting Redis Server with Java Socket

Now that you know redis uses RESP protocol, and the bottom layer of RESP uses TCP protocol, you can use Java socket to connect redis server.

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 {
//          When the close method closes the Socket connection, the port bound by the Socket object is not necessarily released immediately.
//          Sometimes when the Socket connection is closed, the system reconfirms whether there are any packets that have not arrived due to the delay plane.
            //Avoid port already bind when restarting
            socket.setReuseAddress(true);
            socket.setKeepAlive(true);//Keep TCP Connections, Do not Release Resources
            socket.setTcpNoDelay(true);//Send data immediately, disassemble data packets
            socket.setSoLinger(true, 0);//Force closure of connection without blocking close (blocking 0s)
            socket.connect(new InetSocketAddress(host, port), 3000);
            socket.setSoTimeout(3000);//Read data blocking timeout 3s(0 is always blocked)
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return socket;
    }

}

The example above is using socket to connect redis server and get the return value of redis server + OK.

We can use Wireshark to grab the data package and look at it. Wireshark can't grab local packages using WinPcap. It can grab local packages using WinPcap. npcap Download an npcap.

We can see eight TCP packets. The first three TCP packets shake hands three times, the fourth is to send command data, and the last four are to shake hands four times when TCP closes.

Now use jedis to execute 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");
    }

}

You can find that using jedis to grab the package is the same as using socket on our own.

In fact, this is Jedis's principle. Jedis has a better encapsulation of RESP. Let's see how jedis encapsulates RESP.

Jedis's encapsulation of RESP

redis.clients.jedis.Protocol(RESP protocol encapsulation) Redis. clients. jedis. Connection (connection management) Redis. clients. util. RedisOutputStream (inheriting FilterOutputStream) Redis. clients. util. RedisInputStream (inheriting FilterInputStream) Sending commands is encapsulated in Protocol s, using 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);
    }
  }

Processing is also encapsulated in Protocol s, using the method of 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 converts commands into RESP protocol formats, and processes are processed differently according to the first byte of the response. Compared with the RSEP protocol diagram above, it is very clear.

RedisOutputStream and RedisInputStream are set up in Connection generation. RedisOutputStream and RedisInputStream inherit FilterOutputStream and FilterInputStream respectively, and inherit the typical decorative patterns of these two classes. It is the decoration of InputStream and OutputStream acquired by socket.

Jedis separates commands from data reading and writing, and a command corresponds to a response. However, jedis is not thread-safe, so errors like Unknown reply: x and ERR Protocol error: invalid bulk length can easily occur under multithreading. One is the error caused by read buffer concurrency and the other is the error caused by write buffer concurrency. This specific analysis will be introduced next time.

Reference resources

npcap Download

Posted by zeno on Tue, 25 Jun 2019 12:00:30 -0700