Redis 通信协议

原创 2017年09月08日 15:47:41

# 简介

几乎所有的主流编程语言都有Redis的客户端(http://redis.io/clients),不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:

  1. 客户端与服务端之间的通信协议是在 TCP 协议之上构建的。

    客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。

    客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。

  2. Redis制定了 RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

    发送命令

    RESP 在 Redis 1.2 版本中引入, 并最终在 Redis 2.0 版本成为 Redis 服务器通信的标准方式。

    在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。

    RESP 的规定一条命令的格式如下:

    *<参数数量> CR LF
    $<参数 1 的字节数量> CR LF
    <参数 1 的数据> CR LF
    ...
    $<参数 N 的字节数量> CR LF
    <参数 N 的数据> CR LF

    命令本身也作为协议的其中一个参数来发送。

    例如我们经常执行的 SET 命令,在命令行中我们输入如下:

    SET key value

    使用 RESP 协议规定的格式:

    *3
    $3
    SET
    $3 # 这里 key 一共三个字节
    key
    $5 # 这里 value 一共五个字节
    value

    这个命令的实际协议值如下:

    "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"

    回复

    Redis 命令会返回多种不同类型的回复。

    通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:

    • 状态回复(status reply)的第一个字节是 "+"
    • 错误回复(error reply)的第一个字节是 "-"
    • 整数回复(integer reply)的第一个字节是 ":"
    • 批量回复(bulk reply)的第一个字节是 "$"
    • 多条批量回复(multi bulk reply)的第一个字节是 "*"

    我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就按照RESP进行结果解析的,所以看不到中间结果,redis-cli.c 源码对命令结果的解析结构如下:

    static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
      sds out = sdsempty();
      switch (r->type) {
              // 处理错误回复
      case REDIS_REPLY_ERROR:
          out = sdscatprintf(out,"(error) %s\n", r->str);
      break;
              // 处理状态回复
      case REDIS_REPLY_STATUS:
          out = sdscat(out,r->str);
          out = sdscat(out,"\n");
      break;
              // 处理整数回复
      case REDIS_REPLY_INTEGER:
          out = sdscatprintf(out,"(integer) %lld\n",r->integer);
      break;
              // 处理字符串回复
      case REDIS_REPLY_STRING:
          /* If you are producing output for the standard output we want
          * a more interesting output with quoted characters and so forth */
          out = sdscatrepr(out,r->str,r->len);
          out = sdscat(out,"\n");
      break;
              // 处理 nil
      case REDIS_REPLY_NIL:
          out = sdscat(out,"(nil)\n");
      break;
              // 处理多回复
      case REDIS_REPLY_ARRAY:
          if (r->elements == 0) {
              out = sdscat(out,"(empty list or set)\n");
          } else {
              unsigned int i, idxlen = 0;
              char _prefixlen[16];
              char _prefixfmt[16];
              sds _prefix;
              sds tmp;
    
              /* Calculate chars needed to represent the largest index */
              i = r->elements;
              do {
                  idxlen++;
                  i /= 10;
              } while(i);
    
              /* Prefix for nested multi bulks should grow with idxlen+2 spaces */
              memset(_prefixlen,' ',idxlen+2);
              _prefixlen[idxlen+2] = '\0';
              _prefix = sdscat(sdsnew(prefix),_prefixlen);
    
              /* Setup prefix format for every entry */
              snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%ud) ",idxlen);
    
              for (i = 0; i < r->elements; i++) {
                  /* Don't use the prefix for the first element, as the parent
                   * caller already prepended the index number. */
                  out = sdscatprintf(out,_prefixfmt,i == 0 ? "" : prefix,i+1);
    
                  /* Format the multi bulk entry */
                  tmp = cliFormatReplyTTY(r->element[i],_prefix);
                  out = sdscatlen(out,tmp,sdslen(tmp));
                  sdsfree(tmp);
              }
              sdsfree(_prefix);
          }
      break;
      default:
          fprintf(stderr,"Unknown reply type: %d\n", r->type);
          exit(1);
      }
      return out;
    }

    发送命令 一节中使用的格式除了用作命令请求协议之外, 也用在命令的回复协议中: 这种只有一个参数的回复格式被称为批量回复(Bulk Reply)

    统一协议请求原本是用在回复协议中, 用于将列表的多个项返回给客户端的, 这种回复格式被称为多条批量回复(Multi Bulk Reply)

    一个多条批量回复以 *<argc>\r\n 为前缀, 后跟多条不同的批量回复, 其中 argc 为这些批量回复的数量。

    状态回复

    一个状态回复(或者单行回复,single line reply)是一段以 "+" 开始、 "\r\n" 结尾的单行字符串。

    以下是一个状态回复的例子:

    +OK

    客户端库应该返回 "+" 号之后的所有内容。 比如在在上面的这个例子中, 客户端就应该返回字符串 "OK"

    状态回复通常由那些不需要返回数据的命令返回,这种回复不是二进制安全的,它也不能包含新行。

    状态回复的额外开销非常少,只需要三个字节(开头的 "+" 和结尾的 CRLF)。

    错误回复

    错误回复和状态回复非常相似, 它们之间的唯一区别是, 错误回复的第一个字节是 "-" , 而状态回复的第一个字节是 "+"

    错误回复只在某些地方出现问题时发送: 比如说, 当用户对不正确的数据类型执行命令, 或者执行一个不存在的命令, 等等。

    一个客户端库应该在收到错误回复时产生一个异常。

    以下是两个错误回复的例子:

    -ERR unknown command 'foobar'
    -WRONGTYPE Operation against a key holding the wrong kind of value

    "-" 之后,直到遇到第一个空格或新行为止,这中间的内容表示所返回错误的类型。

    ERR 是一个通用错误,而 WRONGTYPE 则是一个更特定的错误。 一个客户端实现可以为不同类型的错误产生不同类型的异常, 或者提供一种通用的方式, 让调用者可以通过提供字符串形式的错误名来捕捉(trap)不同的错误。

    不过这些特性用得并不多, 所以并不是特别重要, 一个受限的(limited)客户端可以通过简单地返回一个逻辑假(false)来表示一个通用的错误条件。

    整数回复

    整数回复就是一个以 ":" 开头, CRLF 结尾的字符串表示的整数。

    比如说, ":0\r\n"":1000\r\n" 都是整数回复。

    返回整数回复的其中两个命令是 INCRLASTSAVE 。 被返回的整数没有什么特殊的含义, INCR 返回键的一个自增后的整数值, 而 LASTSAVE 则返回一个 UNIX 时间戳, 返回值的唯一限制是这些数必须能够用 64 位有符号整数表示。

    整数回复也被广泛地用于表示逻辑真和逻辑假: 比如 EXISTSSISMEMBER 都用返回值 1 表示真, 0 表示假。

    其他一些命令, 比如 SADDSREMSETNX , 只在操作真正被执行了的时候, 才返回 1 , 否则返回 0

    以下命令都返回整数回复: SETNXDELEXISTSINCRINCRBYDECRDECRBYDBSIZELASTSAVERENAMENXMOVELLENSADDSREMSISMEMBERSCARD

    批量回复

    服务器使用批量回复来返回二进制安全的字符串,字符串的最大长度为 512 MB 。

    客户端:GET mykey
    服务器:foobar

    服务器发送的内容中:

    • 第一字节为 "$" 符号
    • 接下来跟着的是表示实际回复长度的数字值
    • 之后跟着一个 CRLF
    • 再后面跟着的是实际回复数据
    • 最末尾是另一个 CRLF

    对于前面的 GET 命令,服务器实际发送的内容为:

    "$6\r\nfoobar\r\n"

    如果被请求的值不存在, 那么批量回复会将特殊值 -1 用作回复的长度值, 就像这样:

    客户端:GET non-existing-key
    服务器:$-1

    这种回复称为空批量回复(NULL Bulk Reply)。

    当请求对象不存在时,客户端应该返回空对象,而不是空字符串: 比如 Ruby 库应该返回 nil , 而 C 库应该返回 NULL (或者在回复对象中设置一个特殊标志), 诸如此类。

    多条批量回复

    LRANGE 这样的命令需要返回多个值, 这一目标可以通过多条批量回复来完成。

    多条批量回复是由多个回复组成的数组, 数组中的每个元素都可以是任意类型的回复, 包括多条批量回复本身。

    多条批量回复的第一个字节为 "*" , 后跟一个字符串表示的整数值, 这个值记录了多条批量回复所包含的回复数量, 再后面是一个 CRLF 。

    客户端: LRANGE mylist 0 3
    服务器: *4
    服务器: $3
    服务器: foo
    服务器: $3
    服务器: bar
    服务器: $5
    服务器: Hello
    服务器: $5
    服务器: World

    在上面的示例中,服务器发送的所有字符串都由 CRLF 结尾。

    正如你所见到的那样, 多条批量回复所使用的格式, 和客户端发送命令时使用的统一请求协议的格式一模一样。 它们之间的唯一区别是:

    • 统一请求协议只发送批量回复。
    • 而服务器应答命令时所发送的多条批量回复,则可以包含任意类型的回复。

    以下例子展示了一个多条批量回复, 回复中包含四个整数值, 以及一个二进制安全字符串:

    *5\r\n
    :1\r\n
    :2\r\n
    :3\r\n
    :4\r\n
    $6\r\n
    foobar\r\n

    在回复的第一行, 服务器发送 *5\r\n , 表示这个多条批量回复包含 5 条回复, 再后面跟着的则是 5 条回复的正文。

    多条批量回复也可以是空白的(empty), 就像这样:

    客户端: LRANGE nokey 0 1
    服务器: *0\r\n

    无内容的多条批量回复(null multi bulk reply)也是存在的, 比如当 BLPOP 命令的阻塞时间超过最大时限时, 它就返回一个无内容的多条批量回复, 这个回复的计数值为 -1

    客户端: BLPOP key 1
    服务器: *-1\r\n

    客户端库应该区别对待空白多条回复和无内容多条回复: 当 Redis 返回一个无内容多条回复时, 客户端库应该返回一个 null 对象, 而不是一个空数组。

    多条批量回复中的空元素

    多条批量回复中的元素可以将自身的长度设置为 -1 , 从而表示该元素不存在, 并且也不是一个空白字符串(empty string)。

    SORT 命令使用 GET pattern 选项对一个不存在的键进行操作时, 就会发生多条批量回复中带有空白元素的情况。

    以下例子展示了一个包含空元素的多重批量回复:

    服务器: *3
    服务器: $3
    服务器: foo
    服务器: $-1
    服务器: $3
    服务器: bar
    

    其中, 回复中的第二个元素为空。

    对于这个回复, 客户端库应该返回类似于这样的回复:

    ["foo", nil, "bar"]

    多命令和 pipline

    客户端可以通过 pipline , 在一次写入操作中发送多个命令:

    • 在发送新命令之前, 无须阅读前一个命令的回复。
    • 多个命令的回复会在最后一并返回。

    内联命令

    当你需要和 Redis 服务器进行沟通, 但又找不到 redis-cli , 而手上只有 telnet 的时候, 你可以通过 Redis 特别为这种情形而设的内联命令格式来发送命令。

    以下是一个客户端和服务器使用内联命令来进行交互的例子:

    客户端: PING
    服务器: +PONG
    

    以下另一个返回整数值的内联命令的例子:

    客户端: EXISTS somekey
    服务器: :0
    

    因为没有了统一请求协议中的 "*" 项来声明参数的数量, 所以在 telnet 会话输入命令的时候, 必须使用空格来分割各个参数, 服务器在接收到数据之后, 会按空格对用户的输入进行分析(parse), 并获取其中的命令参数。

    高性能 Redis 协议分析器

    尽管 Redis 的协议非常利于人类阅读, 定义也很简单, 但这个协议的实现性能仍然可以和二进制协议一样快。

    因为 Redis 协议将数据的长度放在数据正文之前, 所以程序无须像 JSON 那样, 为了寻找某个特殊字符而扫描整个 payload , 也无须对发送至服务器的 payload 进行转义(quote)。

    程序可以在对协议文本中的各个字符进行处理的同时, 查找 CR 字符, 并计算出批量回复或多条批量回复的长度, 就像这样:

    
    #include <stdio.h>
    
    
    int main(void) {
      unsigned char *p = "$123\r\n";
      int len = 0;
    
      p++;
      while(*p != '\r') {
          len = (len*10)+(*p - '0');
          p++;
      }
    
      /* Now p points at '\r', and the len is in bulk_len. */
      printf("%d\n", len);
      return 0;
    }

    得到了批量回复或多条批量回复的长度之后, 程序只需调用一次 read 函数, 就可以将回复的正文数据全部读入到内存中, 而无须对这些数据做任何的处理。

    在回复最末尾的 CR 和 LF 不作处理,丢弃它们。

    Redis 协议的实现性能可以和二进制协议的实现性能相媲美, 并且由于 Redis 协议的简单性, 大部分高级语言都可以轻易地实现这个协议, 这使得客户端软件的 bug 数量大大减少。

    Linux 下 使用 nc 命令操作 Redis

    [coderknock ~]# nc 127.0.0.1 6379
    set hello world
    +OK                          #状态回复
    sethx
    -ERR unknown command 'sethx' #错误回复:由于sethx这条命令不存在,那么返回结果就是"-"号加上错误消息
    incr counter
    :1                           #整数回复:当命令的执行结果是整数时,返回结果就是整数回复,例如 incr、exists、del、dbsize返回结果都是整数
    get hello
    $5                           #字符串回复:当命令的执行结果是字符串时,返回结果就是字符串回复。
    world                        #实际返回的是 $5\r\nworld\r\n 
    mset java jedis python redis-py
    +OK
    mget java python             #多条字符串回复:当命令的执行结果是多条字符串时,返回结果就是多条字符串回复
    *2
    $5
    jedis
    $8
    redis-py
    get not_exist_key            #无论是字符串回复还是多条字符串回复,如果有 nil 值,那么会返回$-1。
    $-1
    mget hello not_exist_key java
    *3
    $5
    world
    $-1
    $5
    jedis

    Python Socket 操作 Redis

    使用 socket 操作 Redis:

    import socket
    
    
    # AF_INET指定使用 IPv4 协议
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.SOL_TCP)
    s.connect(('127.0.0.1', 6379))
    print('get connected from', '127.0.0.1')
    
    # 验证密码
    
    s.send(b'*2\r\n$4\r\nAUTH\r\n$8\r\nadmin123\r\n')
    ra = s.recv(512)
    print(ra)
    
    # 发送一条信息
    
    s.send(b'*3\r\n$3\r\nSET\r\n$8\r\ntestRESP\r\n$10\r\nRESPpython\r\n')
    ra = s.recv(512)
    print(ra)
    
    s.close()

    执行结果:

    get connected from 127.0.0.1
    b'+OK\r\n'
    b'+OK\r\n'

    我们从命令行中查询:

    127.0.0.1:6379> GET testRESP
    "RESPpython"

    可以看到正确的向 Redis 中插入了键值。500000009894242)

