使用 Go 语言实现模拟 redis 协议的服务(一)Go 语言实现 TCP 服务

模拟 Redis 协议首先是要实现一个 TCP 协议的服务,再按照 redis 的参数组织方式(RESP协议)解析和返回。其中 TCP 服务的实现,主要依赖 net 包中的 ListenrConn 两个接口 。

一、GoLang net 包基础

1、net.Listener 流式协议监听接口
1
2
3
4
5
6
// 原始方法:package net
// (1)创建监听
func Listen(network, address string) (Listener, error)

// (2)接收连接 Accept waits for and returns the next connection to the listener.
func Accept() (Conn, error)

参数 network 取值范围:

1
2
3
4
5
tcp
tcp4 // ipv4-only for tcp
tcp6 // ipv6-only for tcp
unix
unixpacket

其中,network 传值为 tcp/tcp4/tcp6 时,Listener 接口 可以替换为 TCPListener ,而 network 值为 unix/unixpacket 时,可以替换为 UnixListener 接口。

Listener 接口 network 参数不支持 udp,因为 UDP 网络连接不需要建立连接,也就是没有 Accept() 步骤。可以直接用 net.ListenUDPlistenter.ReadFrom(buf) 方法来处理。

demo:创建tcp协议的接口监听

1
2
3
4
5
6
7
// 创建端口6480的tcp接口监听
listener, err := net.Listen("tcp", "0.0.0.0:6480")
defer listener.Close()

// 接收一条连接信息
conn, err := listener.Accept()
// conn 即为 net.Conn 对象
2、net.Conn 数据流为向导的网络连接接口

根据连接类型的不同, Conn 也分围 TCPConn / UDPConn / UnixConn / IpConn 四种,它们都是内部内嵌了一个 Conn 结构体,包含了 socket 的文件描述符。

1
2
3
4
5
6
// 原始方法
// (1) 读取连接数据
func (c *Conn) Read(b []byte) (int, error)

// (2) 向连接中写入数据/返回数据给客户端
func (c *Conn) Write(b []byte) (int, error)

demo: 获取连接数据

1
2
3
4
5
6
7
8
9
10
11
12
// 接收一条连接信息
conn, err := listener.Accept()

buf := make([]byte, 256)
_, err = conn.Read(buf) // 获取内容
if err != nil {
// err == io.EOF 表示客户端关闭连接
return
}

// 回复消息
conn.Write([]byte("return message"))

二、阻塞io模型

阻塞IO模型是最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

net.Listener 中的 Accept() 方法则是非常典型的阻塞IO案例。源码中 Accept() 方法的注释如下:

1
Accept waits for and returns the next connection to the listener.

因为 Golang 拥有内存占用和调度开销都很小的 goroutine,我们可以为每个连接分配一个协程,在降低开发难度同时获得不错的性能。

三、TCP 服务的简单代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
"bufio"
"fmt"
"io"
"log"
"net"
)

type Server struct { // 为了后续方便扩展,在 listener 上层定义一个结构体
listener net.Listener
}

func newServer(port int) (*Server, error) {
s := new(Server)
var err error

addr := fmt.Sprintf("0.0.0.0:%d", port)
s.listener, err = net.Listen("tcp", addr)
if err != nil {
return nil, err
}

return s, nil
}

func (s *Server) onConn(conn net.Conn) {
reader := bufio.NewReader(conn)
for {
// ReadString 会一直阻塞直到遇到分隔符 '\n'
// 若在遇到分隔符之前遇到异常, 程序会返回收到的数据或者错误信息
msg, err := reader.ReadString('\n')
if err != nil {
// 通常遇到的错误是连接中断或被关闭(io.EOF)
if err == io.EOF {
// 客户端关闭连接
log.Println("connection close")
} else {
log.Println(err)
}
return
}
log.Println(fmt.Sprintf("got one msg: %s", msg))
b := []byte(fmt.Sprintf("Your msg is: %s", msg))
// 将信息发送给客户端
conn.Write(b)
}
}

func main() {
s, err := newServer(6480)
if err != nil {
panic(err)
}

defer s.listener.Close()
log.Println("server started: *:6480")
for {
conn, err := s.listener.Accept()
if err != nil {
panic(err)
}
go s.onConn(conn)
}
}

终端启动服务:

image

client telnet连接测试:

image


【参考文档】

1、golang net包基础解析


项目代码:https://github.com/silov/redis-protocol-cook