用Go實現Redis之二客戶端/服務端交互

寫在前面

在前一篇梳理了Godis v1.0版本的基本功能,這一篇要作的是實現客戶端/服務端的交互。先讓代碼跑起來,纔算有了生命力。
本篇Godis版本號:v0.0.1 git

在這個系列文章裏,儘可能減小介紹Golang語法、C語言語法和redis原理,聚焦在「用Golang實現Redis」的主題上。其中若有疏漏、不足,還請指正。github

進入正題

Redis事件處理器

既要實現C/S交互,網絡編程必不可少。在Redis中,有實現方式:redis

*Redis基於 Reactor 模式開發了本身的網絡事件處理器: 這個處理器被稱爲文件事件處理器(file event handler):編程

  • 文件事件處理器使用 I/O 多路複用(multiplexing)程序來同時監聽多個套接字, 並根據套接字目前執行的任務來爲套接字關聯不一樣的事件處理器。
  • 當被監聽的套接字準備好執行鏈接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操做時, 與操做相對應的文件事件就會產生, 這時文件事件處理器就會調用套接字以前關聯好的事件處理器來處理這些事件。*

Redis事件處理器的架構圖:圖片描述網絡

Redis兼顧了簡單性和高性能,但出於對其代碼複雜性的考慮,Godis v0.0.1會採用更簡單的弱智方案。Godis的編碼使用最簡單的同步技術,爭取在網絡處理方面簡單化,由於複雜的也不會:)。暫時捨棄高性能,只求可用。在將來的版本中,會結合Golang自身的特性,優化網絡層。數據結構

Godis的方案

客戶端/服務端的交互,只需使用Golang的net包進行socket編程,編寫一個流程爲監聽-讀取-處理-回覆的服務端和一個流程爲創建-發送-接收-顯示的客戶端便可。
圖片描述
圖中英文均爲Golang中net包的主要函數,這裏簡要說明一下在這一篇中使用的net包函數:
ResolveTCPAddr :
用例:tcpAddr, err := net.ResolveTCPAddr("tcp4", 「127.0.0.1:9736」);
功能:建立TCPAddr類型的數據結構,其中包含IP和Port,留做鏈接之用;架構

ListenTCP:
用例:netListen, err := net.Listen("tcp", "127.0.0.1:9763");
功能:監聽socket

Accept:
用例:conn, err := netListen.Accept()
功能:接收請求tcp

Read:
用例:n, err := conn.Read(buff)
功能:讀取數據函數

Write:
用例:conn.Write([]byte(buff))
功能:響應數據

net.DialTCP:
用例:conn, err := net.DialTCP("tcp", nil, tcpAddr)
功能:創建鏈接

代碼實現

按照前圖的流程,服務端的部分代碼以下所示:

func main() {
        netListen, err := net.Listen("tcp", "127.0.0.1:9736")
        if err != nil {
            log.Print("listen err ")
        }
        //checkError(err)
        defer netListen.Close()
    
        for {
            conn, err := netListen.Accept()
            if err != nil {
                continue
            }
            go handle(conn)
        }
    }

真實完整代碼實現請見這裏
在創建客戶端以前,咱們經過telnet測試下服務端是否可用(Redis也支持Telnet方式鏈接、請求,後面的協議部分會介紹)。
編譯godis-server.go
go build godis-server.go 並啓動 ./godis-server

執行 telnet localhost 9736

能夠看到回覆以下:
圖片描述

接下來,按照簡單流程圖的步驟,客戶端的實現以下:

func main() {
    IPPort := "127.0.0.1:9736"

    reader := bufio.NewReader(os.Stdin)
    fmt.Println("Hi Godis")
    tcpAddr, err := net.ResolveTCPAddr("tcp4", IPPort)
    checkError(err)

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(err)
    defer conn.Close()

    for {
        fmt.Print(IPPort + "> ")
        text, _ := reader.ReadString('\n')
        //清除掉回車換行符
        text = strings.Replace(text, "\n", "", -1)
        send2Server(text, conn)

        buff := make([]byte, 1024)
        n, err := conn.Read(buff)
        checkError(err)
        if n == 0 {
            fmt.Println(IPPort+"> ", "nil")
        } else {
            fmt.Println(IPPort+">", string(buff))
        }
    }
}

編譯cli.go 並啓動./cli
與服務端交互,以下圖所示:
圖片描述

到這裏只是實現了基本的客戶端/服務端通訊。接下來爲它添加少量交互選項,也就是在執行Redis命令行時的選項,讓它看起來更像是Redis。

clipboard.png

服務端添加平滑退出。

將來增長持久化後,會在平滑退出時持久化數據到磁盤,防止丟失。
平滑退出這裏使用的方式是讓客戶端監聽信號,有「退出」信號觸達,作完收尾工做再退出。
簡化代碼以下:

func sigHandler(c chan os.Signal) {
    for s := range c {
        switch s {
        case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
            exitHandler()
        default:
            fmt.Println("signal ", s)
        }
    }
}

func exitHandler() {
    fmt.Println("exiting smoothly ...")
    fmt.Println("bye ")
    os.Exit(0)
}

編譯以後,執行Ctrl+c退出,能夠看到:

clipboard.png

本篇遇到過的問題:

1.服務端讀取客戶端信息時,發生err時沒有return,致使服務端會一直讀取數據失敗。
解決方案:return或者其餘能夠退出goroutine的方案。
本篇就是這樣了。簡陋是簡陋了點,又不是不能用,李姐萬歲。完整代碼請看本篇對應的release,(只有release纔是當前本篇文章對應的完整代碼,當前repo狀態不是)。

下集預告

1.[實現get/set命令。][8]
相關文章
相關標籤/搜索