Redis 5通信协议解析以及手写一个Jedis客户端

Redis 5通信协议解析以及手写一个Jedis客户端

Redis系统介绍:

Redis的基础介绍与安装使用步骤:https://www.jianshu.com/p/2a23257af57b Redis的基础数据结构与使用:https://www.jianshu.com/p/c95c8450c5b6 Redis核心原理:https://www.jianshu.com/p/4e6b7809e10a Redis 5 之后版本的高可用集群搭建:https://www.jianshu.com/p/8045b92fafb2 Redis 5 版本的高可用集群的水平扩展:https://www.jianshu.com/p/6355d0827aea Redis 5 集群选举原理分析:https://www.jianshu.com/p/e6894713a6d5 Redis 5 通信协议解析以及手写一个Jedis客户端:https://www.jianshu.com/p/575544f68615

优秀博客: Redis Protocol specification:https://redis.io/topics/protocol 通信协议(protocol):http://doc.redisfans.com/topic/protocol.html


redis的通信协议是什么?我的理解是双方约定了一种编码方式,客户端将要发送的命令进行编码,然后服务端收到后,使用同样的协议进行解码,服务端处理完成后,再次编码返回给客户端,客户端解码拿到返回结果,这样就完成了一次通信。如下图:

1.png

Redis 协议在以下三个目标之间进行折中:
  • 易于实现
  • 可以高效地被计算机分析(parse)
  • 可以很容易地被人类读懂

简单来说:简单,高效,易读。

看一下redis的通信协议:
  • 客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
  • 客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。
  • 在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。
请求协议:
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
举个例子, 以下是一个命令协议的打印版本:
*3
$3
SET
$5
mykey
$7
myvalue
这个命令的实际协议值如下:
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
返回协议:
Redis 命令会返回多种不同类型的回复。
通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:
状态回复(status reply)的第一个字节是 "+"
错误回复(error reply)的第一个字节是 "-"
整数回复(integer reply)的第一个字节是 ":"
批量回复(bulk reply)的第一个字节是 "$"
多条批量回复(multi bulk reply)的第一个字节是 "*"
状态回复
一个状态回复(或者单行回复,single line reply)是一段以 "+" 开始、 "\r\n" 结尾的单行字符串。
例如:
+OK

引用:http://doc.redisfans.com/topic/protocol.html 具体其他的也可以看下官网的介绍


我们看下Jedis是如何连接后台redis服务的

启动后台redis服务

[root@localhost redis-5.0.2]# src/redis-server redis.conf
2800:C 17 Dec 2018 22:53:50.981 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2800:C 17 Dec 2018 22:53:50.982 # Redis version=5.0.2, bits=64, commit=00000000, modified=0, pid=2800, just started
2800:C 17 Dec 2018 22:53:50.982 # Configuration loaded
[root@localhost redis-5.0.2]# ps -ef|grep redis
root       2801      1  0 22:53 ?        00:00:00 src/redis-server *:6379
root       2806   2674  0 22:53 pts/0    00:00:00 grep --color=auto redis
[root@localhost redis-5.0.2]# 
注意:
1、如果出现下面这种异常:
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
linux执行下面命令,开放6379端口:
/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
2、关闭redis的保护模式
vim redis.conf
修改:
protected-mode no

Jedis代码:

pom依赖,我们目前使用jedis-2.9.0,可以连接单台redis,也可以连接集群,也可以开监控:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

代码很简单:

package com.demo.redis.client;

import redis.clients.jedis.Jedis;

public class RedisClient {

public static void main(String[] args) {
    Jedis jedis = new Jedis("192.168.5.100",6379);
    System.out.println(jedis.set("name","xxx"));
    System.out.println(jedis.get("name"));
}
}
返回:
OK
xxx

先看下Jedis的类图:

2.png

大家可以自己点进去看一下,其实很清晰。

具体Jedis是怎么调用的,如果我们点进去看一下:

set方法:
> redis.clients.jedis.Jedis#set(java.lang.String, java.lang.String)
    >redis.clients.jedis.Client#set(java.lang.String, java.lang.String)
        >redis.clients.jedis.BinaryClient#set(byte[], byte[])
        >redis.clients.jedis.Connection#sendCommand(redis.clients.jedis.Protocol.Command, byte[]...)
            >redis.clients.jedis.Protocol#sendCommand(redis.clients.util.RedisOutputStream, redis.clients.jedis.Protocol.Command, byte[]...)
            >redis.clients.jedis.Protocol#sendCommand(redis.clients.util.RedisOutputStream, byte[], byte[]...)

就大致这么几步调用,我们尝试自己写一个试试看 核心代码如下:

