资讯专栏INFORMATION COLUMN

用Go实现Redis之三get/set命令实现

flyer_dev / 812人阅读

摘要:在读者阅读实现代码时,也可以看到最新版本,与有一处是在文件清除掉回车换行符该行被暂时注释掉,也就是在版本,使用作为文本协议分隔符,确定命令的结尾。

写在前面

本篇Godis版本号:v0.0.2

前一篇文章实现了客户端/服务端的交互。这一篇,主要介绍get/set命令的实现。
命令本身比较简单,支撑命令的整个系统基础比较麻烦。本文会介绍get/set操作涉及的组件和模块,并适当简化,最后实现功能。

Redis用C语言写成,C语言自身不支持复杂数据结构,所以Redis中的string、list、set等结构,均是Redis自身实现;而Go版本的Godis,会尽量使用原生数据结构。


原理简介

set命令和get命令是Redis中使用频率最高的命令,以set为例,命令“set key value”,将键值对存储到Redis服务端,可以简化为“操作一个远程关联数组”。
当然,相比关联数组,Redis多了如下特性:

多DB,支持数据库切换;

高可用之数据持久化;

高可用之主从复制;

安全、事务、发布订阅等。

本文重点实现数据在内存中的存储及查询,交互协议和持久化会在后续短文实现。

执行流程 流程拆解

从服务端初始化、客户端输入“set alpha 123”命令,到接收到返回结果,经历如下步骤:

实例化server及相关资源,准备连接;

客户端与服务端建立连接,服务端初始化一个client结构体,用来保存当前连接;

将客户端请求的“set alpha 123”字符串,分拆为“set”、“alpha”、“123”三部分;

查找是否支持set命令,并确定参数合法,调set用命令的实现函数SetCommand,更新db数据;

执行结果响应给客户端;

执行流程大致如下:


接下来分开说明主要步骤。

1.数据交互

前篇实现的客户端/服务端交互使用的协议是textproto,没有使用Redis自身的统一协议。这一篇,客户端对服务端执行的get、set命令,均以原生文本方式发送给服务端执行。在读者阅读实现代码时,也可以看到最新release版本,与v0.0.1有一处diff是在godis-cli.go文件:

//清除掉回车换行符
//text = strings.Replace(text, "
", "", -1)

该行被暂时注释掉,也就是在v0.0.2版本,使用“n”作为文本协议分隔符,确定命令的结尾。

如客户端发送"set alpha 123",服务端接收到的就是如下字节数据:

分别对应ASCII码为:

2.服务端准备

