Go语言实现基于TCP的内存缓存服务

接上文: https://www.cnblogs.com/N3ptune/p/16623738.html

HTTP/REST的解析导致基于HTTP的内存缓存服务性能不佳,本次实现一个基于TCP的缓存服务

TCP缓存协议规范

对于TCP来说,客户端和服务端之间传输的是网络字节流,要自定义一套序列化规范,使用ABFN表达,ABNF是扩展巴科斯范式:

command = op key | key-value
op = 'S' | 'G' | 'D'
key = bytes-array
bytes-array = length SP content
length = 1*DIGIT
content = *OBJECT
key-value = length SP length SP content content
response = error | bytes-array
error = '-' bytes-array

第1行是一条command的规则,command是客户端发送给服务端的命令,由一个op和一个key或一个key-value组成

第2行是op的规则,op可以为下列3个字符的一个: "S"表示这是一个SET操作,"G"表示这是一个GET操作,而"D"表示这是一个DEL操作

第3行的key规则用来表示一个单独的键,它由一个字节数组bytes-array组成

第4行描述了bytes-array的规则,说明是由一个length、一个SP(空格字符)个一个content组成

length规则用来表示字节长度,是由一个或更多DIGIT组成(1*表示一个或多个)

content规则用来表示字节内容,由任意个OBJECT组成(*记法表示有0个或多个)

key-value规则用来表示一个键值对,由一个length、一个SP,然后又是一个length、一个SP以及content组成,分别表示key的字节长度、value的字节长度、key的字节内容和value的字节内容

response规则用来表示客户端发送给客户端的响应,由一个error或者一个bytes-array组成

error由一个"-"(负号)和一个bytes-array组成,表示错误

缓存流程

对于Set流程,客户端发送的command一个大写的"S"开始,后面接一个数字klen表示key的长度,然后是一个空格SP作为分割符,然后是一个数字vlen表示value的长度,然后又是一个空格,最后是key的内容和value的内容。服务端解析command并取出key和value,然后调用inMemoryCache.Set将键值对保存在内存的map中,如果cache.Cache.Set方法返回错误,tcp.Server会向客户端链接写入一个error: 以"-"开头,后面跟一个数字表示错误的长度,然后是一个空格作为分割符,最后是错误的内容。如果cache.Cache.Set方法成功返回,则tcp.Server向客户端写入一个"0",该字符会被解读为一个长度为0的bytes-array,用来表示成功

对于Get流程,客户端发送的command以一个大写的"G"开始,后面跟了一个数字klen表示key的长度,然后是一个空格作为分隔符,最后是key的内容。服务端解析command并取出key,然后调用inMemoryCache.Get方法在map中查询key对应的value,并将其作为byte-array写入客户端连接。如果cache.Cache.Get方法返回错误,tcp.Server会向客户端链接写入一个error

对于Del流程,客户端发送command以一个大写的"D"开始,后面跟了一个数字klen表示key的长度,然后是一个空格作为分割符,最后是key的内容。服务端解析这个command并取出key,然后调用inMemoryCache.Del方法删除该key,如果cache.Cache.Get方法返回错误,tcp.Server会向客户端连接写入一个error

下面编写代码

main函数如下:

package main

import (
	"tcp-cache/cache"
	"tcp-cache/http"
	"tcp-cache/tcp"
)

func main() {
	c := cache.New("inmemory")
	go tcp.New(c).Listen()
	http.New(c).Listen()
}

cache包和上篇文章一样,封装了对map的操作

服务端实现

创建一个tcp包,编写代码:

package tcp

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"net"
	"strconv"
	"strings"
	"tcp-cache/cache"
)

type Server struct {
	cache.Cache
}

func New(c cache.Cache) *Server {
	return &Server{c}
}

