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方法写回字节流
Go语言实现基于TCP的内存缓存服务的更多相关文章
- Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!
Go/Python/Erlang编程语言对比分析及示例 本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...
- 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; usi ...
- IIS 中托管基于TCP绑定的WCF服务
IIS 中托管基于TCP绑定的WCF服务 一.创建一个基于TCP绑定的WCF服务 1.创建一个的简单的服务具体代码如下 服务契约定义 namespace SimpleService { // 注意: ...
- 标准C语言实现基于TCP/IP协议的文件传输
TCP/IP编程实现远程文件传输在LUNIX中一般都采用套接字(socket)系统调用. 采用客户/服务器模式,其程序编写步骤如下: 1.Socket系统调用 为了进行网络I/O,服务器和客户机两 ...
- 实验09——java基于TCP实现客户端与服务端通信
TCP通信 需要先创建连接 - 并且在创建连接的过程中 需要经过三次握手 底层通过 流 发送数据 数据没有大小限制 可靠的传输机制 - 丢包重发 包的顺序的 ...
- 基于Tcp穿越的Windows远程桌面(远程桌面管理工具)
基于Tcp穿越的Windows远程桌面(远程桌面管理工具) 1.<C# WinForm 跨线程访问控件(实用简洁写法)> 2.<基于.NET环境,C#语言 实现 ...
- 基于.net的分布式系统限流组件 C# DataGridView绑定List对象时,利用BindingList来实现增删查改 .net中ThreadPool与Task的认识总结 C# 排序技术研究与对比 基于.net的通用内存缓存模型组件 Scala学习笔记:重要语法特性
基于.net的分布式系统限流组件 在互联网应用中,流量洪峰是常有的事情.在应对流量洪峰时,通用的处理模式一般有排队.限流,这样可以非常直接有效的保护系统,防止系统被打爆.另外,通过限流技术手段,可 ...
- 基于STSdb和fastJson的磁盘/内存缓存
更新 1. 增加了对批量处理的支持,写操作速度提升5倍,读操作提升100倍 2. 增加了对并发的支持 需求 业务系统用的是数据库,数据量大,部分只读或相对稳定业务查询复杂,每次页面加载都要花耗不少时间 ...
- 一个基于STSdb和fastJson的磁盘/内存缓存
一个基于STSdb和fastJson的磁盘/内存缓存 需求 业务系统用的是数据库,数据量大,部分只读或相对稳定业务查询复杂,每次页面加载都要花耗不少时间(不讨论异步),觉得可以做一下高速缓存,譬如用n ...
- 基于.net的通用内存缓存模型组件
谈到缓存,我们自然而然就会想到缓存的好处,比如: 降低高并发数据读取的系统压力:静态数据访问.动态数据访问 存储预处理数据,提升系统响应速度和TPS 降低高并发数据写入的系统压力 提升系统可用性,后台 ...
随机推荐
- []Python][simple]Serialize data with Pickle and deserialize data from pickle
序列化 import pickle friend = {"Dan": [20, "Lodon", 123123], "Mary" : [24 ...
- defer语句
1.defer语句 延时机制,在函数中,经常需要创建资源(比如:数据库连接.文件句柄.锁等),为了在函数执行完毕后,及时释放资源,使用defer a. 当执行defer时,暂时不执行,会将defer后 ...
- java中String为什么设计成不可变对象
设计成不可变对象为了安全和高效 1.安全方面:举例说明:在文件操作或反射操作的时候需要传递字符串,如果是设计成可变的那么就会造成安全问题 2.高效:举例说明 字符串源码中会将hashcode缓冲下来, ...
- mybatis-plus自动填充踩坑
学习使用mybatis-plus的自动填充功能,对create_time和update_time做一个自动填充,期间碰到了一些问题,记录一下问题和相关代码 在实体类字段上增加注解@TableField ...
- 集群的概念、分类及LVS三种模式总结
1.集群概念: 集群是一组协同工作的服务器,各有分工,对外表现为一个整体. 2.集群分类: 负载均衡集群LBC:分担服务的总体压力 高可用集群HAC:尽可能的保障服务状态的可用性 高性能运算集群HPC ...
- Python内置函数:enumerate
enumerate(sequence, [start=0]) enumerate单词本身翻译为列举.枚举. 官方说明: enumerate() 函数用于将一个可遍历的数据对象(如列表.元组或字符串)组 ...
- 利用MVC三层架构做一个案例(利用MyBatis实现增删改查)
查询所有 利用昨天学习到的MyBatis知识,再加上servlet页面的跳转, demo1.jsp UserMapper.java(接口) servletDemo.java MyBatisDemo.j ...
- 在golang中如何正确判断接口是否为nil
本文主要来分析一下在golang中,如何判断interface是否为nil,以及相关注意事项. 正常情况下,我们声明一个interface类型的变量,默认值将会返回nil,以golang自带的io.W ...
- 如何基于 Agora Android SDK 在应用中实现视频通话?
在很多产品,实时视频通话已经不是新鲜的功能了,例如视频会议.社交应用.在线教育,甚至也可能出现在一些元宇宙的场景中. 本文将教你如何通过声网Agora 视频 SDK 在 Android 端实现一个视频 ...
- GitHub+Typora实现云笔记一键上传
git实现笔记自动上传功能 简介: 将更新内容自动上传同步git,无需手动提交,解锁一键式同步.流程大致为,创建新仓库,配置公钥和私钥,安装quicker软件,通过quicker上某脚本完成一键提交. ...