前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis 5通信协议解析以及手写一个Jedis客户端

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

作者头像
zoro
发布2019-04-11 15:43:16
9721
发布2019-04-11 15:43:16
举报
文章被收录于专栏:Java开发者Java开发者

Redis 5通信协议解析以及手写一个Jedis客户端
Redis系统介绍:

Redis的基础介绍与安装使用步骤:https://cloud.tencent.com/developer/article/1412664

Redis的基础数据结构与使用:https://cloud.tencent.com/developer/article/1412674

Redis核心原理:https://cloud.tencent.com/developer/article/1412670

Redis 5 之后版本的高可用集群搭建:https://cloud.tencent.com/developer/article/1412673

Redis 5 版本的高可用集群的水平扩展:https://cloud.tencent.com/developer/article/1412672

Redis 5 集群选举原理分析:https://cloud.tencent.com/developer/article/1412676

Redis 5 通信协议解析以及手写一个Jedis客户端:https://cloud.tencent.com/developer/article/1412671

优秀博客:

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)的。
请求协议:
代码语言:javascript
复制
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
举个例子, 以下是一个命令协议的打印版本:
代码语言:javascript
复制
*3
$3
SET
$5
mykey
$7
myvalue
这个命令的实际协议值如下:
代码语言:javascript
复制
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
返回协议:
Redis 命令会返回多种不同类型的回复。
通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:
代码语言:javascript
复制
状态回复(status reply)的第一个字节是 "+"
错误回复(error reply)的第一个字节是 "-"
整数回复(integer reply)的第一个字节是 ":"
批量回复(bulk reply)的第一个字节是 "$"
多条批量回复(multi bulk reply)的第一个字节是 "*"
状态回复
一个状态回复(或者单行回复,single line reply)是一段以 "+" 开始、 "\r\n" 结尾的单行字符串。
例如:
代码语言:javascript
复制
+OK

引用:http://doc.redisfans.com/topic/protocol.html

具体其他的也可以看下官网的介绍


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

启动后台redis服务

代码语言:javascript
复制
[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、如果出现下面这种异常:
代码语言:javascript
复制
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
linux执行下面命令,开放6379端口:
代码语言:javascript
复制
/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
2、关闭redis的保护模式
代码语言:javascript
复制
vim redis.conf
修改:
代码语言:javascript
复制
protected-mode no

Jedis代码:

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

代码语言:javascript
复制
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

代码很简单:

代码语言:javascript
复制
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"));
}
}
返回:
代码语言:javascript
复制
OK
xxx

先看下Jedis的类图:

2.png

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

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

set方法:
代码语言:javascript
复制
> 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[]...)

就大致这么几步调用,我们尝试自己写一个试试看

核心代码如下:

代码语言:javascript
复制
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();
    }
}

代码语言:javascript
复制
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);
        }
    }
}

代码语言:javascript
复制
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);
    }
}

代码语言:javascript
复制
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,试试能否成功。

代码语言:javascript
复制
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"));
    }
}

返回结果:

代码语言:javascript
复制
+OK

$4
xxxx

ok,成功了!如果有兴趣,可以尝试写一下:)。
如果感觉有帮助,可以点个喜欢:)。
如需转载,请注明出处,谢谢:)。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.12.18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis 5通信协议解析以及手写一个Jedis客户端
    • Redis系统介绍:
      • Redis 协议在以下三个目标之间进行折中:
        • 看一下redis的通信协议:
          • 请求协议:
            • 举个例子, 以下是一个命令协议的打印版本:
              • 这个命令的实际协议值如下:
                • 返回协议:
                  • Redis 命令会返回多种不同类型的回复。
                    • 通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:
                      • 状态回复
                        • 一个状态回复(或者单行回复,single line reply)是一段以 "+" 开始、 "\r\n" 结尾的单行字符串。
                          • 例如:
                            • 我们看下Jedis是如何连接后台redis服务的
                              • 注意:
                                • 1、如果出现下面这种异常:
                                • linux执行下面命令,开放6379端口:
                                • 2、关闭redis的保护模式
                                • 修改:
                                • Jedis代码:
                              • 返回:
                                • set方法:
                                • ok,成功了!如果有兴趣,可以尝试写一下:)。
                                • 如果感觉有帮助,可以点个喜欢:)。
                                • 如需转载,请注明出处,谢谢:)。
                            相关产品与服务
                            云数据库 Redis
                            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档