Go的net/http 包实现了http 编程,但是会话即session 还需应用自己实现。本文基于内存存储实现一个简单的session 管理,并解释一些语言重要的基础概念(如传值传址),基于Go 1.9.2。
分析
简单来说session 管理的难点在于过期的session 要能自动销毁以回收内存避免内存泄露,但是该销毁过程尽量不要影响其他非过期session 的正常使用。
通常所有session 存放在一个map,以session id 为key 这样可以快速由id 获取session。但是过期的session 要能及时清除,所以考虑在map 之外增加一个linked list 将所有session 按活跃程度链接起来:每次获取一个session 时将它提升到链头,这样链尾就逐步堆积了所有不活跃的session,过期清理从链尾可以快速执行。
如此将map value 改为list 的元素。使用Go container/list 双向列表:已知该列表的某个元素,可以快速将其移动而不需遍历整个列表。
设计
目录结构如下:
$GOPATH/src/sample/memses
main.go
$GOPATH/src/sample/memses/session
session.go
main.go 是主程序,session 目录是session 实现。
首先定义session类型:
type ISession interface {
Id() string
Get(key string) interface{}
Set(key string, value interface{})
Remove(key string)
Invalidate()
}
Go 类似于Java 也可使用接口定义类型。session 有get/set/删除,可以主动过期。session id 一般固定不会修改。interface{} 类似于Java 的Object,但也可以是int/float32等。
session 管理器:
type ISessionManager interface {
Get(id string) ISession // get or create a session (with new id)
}
session 管理器用来获取一个session,传入session id。如果有就返回它,如果没有或者已经过期了,则新建一个再返回。
实现
一个session 所有需要的数据我们定义在一个struct 上:
type ses struct {
mgr *sesmgr
id string
lock *sync.RWMutex // for smap,time
smap map[string]interface{}
time time.Time //access time
}
session 的数据存储在smap 里。由于session 可能被并发使用(例如一个页面同时发起2个后台请求),所以通过sync RW锁来做访问同步。注意Go 语言通常建议通过channel/goroutine 来使得一个数据只会通过一个goroutine 来访问以规避多个goroutine 并发访问同一数据,但是该建议通常用于程序流程级的控制,当细化到对于一个map 的读写做并发控制时,简单的RW 锁看起来更合适。
lock 字段的类型是*sync.RWMutex 而非sync.RWMutex,区别何在?参见godoc faq#pass_by_value:Go 函数调用时所有参数、结果传递都是传值(by value)而非传址(by reference)。这样当调用ses.lock.XX方法时传递的是 *sync.RWMutex 的拷贝 - 指针拷贝不影响仍使用原始的lock 对象,而如果是 sync.RWMutex拷贝 则已经不是原始的lock 对象。RWMutex godoc 里要求“An RWMutex must not be copied after first use.”。
同样见faq#references,Go map|slice|channel 实际是指针:即一个map对象(非map指针)其内部实际存储的是指向实际数据的指针,所以map|slice|channel 传值也是可以的。但是注意Go array 是实际的值对象,当要传递一个较大的array 时最好改为传递其指针。
另外,小的struct 因为拷贝很'廉价' 所以仍可以传值。最后最好统一:同一对象的所有方法要么都传值要么都传址。
关于session 访问时间ses.time 我们实现上将简化为仅在通过manager 获取session 时更新它,后续session.get/set/.. 时不再更新。
ses.mgr 字段用于实现session.Invalidate 方法。
在Go 里一个类型如果实现了某个接口的所有方法则该类型也就实现了该接口,不需要类似Java里明确的'implements'。ses struct 实现了ISession 接口:
func (s ses) Id() string {
return s.id
}
func (s ses) Get(key string) interface{} {
s.lock.Lock()
defer s.lock.Unlock()
return s.smap[key]
}
func (s ses) Set(key string, value interface{}) {
s.lock.Lock()
defer s.lock.Unlock()
s.smap[key] = value
}
func (s ses) Remove(key string) {
。。。
}
func (s ses) Invalidate() {
s.mgr.invalidate(s.id)
}
如上Get方法里由于要防止并发所以先Lock。defer Unlock 将在方法返回前(返回值已经得到)执行,这类似于Java 的try/finally。
然后是session manager 的实现struct:
type sesmgr struct {
lock *sync.RWMutex
list *list.List // a list of ses Element, active (top) -> inactive (bottom)
smap map[string]*list.Element // id => ses Element
timeout time.Duration
}
在map 之外使用双向链表:map value 是链表的元素,链表元素的Value 是ses struct。已知id 从map 找到list element 其value 就是ses (ISession)。同样list element 可以快速在链表内移动及删除。同样用RW 锁控制并发。
ISessionManager 的Get 方法实现:
func (sm sesmgr) Get(id string) ISession {
sm.lock.Lock()
defer sm.lock.Unlock()
if e, ok := sm.smap[id]; ok {
s := e.Value.(ses)
if s.checkTimeout(sm.timeout) {
sm.list.MoveToFront(e) // front means most active
return s
} else {
sm.delete(e)
}
}
// not exists or timed out
s := ses{
mgr: &sm,
id: genSesId(),
lock: new(sync.RWMutex),
smap: make(map[string]interface{}, 24),
time: time.Now(),
}
e := sm.list.PushFront(s)
sm.smap[s.id] = e
return s
}
如果从smap 里找到并且尚未过期,则移动到链表头部后返回,否则删除过期的、新建一个后返回。
List.Element.Value 的类型是interface{}。“e.Value.(ses)”是一个type assertion “x.(T)”:x 需要是接口类型,当T 不是接口类型时x 的动态类型应该 = T。
new(xx)返回指针,make(xx)返回值。
清理时从链表的尾部开始。为避免耗时过长一次清理设置数量上限,超过而链表非空时下次清理将提前,但链表已经清空或剩余的未过期时,则要避免频繁启动下次清理:
func (sm sesmgr) gcOnce() time.Duration {
sm.lock.Lock()
defer sm.lock.Unlock()
for i := 0; i < 1000; i++ { // max 1000 del
e := sm.list.Back()
if e == nil {
break
}
s := e.Value.(ses)
if d := s.getLeftTimeout(sm.timeout); d >= 0 {
sm.delete(e)
} else {
if -d < 2*time.Minute { // still valid, wait a bit longer
return 2 * time.Minute
} else {
return -d
}
}
}
if sm.list.Len() > 0 { // assume more to gc, catch up
return 1 * time.Second
} else {
return 2 * time.Minute
}
}
这里的返回值=到下次清理的等待时间,在创建session manager 时启动一个goroutine 来持续清理:
go func() {
for {
time.Sleep(sm.gcOnce())
}
}()
go funcxx 就启动一个goroutine。goroutine 是更轻量级的线程,可以想象成一根Java线程可以调度多个goroutine。见faq#goroutines,一个goroutine 初始仅占几k 内存,一个程序可以使用成百上千goroutine。
单元测试
Go 提供了方便的单元测试编写,在session.go 相同目录创建session_test.go,里面的每个“func TestXX(*testing.T)”方法即为测试方法,通过go test 运行测试。
Go 里同一个包下的类型互相之间都可见,不存在private。由于test 类和session.go 在一个包里,这方便我们编写一个创建session manager 但不启动自动清理的工具方法:
func createMgr(d time.Duration) sesmgr {
sm := sesmgr{
lock: new(sync.RWMutex),
list: new(list.List),
smap: make(map[string]*list.Element, 100),
timeout: d,
}
return sm
}
简单的测试:
func Test1(t *testing.T) {
sm := createMgr(time.Minute)
//1. one
s := sm.Get("")
if sm.list.Len() != 1 {
t.Errorf("one: len != 1")
}
//。。。
}
“t.Errorf”方法报告一次失败。
通过调用gcOnce方法来测试清理:
sm = createMgr(10 * time.Second) //10s timeout
id1 = sm.Get("").Id()
sm.gcOnce()
if sm.list.Len() != 1 {
t.Errorf("gc: should gc none")
}
浏览器测试
需要在浏览器里实际验证一下会话:main.go 使用net/http 启动一个http server,接收到浏览器请求时(类似Java servlet)通过一个固定名称的cookie 存储产生的session 的id。该cookie 会发送到浏览器端,浏览器后续每次请求时会再发送给server:
const TOKEN = "GSESSIONID" // session cookie name
func getSession(w http.ResponseWriter, req *http.Request) session.ISession {
var id = ""
if c, err := req.Cookie(TOKEN); err == nil {
id = c.Value
}
ses := sesmgr.Get(id)
if ses.Id() != id { //new session
http.SetCookie(w, &http.Cookie{
Name: TOKEN,
Value: ses.Id(),
})
}
return ses
}
注意以上getSession 方法仅用于测试,用于生产可能还有一些潜在问题(例如域名换ip)。
运行程序,访问展示页面确认每次刷新的session id 相同,另外还可以set 值、销毁当前session。
分享到:
相关推荐
在Go中,session管理通常依赖于第三方库,如`go-sessions`,它是专门为Go语言设计的一个高效且灵活的session管理库,可以很好地与Macaron框架集成。 首先,我们需要安装`go-sessions`库。通过运行以下命令,你可以...
Go语言,也称为Golang,是由Google开发的一种静态类型的、编译型的、并发型的、垃圾回收的编程语言,因其高效性能和简洁的语法而受到广大开发者喜爱。在后端开发领域,Go语言以其强大的并发处理能力而独树一帜,这...
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。...如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
【开源项目-lcbluestorm-ssdb-session.zip】是一个基于Go语言编写的开源项目,主要目的是为Beego框架提供一种高效、可靠的session管理解决方案。Beego是一个流行的Go语言Web应用框架,它提供了丰富的功能来简化Web...
Tomcat是Apache软件基金会的一个开源项目,它是一个轻量级的Java Servlet容器,支持Servlet和JSP标准。Tomcat 8是其最新版本,提供了一些新特性,如更好的性能、改进的WebSocket支持和增强的安全性。 接下来是Redis...
在Go语言中,Gin框架是一个非常流行的用于构建Web服务的库,它提供了简洁的API设计,使得开发者能够快速地开发出高性能的应用程序。本文将深入探讨如何在Gin框架中处理Cookie和Session,这对于构建用户认证和会话...
在本项目中,我们将深入探讨如何使用Golang(Go语言)来开发一个个人博客系统。Golang作为一种现代、高效且简洁的编程语言,因其强大的并发处理能力、内存管理以及易于学习的特点,近年来在Web开发领域得到了广泛...
一款集漏洞探测、攻击,Session会话,蜜罐识别等功能于一身的软件,基于go-micro微服务框架并...如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
总结,Go-Gosession是Go语言中用于Web session管理的一个强大工具,它提供了高性能、兼容性和灵活性,使得开发者能够轻松地在不同的Web框架下实现用户会话管理。正确理解和使用session管理对于构建健壮的Web应用至关...
Go-stun项目是Go语言实现的STUN客户端,遵循了RFC3489和RFC5389规范。 RFC3489是STUN的最初版本,发布于2003年,定义了STUN的基本操作,包括绑定请求、绑定响应和错误响应等消息格式。这个版本的STUN主要用于帮助...
- 定义了一个简单的 JavaBean 类,包含 `id`, `title`, 和 `content` 属性及其 getter/setter 方法。 ##### 5.2 JavaBean 在 JSP 中的使用 **beantest.jsp:** ```jsp ;charset=gb2312" %> <title>...
总的来说,Django的session机制在实现唯一登录时,通过在用户登录时创建session并保存用户信息,然后在检测到新登录时清理旧的session,确保了用户在同一时间只能在一个设备上保持登录状态。同时,结合cookie可以...
综上所述,Go-HiMagpie管家是构建在Go语言基础上的一个服务,它涉及了身份验证的核心组件,包括Token验证、Session管理以及OAuth的实现。同时,它还具备处理和分发推送消息的能力,使得整体架构更加完整,能够满足...
在构建Go语言版本的权限管理服务...总之,构建一个基于Go的权限管理服务平台,结合Shiro的思想,可以帮助我们创建一个强大而灵活的权限控制系统,既能实现用户身份验证,又能进行细粒度的权限控制,提升系统的安全性。
让我们深入探讨这些概念及其在Golang(一种流行的开源编程语言)中的实现。 1. **Cookie**: Cookie是一种小型文本文件,由服务器发送到用户的浏览器并存储在那里。当用户访问同一服务器的页面时,浏览器会将这些...
《Go语言实现GB28181-2016标准的网络视频平台详解》 在信息技术领域,网络视频平台的构建是当前热门且至关重要的一个环节,它为用户提供高效、稳定、安全的音视频服务。GB28181-2016是中国国家标准,全称为《公共...
在Go语言中,有一个名为`gorilla/websocket`的库,它是Go实现WebSocket协议的一个流行选择。这个库提供了一个完整的框架,包括创建WebSocket服务器、处理升级请求、读写WebSocket消息等功能。 要实现WebSocket客户...
goblog是一个用Golang编程语言开发的博客系统,它旨在提供一个简洁而高效的个人博客解决方案。基于GoLang的特性,goblog能够实现高性能和高并发处理,同时利用Mysql作为数据存储,确保了数据的稳定性和可靠性。此外...
Gin是一个Web框架,是Go语言编写的,主要用于开发API服务,而Xorm是一个ORM库,用于简化数据库操作。我们将深入了解如何将Gin与Xorm结合起来,构建一个高效率的Web应用。 首先,我们需要了解Golang在面对一些开发...