我是广告

本人 Redis 方面的讲座已经上线快去看看有没有你感兴趣的吧(说不定会有优惠哟~~数量有限要抓紧呀):

[Spring Boot + Redis 实现 论坛系统之项目整体规划] (https://segmentfault.com/l/1500000010993497) 10月9日之前有99份优惠名额,有兴趣的同学赶快入手吧

提升职业竞争力的必备知识 Redis 之初识

提升职业竞争力的必备知识 Redis 之高级特性

提升职业竞争力的必备知识 Redis 之缓存与Session共享

提升职业竞争力的必备知识 Redis 之排行榜与附近的人

以上课程现在可以打包购买了 Redis 系列讲座合集
打包购买更实惠

版权声明:本文为博主原创文章,转载请标注出处

Redis Shell

Redis提供了redis-cli、redis-server、redis-benchmark等Shell工具。它们虽然比较简单,但是麻雀虽小五脏俱全,有时可以很巧妙地解决一些问题。Redis提供了re...

Redis Shell

Redis提供了redis-cli、redis-server、redis-benchmark等Shell工具。它们虽然比较简单,但是麻雀虽小五脏俱全,有时可以很巧妙地解决一些问题。Redis提供了re...

Redis研究(十三)—安全和通信协议

一、安全       Redis的作者Salvatore Sanfilippo曾经发表过Redis宣言,其中提到Redis以简洁为美。同样在安全层面Redis也没有做太多的工作。 1、可信的环境  ...

Redis通信协议(protocol)

Redis通信协议(protocol) 本文档翻译自: http://redis.io/topics/protocol 。 Redis 协议在以下三个目标之间进行折中: 易于实现...

Redis 通信协议

客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。 客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。 Redis 服务器通信协议中, 所有发送至 ...

Jedis - Redis通信协议

package redis.clients.jedis; import java.io.IOException; import java.util.ArrayList; import java.ut...

redis通信协议

通信协议(protocol) 本文档翻译自: http://redis.io/topics/protocol 。 Redis 协议在以下三个目标之间进行折中: 易于实现可以高效地...

Redis学习之通信协议详解

本文和大家分享的主要是redis 通信协议相关内容,一起来看看吧,希望对大家 学习redis有所帮助。   几乎所有的主流编程语言都有Redis 的客户端,不考虑 Redis 非常流行的原因,如...

redis networking通信协议的源码分析

networking的代码主要是针对client的命令进行处理,主要是实现三个功能:client连接的管理; 解析client的请求;发送回复内容给client。...

Redis基础学习--持久化(数据备份与恢复)、复制、安全、通信协议、管理工具

二、复制     通过持久化功能,Redis保证了即使在服务器重启的情况下也不会损失(或少量损失)数据。但是由于数据是存储在一台服务器上的,如果这台服务器的硬盘出现故障,也会导致数据丢失。为了避免单点...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Redis 通信协议
举报原因:
原因补充:

(最多只允许输入30个字)