文档章节

Redis序列化协议

trayvon
 trayvon
发布于 2017/06/03 17:00
字数 1367
阅读 229
收藏 1

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

RESP

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

telnet

telnet_cmd

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

loopback

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

loopback

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

© 著作权归作者所有

共有 人打赏支持
下一篇: Zookeeper ACL
trayvon
粉丝 14
博文 125
码字总数 185343
作品 1
程序员
私信 提问
Netty 源码中对 Redis 协议的实现

近期一直在做网络协议相关的工作,所以博客也就与之相关的比较多,今天楼主结合 Redis的协议 RESP 看看在 Netty 源码中是如何实现的。 RESP 协议 RESP 是 Redis 序列化协议的简写。它是一种直...

haifeiWu
08/09
0
0
Spring Data操作Redis时,发现key值出现\xAC\xED\x00\x05t\x00

最近在研究Redis,以及spring data对redis的支持发现了一个奇怪的现象 先说现象吧,通过redisTemplate下的opsForHash方法存储hash类型的值,操作成功以后,去redis控制台显示keys * 的时候,...

楠木楠
2016/12/10
488
0
小路/async-redis-client

async-redis-client 基于netty实现的非阻塞redis客户端 AsyncRedisClient client = new NettyRedisClient("172.16.3.213:6379", 1, null);String result = client.set("TEST_KEY2", "CACHED......

小路
2015/03/02
0
0
PHP异步协程框架 - Group-Co

PHP异步协程框架,支持SOA服务化调用,支持并行、串行调用。 支持异步日志,异步文件读写,异步Mysql,异步Redis,Mysql,Redis连接池。 为什么写这个框架? 利用协程特性以同步方式来编写异步代码...

coco1225
2017/11/02
0
0
MqttWk 1.0.3 发布,Java 物联网 MQTT 消息服务

基于 nutzboot + netty + redis + kafka 实现的MQTT消息服务(t-io版本预研中) 1.0.1-netty 至 1.0.3-netty更新内容: 完善QoS=1/2时messageId生成方法 完善协议解析的异常处理 完善Channel...

Wizzer
08/10
1K
2

没有更多内容

加载失败,请刷新页面

加载更多

windows10小鹤双拼注册表

《安全第一》直接发文本内容,自己建文本,改文本后缀reg。 使用方法: 1.复制下示文本内容,打开你的文本编辑器(#记事本notepad或其他++),粘贴文本内容并保存在任意位置(不影响使用) ...

漫步海边小路
8分钟前
0
0
一、数据挖掘

数据挖掘的发展动力---需要是发明之母 数据爆炸问题 自动数据收集工具和成熟的数据库技术使得大量的数据被收集,存储在数据库、数据仓库或其他信息库中以待分析。我们拥有丰富的数据,但却缺...

凯文加内特
11分钟前
0
0
Java线程池ThreadPoolExecutor

线程池 ThreadPoolExecutor 线程池是ExecutorService的实现,可以通过Executors执行工厂构造不同类型的执行服务(线程池)。 ThreadPoolExecutor线程池使用的是阻塞队列BlockingQueue。 用于...

器石_
12分钟前
1
0
Mybatis 实现SQL拦截并在控制台打印SQL和参数

注:可以拦截sql 执行时间,优化sql。并打印sql 以及参数 第一步:创建类: SqlPrintInterceptor 并实现 Interceptor 该类如下: package com.ra.common.plugin;import org.apache.ibati...

轻量级赤影
15分钟前
1
0
Log4j2 配置

config: <?xml version="1.0" encoding="UTF-8"?><!-- http://logging.apache.org/log4j/2.x/manual/appenders.html --><!-- status 负责打印日记系统的 WARN 级别以及以上的日记 --><C......

Credo-Zhao
15分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部