V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
kindjeff
V2EX  ›  分享创造

用 Python 实现 redis 方轮子-第 0 篇-了解通信协议

  •  2
     
  •   kindjeff ·
    sljeff · 2018-09-10 00:42:18 +08:00 · 2733 次点击
    这是一个创建于 2267 天前的主题,其中的信息可能已经有所发展或是发生改变。

    初识 RESP

    当我们想实现一个 redis server,首先要了解 redis 的通信协议。

    redis 作者认为数据库系统的瓶颈一般不在于网络流量上,所以使用了一个简单的纯文本的通信协议,叫做 RESP(Redis Serialization Protocol)。

    RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。

    RESP 定义了五种类型的数据结构,每个最小单元之间用\r\n隔开。

    • 简单字符串 以 + 字符开头,后跟字符串本体。
    • 错误消息 以 - 字符开头,后跟错误消息本体。
    • 整数值 以 : 字符开头,后跟整数的字符串形式。
    • 复杂字符串(最多 512M 的) 以 $ 字符开头,后跟字符串长度;后面再跟字符串本体。
    • 数组 以 * 字符开头,后跟数组的长度;后面再跟数组元素。

    如简单字符串:

    "+OK\r\n"
    

    错误消息:

    "-Error message\r\n"
    

    整数:

    ":1000\r\n"
    ":0\r\n"
    

    复杂字符串:

    "$6\r\nfoobar\r\n"
    "$0\r\n\r\n"
    "$-1\r\n"        // 长度为-1 的是 null
    

    数组:

    "*0\r\n"
    "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
    "*3\r\n:1\r\n:2\r\n:3\r\n"
    "*-1\r\n"        // 长度为-1 的是 null。比如 blpop 超时时应该返回它,客户端就应该展示为 null。
    

    数组可以是混合类型,如数组的数组,把他们每行分隔开展示如下:

    *2\r\n
    *3\r\n
    :1\r\n
    :2\r\n
    :3\r\n
    *2\r\n
    +Foo\r\n
    -Bar\r\n
    

    状态机

    RESP 非常简单,协议的定义十分有限。所以要实现一个 redis server,我们可以按行读取客户端的请求数据,同时按行解析它们。

    我们可以按照客户端的行数据到达的不同状态画出一个状态机。

    从 S 状态(开始状态)开始,如果接受到的字符串时+-:,则是三种最简单的一行数据结构:简单字符串、错误消息、整数。那么进入 A 状态,接下来接收到任意值,就进入 END 状态,代表一条请求接收完毕。

    如果接收到的是$符号,那么进入 B 状态。接下来接收到-1则为 null,接收结束;接收到的是 0 或正整数,则进入 C 状态,此时还会接收到任意一行数据,就进入 END 状态代表结束。

    如果接收到*号,代表数组,这时情况稍微复杂一点。先进入 D 状态,此时接受到的是0-1的话都不会有一个新行,请求结束;此时接收到一个自然数值 n 的话,则代表数组长度为 n,则进入 En 状态,重新开始一个 S 状态的状态机,直到该状态机到 END 状态,n 可以自减 1 ;当 n 为 0 时,直接成为 END 状态。

    Python 实现

    RESP 的状态机实现部分略长,限于文章篇幅已经放在 GitHub:

    https://github.com/sljeff/python-redis-server/blob/master/resp.py

    redis server 的实现直接使用 Python 自带的 TCPServer:

    from socketserver import TCPServer, StreamRequestHandler
    from resp import handle_line
    
    
    class RedisHandler(StreamRequestHandler):
        def handle(self):
            state = None
            while True:
                data = self.rfile.readline().strip()
                print(data)
                state = handle_line(data, state)
                if state.is_stoped:
                    break
            self.wfile.write(b'+OK\r\n')
            print('end')
    
    
    if __name__ == '__main__':
        host, port = 'localhost', 6379
        with TCPServer((host, port), RedisHandler) as server:
            server.serve_forever()
    

    现在不管什么请求到达,server 都会响应+OK\r\n给客户端。我们的 server 端还会打印出请求的每行字符。

    运行效果

    直接敲入python redis_server.py运行代码;安装 redis-cli 作为客户端。

    当敲入redis-cli时,可以看到客户端发送了一个长度为 1 的数组过来,里面只有一个字符串为COMMAND

    我们在redis-cli敲入命令get a,客户端将geta作为长度为 2 的数组发送到 server。当然,我们的 server 现在还只能响应OK给客户端。


    这个系列可能还会继续更新下去……

    项目地址: https://github.com/sljeff/python-redis-server

    欢迎 star

    3 条回复    2018-09-11 06:02:58 +08:00
    hanxiV2EX
        1
    hanxiV2EX  
       2018-09-10 06:36:10 +08:00 via Android
    牛逼,可以先搞个 python-redis-client
    kindjeff
        2
    kindjeff  
    OP
       2018-09-10 09:24:11 +08:00
    @hanxiV2EX python 的 redis client 已经非常多了,想做个复杂一点的,顺便能多写几篇文章。
    yoyohaha
        3
    yoyohaha  
       2018-09-11 06:02:58 +08:00
    非常好,继续写,不要停。我准备照着你的思路用 C 写一个,因为我刚学完 c,正找东西练手。非常感谢!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2922 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 39ms · UTC 14:27 · PVG 22:27 · LAX 06:27 · JFK 09:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.