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

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

TCP缓存协议规范

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

  1. command = op key | key-value
  2. op = 'S' | 'G' | 'D'
  3. key = bytes-array
  4. bytes-array = length SP content
  5. length = 1*DIGIT
  6. content = *OBJECT
  7. key-value = length SP length SP content content
  8. response = error | bytes-array
  9. 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函数如下:

  1. package main
  2. import (
  3. "tcp-cache/cache"
  4. "tcp-cache/http"
  5. "tcp-cache/tcp"
  6. )
  7. func main() {
  8. c := cache.New("inmemory")
  9. go tcp.New(c).Listen()
  10. http.New(c).Listen()
  11. }

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

服务端实现

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

  1. package tcp
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "log"
  7. "net"
  8. "strconv"
  9. "strings"
  10. "tcp-cache/cache"
  11. )
  12. type Server struct {
  13. cache.Cache
  14. }
  15. func New(c cache.Cache) *Server {
  16. return &Server{c}
  17. }
  18. func (s *Server) Listen() {
  19. listen, err := net.Listen("tcp", ":9090")
  20. if err != nil {
  21. panic(err)
  22. }
  23. for {
  24. cli, err := listen.Accept()
  25. if err != nil {
  26. panic(err)
  27. }
  28. go s.process(cli)
  29. }
  30. }
  31. func (s *Server) readKey(r *bufio.Reader) (string, error) {
  32. klen, err := readLen(r)
  33. if err != nil {
  34. return "", err
  35. }
  36. k := make([]byte, klen)
  37. _, err = io.ReadFull(r, k)
  38. if err != nil {
  39. return "", err
  40. }
  41. return string(k), nil
  42. }
  43. func (s *Server) readKeyAndValue(r *bufio.Reader) (string, []byte, error) {
  44. klen, err := readLen(r)
  45. if err != nil {
  46. return "", nil, err
  47. }
  48. vlen, err := readLen(r)
  49. if err != nil {
  50. return "", nil, err
  51. }
  52. k := make([]byte, klen)
  53. _, err = io.ReadFull(r, k)
  54. if err != nil {
  55. return "", nil, err
  56. }
  57. v := make([]byte, vlen)
  58. _, err = io.ReadFull(r, v)
  59. if err != nil {
  60. return "", nil, err
  61. }
  62. return string(k), v, nil
  63. }
  64. func readLen(r *bufio.Reader) (int, error) {
  65. t, err := r.ReadString(' ')
  66. if err != nil {
  67. return 0, err
  68. }
  69. n, err := strconv.Atoi(strings.TrimSpace(t))
  70. if err != nil {
  71. return 0, err
  72. }
  73. return n, nil
  74. }
  75. func sendResponse(value []byte, err error, conn net.Conn) error {
  76. if err != nil {
  77. errString := err.Error()
  78. t := fmt.Sprintf("%-%d ", len(errString)) + errString
  79. _, e := conn.Write([]byte(t))
  80. return e
  81. }
  82. vlen := fmt.Sprintf("%d ", len(value))
  83. _, e := conn.Write(append([]byte(vlen), value...))
  84. return e
  85. }
  86. func (s *Server) get(conn net.Conn, r *bufio.Reader) error {
  87. k, err := s.readKey(r)
  88. if err != nil {
  89. return err
  90. }
  91. v, err := s.Get(k)
  92. return sendResponse(v, err, conn)
  93. }
  94. func (s *Server) set(conn net.Conn, r *bufio.Reader) error {
  95. k, v, err := s.readKeyAndValue(r)
  96. if err != nil {
  97. return err
  98. }
  99. return sendResponse(nil, s.Set(k, v), conn)
  100. }
  101. func (s *Server) del(conn net.Conn, r *bufio.Reader) error {
  102. k, err := s.readKey(r)
  103. if err != nil {
  104. return err
  105. }
  106. return sendResponse(nil, s.Del(k), conn)
  107. }
  108. func (s *Server) process(conn net.Conn) {
  109. defer conn.Close()
  110. r := bufio.NewReader(conn)
  111. for {
  112. op, err := r.ReadByte()
  113. if err != nil {
  114. if err != io.EOF {
  115. log.Println("close connection due to error:", err)
  116. return
  117. }
  118. }
  119. if op == 'S' {
  120. err = s.set(conn, r)
  121. } else if op == 'G' {
  122. err = s.get(conn, r)
  123. } else if op == 'D' {
  124. err = s.del(conn, r)
  125. } else {
  126. log.Println("close connection due to invalid operation:", op)
  127. return
  128. }
  129. if err != nil {
  130. log.Println("close connection due to error:", err)
  131. return
  132. }
  133. }
  134. }

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

  1. func (s *Server) Listen() {
  2. listen, err := net.Listen("tcp", ":9090")
  3. if err != nil {
  4. panic(err)
  5. }
  6. for {
  7. cli, err := listen.Accept()
  8. if err != nil {
  9. panic(err)
  10. }
  11. go s.process(cli)
  12. }
  13. }