第一篇(https://segmentfault.com/a/11...)提到过服务端需要一个server结构体存储相关信息,在服务端准备好处理请求前,对该结构进行实例化并进行一系列初始化操作:初始化基本配置、分配多db资源、加载磁盘持久化数据、信号监听处理等。
初始化server的代码主要是一些赋值操作和相应结构体初始化:

// 初始化服务端实例
func initServer() {
    godis.Pid = os.Getpid()
    godis.DbNum = 16
    initDb()
    godis.Start = time.Now().UnixNano() / 1000000
    //var getf server.CmdFun

    getCommand := &core.GodisCommand{Name: "get", Proc: core.GetCommand}
    setCommand := &core.GodisCommand{Name: "set", Proc: core.SetCommand}

    godis.Commands = map[string]*core.GodisCommand{
        "get": getCommand,
        "set": setCommand,
    }
}

// 初始化db
func initDb() {
    godis.Db = make([]*core.GodisDb, godis.DbNum)
    for i := 0; i < godis.DbNum; i++ {
        godis.Db[i] = new(core.GodisDb)
        godis.Db[i].Dict = make(map[string]*core.GodisObject, 100)
    }
}

这里简单解释下core.GodisCommand结构。该结构很简单,记录了命令的名字、函数指针和参数校验相关的信息。在执行命令前,校验命令是否存在的过程,需要查找支持的”命令表“。该命令表就是commands,commands由一组core.GodisCommand构成。commands中查不到的命令,则为不支持的命令;而命令参数需要满足哪些条件,由core.GodisCommand结构的其他字段记录。

3.服务端接收

当服务端准备就绪,开始接受请求。
请求到来,server会实例化一个client结构体,保存当前连接。该client结构体也在前篇有介绍,主要用来存储当前连接的db等信息。

// CreateClient 连接建立 创建client记录当前连接
func (s *Server) CreateClient(conn net.Conn) (c *Client) {
    c = new(Client)
    c.Db = s.Db[0]
    c.Argv = make([]*GodisObject, 5)
    c.QueryBuf = ""
    return c
}
4.执行命令

将请求的命令分解,校验无误后,调用响应函数执行。注意,只在当前client结构指向的db中执行插入、查询、更新等操作。如果需要操作其他db,执行"select"命令便将当前client指向的db指针指向select后的位置。
执行完成后,将结果写入到client结构的Buf字段。

下面的handle函数包括了client的创建、数据接收、执行和返回。

// 处理请求
func handle(conn net.Conn) {
    c := godis.CreateClient(conn)
    for {
        err := c.ReadQueryFromClient(conn)

        if err != nil {
            log.Println("readQueryFromClient err", err)
            return
        }
        c.ProcessInputBuffer()
        godis.ProcessCommand(c)
        responseConn(conn, c)
    }
}
// ProcessCommand 执行命令
func (s *Server) ProcessCommand(c *Client) {
    v := c.Argv[0].Ptr
    name, ok := v.(string)
    if !ok {
        log.Println("error cmd")
        os.Exit(1)
    }
    cmd := lookupCommand(name, s)
    if cmd != nil {
        c.Cmd = cmd
        call(c, s)
    } else {
        addReply(c, CreateObject(ObjectTypeString, fmt.Sprintf("(error) ERR unknown command "%s"", name)))
    }
}

ProcessCommand函数先从命令表中查找命令,如果存在,调用该命令的实现,并将结果写入client.Buf字段。

5.响应请求

将client.Buf内容,返回给请求方,完成。
最后将执行结果返回给请求方。

// 响应返回给客户端
func responseConn(conn net.Conn, c *core.Client) {
    conn.Write([]byte(c.Buf))
}
测试

分别编译服务端和命令行客户端:
go build godis-server.go
go build godis-server.go

启动 ./godis-server

1.非法命令:


2.set/get命令:

服务端启动:

cli请求:

本篇问题

文本协议的格式分隔符没有处理好,在服务端又是使用的conn.Read(buff),读入的数据与buff额外的缓冲区混在一起。而print字符串又屏蔽了有效字符串后全零的问题(print调试最好输出原始数据)。

下集预告

1. 实现Redis统一协议

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/36656.html

相关文章

  • Go实现Redis之三get/set命令实现

    摘要:在读者阅读实现代码时,也可以看到最新版本,与有一处是在文件清除掉回车换行符该行被暂时注释掉,也就是在版本,使用作为文本协议分隔符,确定命令的结尾。 写在前面 本篇Godis版本号:v0.0.2 前一篇文章实现了客户端/服务端的交互。这一篇,主要介绍get/set命令的实现。命令本身比较简单,支撑命令的整个系统基础比较麻烦。本文会介绍get/set操作涉及的组件和模块,并适当简化,最后实...

    Ethan815 评论0 收藏0
  • Go实现Redis之四实现Redis的协议交互

    摘要:在本文,将替换文本协议为版本后的统一协议。协议格式在发送命令和返回结果中均使用同一套标准协议。实现通信协议版本协议实现初探很多相关的组件模块工具都有协议的生成和解析实现,并历经生产环境的考验。 写在前面 本文实现的Godis代码版本为:v0.0.3 在前三篇文章中,实现了客户端/服务端的交互(基于textprotoco)、服务端初始化和get/set命令。如果阅读过或者调试过粗略的代码...

    DandJ 评论0 收藏0
  • Go实现Redis之四实现Redis的协议交互

    摘要:在本文,将替换文本协议为版本后的统一协议。协议格式在发送命令和返回结果中均使用同一套标准协议。实现通信协议版本协议实现初探很多相关的组件模块工具都有协议的生成和解析实现,并历经生产环境的考验。 写在前面 本文实现的Godis代码版本为:v0.0.3 在前三篇文章中,实现了客户端/服务端的交互(基于textprotoco)、服务端初始化和get/set命令。如果阅读过或者调试过粗略的代码...

    legendmohe 评论0 收藏0
  • Go实现Redis之二客户端/服务端交互

    摘要:写在前面在前一篇梳理了版本的基本功能,这一篇要做的是实现客户端服务端的交互。进入正题事件处理器既要实现交互,网络编程必不可少。 写在前面 在前一篇梳理了Godis v1.0版本的基本功能,这一篇要做的是实现客户端/服务端的交互。先让代码跑起来,才算有了生命力。本篇Godis版本号:v0.0.1 在这个系列文章里,尽量减少介绍Golang语法、C语言语法和redis原理,聚焦在用Gol...

    qieangel2013 评论0 收藏0
  • Go实现Redis之二客户端/服务端交互

    摘要:写在前面在前一篇梳理了版本的基本功能,这一篇要做的是实现客户端服务端的交互。进入正题事件处理器既要实现交互,网络编程必不可少。 写在前面 在前一篇梳理了Godis v1.0版本的基本功能,这一篇要做的是实现客户端/服务端的交互。先让代码跑起来,才算有了生命力。本篇Godis版本号:v0.0.1 在这个系列文章里,尽量减少介绍Golang语法、C语言语法和redis原理,聚焦在用Gol...

    Scliang 评论0 收藏0

发表评论

0条评论

flyer_dev

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<