寫在前面
在前一篇梳理了Godis v1.0版本的準系統,這一篇要做的是實現用戶端/服務端的互動。先讓代碼跑起來,才算有了生命力。
本篇Godis版本號碼:v0.0.1
在這個系列文章裡,盡量減少介紹Golang文法、C語言文法和redis原理,聚焦在“用Golang實現Redis”的主題上。其中如有疏漏、不足,還請指正。
進入正題
Redis事件處理器
既要實現C/S互動,網路編程必不可少。在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");
功能:監聽
Accept:
用例:conn, err := netListen.Accept()
功能:接收請求
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。
服務端添加平滑退出。
未來增加持久化後,會在平滑退出時持久化資料到磁碟,防止丟失。
平滑退出這裡使用的方式是讓用戶端監聽訊號,有“退出”訊號觸達,做完收尾工作再退出。
簡化代碼如下:
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](/img/bVbb2yA)本篇遇到過的問題:---------1.服務端讀取用戶端資訊時,發生err時沒有return,導致服務端會一直讀取資料失敗。解決方案:return或者其他可以退出goroutine的方案。本篇就是這樣了。簡陋是簡陋了點,又不是不能用,李姐萬歲。完整代碼請看本篇對應的[release][7],(只有release才是當前本篇文章對應的完整代碼,當前repo狀態不是)。下集預告---- 實現get/set命令。 [1]: https://github.com/alphali/godis/releases/tag/0.0.1 [2]: /img/bVbb1C3 [3]: /img/bVbb1FT [4]: https://github.com/alphali/godis/releases/tag/0.0.1 [5]: /img/bVbb2mA [6]: /img/bVbb2nw