如下是process方法的实现:

  1. func (s *Server) process(conn net.Conn) {
  2. defer conn.Close()
  3. r := bufio.NewReader(conn)
  4. for {
  5. op, err := r.ReadByte()
  6. if err != nil {
  7. if err != io.EOF {
  8. log.Println("close connection due to error:", err)
  9. return
  10. }
  11. }
  12. if op == 'S' {
  13. err = s.set(conn, r)
  14. } else if op == 'G' {
  15. err = s.get(conn, r)
  16. } else if op == 'D' {
  17. err = s.del(conn, r)
  18. } else {
  19. log.Println("close connection due to invalid operation:", op)
  20. return
  21. }
  22. if err != nil {
  23. log.Println("close connection due to error:", err)
  24. return
  25. }
  26. }
  27. }

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

如下是3个方法的实现:

  1. func (s *Server) get(conn net.Conn, r *bufio.Reader) error {
  2. k, err := s.readKey(r)
  3. if err != nil {
  4. return err
  5. }
  6. v, err := s.Get(k)
  7. return sendResponse(v, err, conn)
  8. }
  9. func (s *Server) set(conn net.Conn, r *bufio.Reader) error {
  10. k, v, err := s.readKeyAndValue(r)
  11. if err != nil {
  12. return err
  13. }
  14. return sendResponse(nil, s.Set(k, v), conn)
  15. }
  16. func (s *Server) del(conn net.Conn, r *bufio.Reader) error {
  17. k, err := s.readKey(r)
  18. if err != nil {
  19. return err
  20. }
  21. return sendResponse(nil, s.Del(k), conn)
  22. }

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

  1. func (s *Server) readKey(r *bufio.Reader) (string, error) {
  2. klen, err := readLen(r)
  3. if err != nil {
  4. return "", err
  5. }
  6. k := make([]byte, klen)
  7. _, err = io.ReadFull(r, k)
  8. if err != nil {
  9. return "", err
  10. }
  11. return string(k), nil
  12. }

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

  1. func readLen(r *bufio.Reader) (int, error) {
  2. t, err := r.ReadString(' ')
  3. if err != nil {
  4. return 0, err
  5. }
  6. n, err := strconv.Atoi(strings.TrimSpace(t))
  7. if err != nil {
  8. return 0, err
  9. }
  10. return n, nil
  11. }

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

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

  1. func (s *Server) readKeyAndValue(r *bufio.Reader) (string, []byte, error) {
  2. klen, err := readLen(r)
  3. if err != nil {
  4. return "", nil, err
  5. }
  6. vlen, err := readLen(r)
  7. if err != nil {
  8. return "", nil, err
  9. }
  10. k := make([]byte, klen)
  11. _, err = io.ReadFull(r, k)
  12. if err != nil {
  13. return "", nil, err
  14. }
  15. v := make([]byte, vlen)
  16. _, err = io.ReadFull(r, v)
  17. if err != nil {
  18. return "", nil, err
  19. }
  20. return string(k), v, nil
  21. }

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

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

最后是sendResponce的实现:

  1. func sendResponse(value []byte, err error, conn net.Conn) error {
  2. if err != nil {
  3. errString := err.Error()
  4. t := fmt.Sprintf("-%d ", len(errString)) + errString
  5. _, e := conn.Write([]byte(t))
  6. return e
  7. }
  8. vlen := fmt.Sprintf("%d ", len(value))
  9. _, e := conn.Write(append([]byte(vlen), value...))
  10. return e
  11. }

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

