使用 Go 语言实现模拟 redis 协议的服务(一)Go 语言实现 TCP 服务
模拟 Redis 协议首先是要实现一个 TCP
协议的服务,再按照 redis
的参数组织方式(RESP协议)解析和返回。其中 TCP 服务的实现,主要依赖 net
包中的 Listenr
和 Conn
两个接口 。
一、GoLang net 包基础
1、net.Listener 流式协议监听接口
1 2 3 4 5 6
|
func Listen(network, address string) (Listener, error)
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.ListenUDP
和 listenter.ReadFrom(buf)
方法来处理。
demo:创建tcp协议的接口监听
1 2 3 4 5 6 7
| listener, err := net.Listen("tcp", "0.0.0.0:6480") defer listener.Close()
conn, err := listener.Accept()
|
2、net.Conn 数据流为向导的网络连接接口
根据连接类型的不同, Conn
也分围 TCPConn / UDPConn / UnixConn / IpConn
四种,它们都是内部内嵌了一个 Conn
结构体,包含了 socket
的文件描述符。
1 2 3 4 5 6
|
func (c *Conn) Read(b []byte) (int, error)
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 { 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 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 { msg, err := reader.ReadString('\n') if err != nil { 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) } }
|
终端启动服务:

client telnet连接测试:

【参考文档】
1、golang net包基础解析
项目代码:https://github.com/silov/redis-protocol-cook