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

1.jpg

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,成功了!如果有兴趣,可以尝试写一下:)。
如果感觉有帮助,可以点个喜欢:)。
如需转载,请注明出处,谢谢:)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容