Go语言实现基于TCP的内存缓存服务的更多相关文章

  1. Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!

    Go/Python/Erlang编程语言对比分析及示例   本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...

  2. 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!

    using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; usi ...

  3. IIS 中托管基于TCP绑定的WCF服务

    IIS 中托管基于TCP绑定的WCF服务 一.创建一个基于TCP绑定的WCF服务 1.创建一个的简单的服务具体代码如下 服务契约定义 namespace SimpleService { // 注意: ...

  4. 标准C语言实现基于TCP/IP协议的文件传输

    TCP/IP编程实现远程文件传输在LUNIX中一般都采用套接字(socket)系统调用. 采用客户/服务器模式,其程序编写步骤如下:  1.Socket系统调用  为了进行网络I/O,服务器和客户机两 ...

  5. 实验09——java基于TCP实现客户端与服务端通信

    TCP通信         需要先创建连接 - 并且在创建连接的过程中 需要经过三次握手        底层通过 流 发送数据 数据没有大小限制        可靠的传输机制 - 丢包重发 包的顺序的 ...

  6. 基于Tcp穿越的Windows远程桌面(远程桌面管理工具)

    基于Tcp穿越的Windows远程桌面(远程桌面管理工具) 1.<C# WinForm 跨线程访问控件(实用简洁写法)>            2.<基于.NET环境,C#语言 实现 ...

  7. 基于.net的分布式系统限流组件 C# DataGridView绑定List对象时,利用BindingList来实现增删查改 .net中ThreadPool与Task的认识总结 C# 排序技术研究与对比 基于.net的通用内存缓存模型组件 Scala学习笔记:重要语法特性

    基于.net的分布式系统限流组件   在互联网应用中,流量洪峰是常有的事情.在应对流量洪峰时,通用的处理模式一般有排队.限流,这样可以非常直接有效的保护系统,防止系统被打爆.另外,通过限流技术手段,可 ...

  8. 基于STSdb和fastJson的磁盘/内存缓存

    更新 1. 增加了对批量处理的支持,写操作速度提升5倍,读操作提升100倍 2. 增加了对并发的支持 需求 业务系统用的是数据库,数据量大,部分只读或相对稳定业务查询复杂,每次页面加载都要花耗不少时间 ...

  9. 一个基于STSdb和fastJson的磁盘/内存缓存

    一个基于STSdb和fastJson的磁盘/内存缓存 需求 业务系统用的是数据库,数据量大,部分只读或相对稳定业务查询复杂,每次页面加载都要花耗不少时间(不讨论异步),觉得可以做一下高速缓存,譬如用n ...

  10. 基于.net的通用内存缓存模型组件

    谈到缓存,我们自然而然就会想到缓存的好处,比如: 降低高并发数据读取的系统压力:静态数据访问.动态数据访问 存储预处理数据,提升系统响应速度和TPS 降低高并发数据写入的系统压力 提升系统可用性,后台 ...

随机推荐

  1. 了解JAVA基本知识以及一下常用的dos命令

    9月5日学习 常用的Dos命令 #盘符切换盘符名称: =>回车​#查看当前目录下的所有文件dir​#切换目录 cd change directorycd .. =>返回上一级目录​#清理屏 ...

  2. 正确理解RestFul 接口

    一.REST# REST,即Representational State Transfer的缩写,翻译过来就是"表现层状态转化".不得不承认,我在刚开始看到这个名词的时候是一脸懵逼 ...

  3. 为什么reids是单线程

    我们首先要明白,reids很快,官方表示,因为reids是基于内存的操作,cpu不是reids的瓶颈,redis的瓶颈有可能是机器内存的大小或者网络带宽,既然单线程容易控制,而且cpu不会成为瓶颈,所 ...

  4. shortcuts

    关闭选项卡 Ctrl+W 关闭当前窗口 alt + F4 alt + 空格 + c alt + 空格 + n 最小化窗口 alt + 空格 + x 最大化窗口 ALT+F4 关闭当前应用程序 ctrl ...

  5. Mysql_5.7编译部署

    自述 - 概述:数据库是"按照数据结构来组织.存储和管理数据的仓库".是一个长期存储在计算机内的.有组织的.可共享的.统一管理的大量数据的集合:本文主要介绍mysql_5.7的部署 ...

  6. pip安装报错 cannot uninstall a distutils installed project

    sudo pip install --ignore-installed xxx 在安装jupyter notebook的时候,遇到了这个问题,于是上网搜索,搜到了靠谱答案github解决方案 sudo ...

  7. windows下查找端口、PID、查找进程、杀死进程

    查找端口 netstat -ano|findstr "1099" TCP 0.0.0.0:1099 0.0.0.0:0 LISTENING 10120 TCP [::]:1099 ...

  8. js 小数和百分数的转换

    百分数转化为小数 function toPoint(percent){ var str=percent.replace("%",""); str= str/10 ...

  9. swiper常见问题、动态加载数据问题

    swiper加载静态文件是没有问题的 swiper加载动态文件需要在请求后再加载这个函数 参考链接:  https://blog.csdn.net/webzrh/article/details/781 ...

  10. 读取远程服务器linux指定目录下文本内容(工具类)

    package com.aa.dataadmin.common.utils; import cn.hutool.extra.ssh.JschUtil; import com.jcraft.jsch.C ...