package com.demo.redis.client;

import com.demo.redis.connection.Connection;
import com.demo.redis.protocol.Protocol;

/**
 *  提供api服务
 *  @author zyy
 *  @date 2018年12月17日
 * */
public class Client {
    private Connection connection;

    public Client(String host, int port) {
        connection = new Connection(host, port);
    }

    public String set(String key, String value) {
        set(SafeEncoder.encode(key), SafeEncoder.encode(value));
        return connection.getStatusReply();
    }

    public void set(byte[] key, byte[] value) {
        this.connection.sendCommand(Protocol.Command.SET,new byte[][]{key,value});
    }

    public String get(String key) {
        this.connection.sendCommand(Protocol.Command.GET,SafeEncoder.encode(key));
        return connection.getStatusReply();
    }
}

package com.demo.redis.client;

import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.exceptions.JedisException;

import java.io.UnsupportedEncodingException;


/**
 *  编码
 *  @author zyy
 *  @date 2018年12月17日
 * */
public class SafeEncoder {

    public static byte[] encode(String str) {
        try {
            if (str == null) {
                throw new JedisDataException("value sent to redis cannot be null");
            } else {
                return str.getBytes("UTF-8");
            }
        } catch (UnsupportedEncodingException var2) {
            throw new JedisException(var2);
        }
    }
}

package com.demo.redis.connection;


import com.demo.redis.protocol.Protocol;
import redis.clients.jedis.exceptions.JedisConnectionException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * 建立连接
 *
 * @author zyy
 * @date 2018年12月17日
 */
public class Connection {
    private Socket socket;
    private String host;
    private int port;
    private OutputStream outputStream;
    private InputStream inputStream;

    public Connection(String host, int port) {
        this.host = host;
        this.port = port;
    }

    //发送命令
    public Connection sendCommand(Protocol.Command cmd, byte[]... args) {
        try {
            this.connect();
            Protocol.sendCommand(this.outputStream, cmd, args);
            //++this.pipelinedCommands;
            return this;
        } catch (JedisConnectionException var6) {
            throw var6;
        }
    }

    //如果未建立连接,则scoket 连接
    public void connect() {
        try {
            if (!isConnected()) {
                socket = new Socket(host, port);
                inputStream = socket.getInputStream();
                outputStream = socket.getOutputStream();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //判断是否已建立连接
    public boolean isConnected() {
        return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()
                && !socket.isInputShutdown() && !socket.isOutputShutdown();
    }

    //获取返回信息
    public String getStatusReply() {
        byte b[] = new byte[1024];
        try {
            socket.getInputStream().read(b);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new String(b);
    }
}

package com.demo.redis.protocol;

import java.io.IOException;
import java.io.OutputStream;

/**
 *  进行协议编码
 *  @author zyy
 *  @date 2018年12月17日
 * */
public class Protocol {
    /**
     * *    <参数数量> CR LF
     * $    <参数 1 的字节数量> CR LF
     *      <参数 1 的数据> CR LF
     *      ...
     * $    <参数 N 的字节数量> CR LF
     *      <参数 N 的数据> CR LF
     * */

    public static final String PARAM_BYTE_NUM  = "$";
    public static final String PARAM_NUM       = "*";
    public static final String TERMINATION     = "\r\n";


    public static void sendCommand(OutputStream outputStream, Command command, byte[]... b) {
        /*
            照着 SET mykey myvalue 的格式进行编码:
            *3
            $3
                SET
            $5
                mykey
            $7
                myvalue
            最终如下:
            "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
        */
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(PARAM_NUM).append(b.length + 1).append(TERMINATION);
        stringBuffer.append(PARAM_BYTE_NUM).append(command.name().length()).append(TERMINATION);
        stringBuffer.append(command).append(TERMINATION);
        for (byte[] arg : b) {
            stringBuffer.append(PARAM_BYTE_NUM).append(arg.length).append(TERMINATION);
            stringBuffer.append(new String(arg)).append(TERMINATION);
        }
        try {
            outputStream.write(stringBuffer.toString().getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static enum Command {
        SET,
        GET;
    }
}

ok,我们调用下自己写的client,试试能否成功。

package com.demo.redis.client;

public class Jedis {
    public static void main(String[] args) {
        Client client = new Client("192.168.5.100",6379);
        System.out.println(client.set("name","xxxx"));
        System.out.println(client.get("name"));
    }
}

返回结果:

+OK

$4
xxxx

ok,成功了!如果有兴趣,可以尝试写一下:)。
如果感觉有帮助,可以点个喜欢:)。
如需转载,请注明出处,谢谢:)。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券