func (s *Server) Listen() {
	listen, err := net.Listen("tcp", ":9090")
	if err != nil {
		panic(err)
	}
	for {
		cli, err := listen.Accept()
		if err != nil {
			panic(err)
		}
		go s.process(cli)
	}
}

func (s *Server) readKey(r *bufio.Reader) (string, error) {
	klen, err := readLen(r)
	if err != nil {
		return "", err
	}
	k := make([]byte, klen)
	_, err = io.ReadFull(r, k)
	if err != nil {
		return "", err
	}
	return string(k), nil
}

func (s *Server) readKeyAndValue(r *bufio.Reader) (string, []byte, error) {
	klen, err := readLen(r)
	if err != nil {
		return "", nil, err
	}
	vlen, err := readLen(r)
	if err != nil {
		return "", nil, err
	}
	k := make([]byte, klen)
	_, err = io.ReadFull(r, k)
	if err != nil {
		return "", nil, err
	}
	v := make([]byte, vlen)
	_, err = io.ReadFull(r, v)
	if err != nil {
		return "", nil, err
	}
	return string(k), v, nil
}

func readLen(r *bufio.Reader) (int, error) {
	t, err := r.ReadString(' ')
	if err != nil {
		return 0, err
	}
	n, err := strconv.Atoi(strings.TrimSpace(t))
	if err != nil {
		return 0, err
	}
	return n, nil
}

func sendResponse(value []byte, err error, conn net.Conn) error {
	if err != nil {
		errString := err.Error()
		t := fmt.Sprintf("%-%d ", len(errString)) + errString
		_, e := conn.Write([]byte(t))
		return e
	}
	vlen := fmt.Sprintf("%d ", len(value))
	_, e := conn.Write(append([]byte(vlen), value...))
	return e
}

func (s *Server) get(conn net.Conn, r *bufio.Reader) error {
	k, err := s.readKey(r)
	if err != nil {
		return err
	}
	v, err := s.Get(k)
	return sendResponse(v, err, conn)
}

func (s *Server) set(conn net.Conn, r *bufio.Reader) error {
	k, v, err := s.readKeyAndValue(r)
	if err != nil {
		return err
	}
	return sendResponse(nil, s.Set(k, v), conn)
}

func (s *Server) del(conn net.Conn, r *bufio.Reader) error {
	k, err := s.readKey(r)
	if err != nil {
		return err
	}
	return sendResponse(nil, s.Del(k), conn)
}

func (s *Server) process(conn net.Conn) {
	defer conn.Close()
	r := bufio.NewReader(conn)
	for {
		op, err := r.ReadByte()
		if err != nil {
			if err != io.EOF {
				log.Println("close connection due to error:", err)
				return
			}
		}
		if op == 'S' {
			err = s.set(conn, r)
		} else if op == 'G' {
			err = s.get(conn, r)
		} else if op == 'D' {
			err = s.del(conn, r)
		} else {
			log.Println("close connection due to invalid operation:", op)
			return
		}
		if err != nil {
			log.Println("close connection due to error:", err)
			return
		}
	}
}

首先定义了一个Server结构体,内嵌一个cache.Cache接口,其Listen方法调用Go语言的net包的Listen函数监听本机TCP的9090端口,并在一个循环中接收客户端的连接并调用Server.process处理这个连接。处理连接的Server.process方法运行在一个新的goroutine上,原来的goroutine可以继续执行,监听新的请求

func (s *Server) Listen() {
	listen, err := net.Listen("tcp", ":9090")
	if err != nil {
		panic(err)
	}
	for {
		cli, err := listen.Accept()
		if err != nil {
			panic(err)
		}
		go s.process(cli)
	}
}

如下是process方法的实现:

func (s *Server) process(conn net.Conn) {
	defer conn.Close()
	r := bufio.NewReader(conn)
	for {
		op, err := r.ReadByte()
		if err != nil {
			if err != io.EOF {
				log.Println("close connection due to error:", err)
				return
			}
		}
		if op == 'S' {
			err = s.set(conn, r)
		} else if op == 'G' {
			err = s.get(conn, r)
		} else if op == 'D' {
			err = s.del(conn, r)
		} else {
			log.Println("close connection due to invalid operation:", op)
			return
		}
		if err != nil {
			log.Println("close connection due to error:", err)
			return
		}
	}
}

利用bufio.Reader结构体可以对客户端连接进行一个缓冲读取,可以在数据传输不稳定时进行阻塞等待,先读取请求内容的第一个字符,根据这个字符来判断来调用何种方法(set,get,del)

如下是3个方法的实现:

func (s *Server) get(conn net.Conn, r *bufio.Reader) error {
	k, err := s.readKey(r)
	if err != nil {
		return err
	}
	v, err := s.Get(k)
	return sendResponse(v, err, conn)
}

func (s *Server) set(conn net.Conn, r *bufio.Reader) error {
	k, v, err := s.readKeyAndValue(r)
	if err != nil {
		return err
	}
	return sendResponse(nil, s.Set(k, v), conn)
}

func (s *Server) del(conn net.Conn, r *bufio.Reader) error {
	k, err := s.readKey(r)
	if err != nil {
		return err
	}
	return sendResponse(nil, s.Del(k), conn)
}

get方法首先调用readKey函数,从command中读取key,readKey实现如下:

func (s *Server) readKey(r *bufio.Reader) (string, error) {
	klen, err := readLen(r)
	if err != nil {
		return "", err
	}
	k := make([]byte, klen)
	_, err = io.ReadFull(r, k)
	if err != nil {
		return "", err
	}
	return string(k), nil
}

该函数的作用就是从缓冲区字节数据中取出key,先调用readLen取出长度,在读取这个长度的字节,就得到了key,然后将其返回,readLen的实现如下:

func readLen(r *bufio.Reader) (int, error) {
	t, err := r.ReadString(' ')
	if err != nil {
		return 0, err
	}
	n, err := strconv.Atoi(strings.TrimSpace(t))
	if err != nil {
		return 0, err
	}
	return n, nil
}

缓冲区中字节数据是按空格分割的,第一段字符串就是长度,这在协议规范中已经表明(length SP content),之后将这个长度返回,之后在get中得到key后就可以调用cache的Get方法获得数据,调用sendReponse发送响应数据给客户端

Set方法与其相似,先调用readKeyAndValue方法获得键值,调用cache的Set方法添加数据,然后调用sendResponse发送给客户端,readKeyAndValue实现如下:

func (s *Server) readKeyAndValue(r *bufio.Reader) (string, []byte, error) {
	klen, err := readLen(r)
	if err != nil {
		return "", nil, err
	}
	vlen, err := readLen(r)
	if err != nil {
		return "", nil, err
	}
	k := make([]byte, klen)
	_, err = io.ReadFull(r, k)
	if err != nil {
		return "", nil, err
	}
	v := make([]byte, vlen)
	_, err = io.ReadFull(r, v)
	if err != nil {
		return "", nil, err
	}
	return string(k), v, nil
}

按照最初定义的协议规范,key-value的定义是length SP length SP content content这里依然用空格分割,前两段数据是key和value的长度

del与get一样,只要获取key,然后调用cache的Del方法进行删除就行了

最后是sendResponce的实现:

func sendResponse(value []byte, err error, conn net.Conn) error {
	if err != nil {
		errString := err.Error()
		t := fmt.Sprintf("-%d ", len(errString)) + errString
		_, e := conn.Write([]byte(t))
		return e
	}
	vlen := fmt.Sprintf("%d ", len(value))
	_, e := conn.Write(append([]byte(vlen), value...))
	return e
}

作用就是向客户端发送数据,发送取出的数据和错误信息,调用Write方法写回字节流

posted @ 2022-08-29 22:02  N3ptune  阅读(192)  评论(0编辑  收藏  举报