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。
分享到:
相关推荐
3.2 Go搭建一个简单的web服务 3.3 Go如何使得web工作 3.4 Go的http包详解 3.5 小结 4.表单 4.1 处理表单的输入 4.2 验证表单的输入 4.3 预防跨站脚本 4.4 防止多次递交表单 4.5 处理文件上传 4.6 小结 5.访问数据库 ...
ASP.NET实现多域名多网站共享Session值 1、实现功能:可设置哪些站点可以共享Session值,这样就防止别人利用这个去访问 要想实现这个功能就必须得把Session值 放入数据库中, 所有我们先在VS命令工具下注册一个 命名...
配置连接数据库 DATABASES = { 'default': { 'ENGINE': 'django.db.backends....生成session表 python manage.py makemigrations python manage.py migrate 登录时记住保存用户登录信息 # 登录验证 def logi
C++ 打开一个虚拟桌面的代码 简单实用。可以从原来的桌面切换到虚拟桌面。 win7环境 欢迎大家体验哈
Seago是golang实现的简单的web框架,router包来自web.go和martini 功能 支持RESTful 支持Session 支持Cache 支持Middleware 标签:Seago Web框架
由于用的是Django自带的认证,然后校验用户是否登录其实就是通过Session实现的。下面就简单分享一下怎么实现的吧。 单用户登录实现 在做用户登录认证的时候Django自带的有is_authenticated()方法。下面就是一个简单...
mgo(音mango)是MongoDB的Go语言驱动,它用基于Go语法的简单API实现了丰富的特性,并经过良好测试。 初始化 操作没有用户权限的MongoDB var globalS *mgo.Session func init() { s, err := mgo.Dial(dialInfo) ...
所以这样就需要状态保持功能,状态保存有两种方式:session和cookie都能实现状态保持。 状态保持 http协议是无状态的:每次请求都是一次新的请求,不会记得之前通信的状态 客户端与服务器端的一次通信,就是一次...
主要实现如下功能: 1. 支持注册,登录功能,用户可以注册完成后,进行登录,登录完成后会进入到列表增删改查页面。 2. 支持session会话,也就是说设置了多长时间登录过期,如果用户没有登录,直接进查询列表页面,...
当一个客户端通过nginx负载后连接到broker1时,会产生一条session,保存会话信息(相应的主题与路由表改变,这里不先谈)。当客户端断线重连时,可能会连接到其它broker,如broker2,如果断线前的那个连接没有选择...
但是,你说你每次上网的时候,只需要登录一下就行了,并没有我说的让你每次都登录,这是会话路径技术帮你记录了你的登录信息,现在我们们就来讲讲Django的会话路径技术cookie和session,实现会话追踪。 二、cookie ...
百度BDUSS获取工具 v1.2.1 Go语言 功能 增加 session 支持,数据安全性提高 百度: 获取百度帐号 BDUSS, PTOKEN, STOKEN 值 百度: 支持在线 手机/邮箱 安全验证(beta) 如何使用 Go语言程序, 可直接下载使用 在 ...
ecgo 是一个易学、易用、易扩展的go web开发框架。核心功能包括:自动规则路由,支持RESTfulrequest的二次封装可以直接使用格式化的Get,Post,Cookie,Session等变量来处理请求数据方便的上传文件操作response二次...
Django Web框架的Curl,对Django服务器进行身份验证请求
cookie是一个key-value的数据结构(类似python字典),用于保存需要维护状态的数据,cookie与session最大的区别是cookie的数据保存在客户端,而session把数据保存在服务端。 cookie一般由服务器设置,并可以存放在...
Cookie与Session说明与实现 Cookie 说明 Cookie是一段小信息(数据格式一般是类似key-value的键值对),由服务器生成,并发送给浏览器让浏览器保存(保存时间由服务端定夺)。当浏览器下次访问该服务端时,会将它保存的...
使用Singo开发Web服务: 用最简单的架构,实现够用的框架,服务海量用户 https://github.com/Gourouting/singo Singo文档 https://gourouting.github.io/ 视频实况教程 让我们写个G站吧!Golang全栈编程实况 ...
转换为AccountHash值recipient := NewKeyHolder(nil, , "secp256k1")recipientAccountHash := recipient.AccountHash()// deploy需要由payment和session两部分组成// 创建一个标准的payment数据payment := ...
要获取会话,请使用SessionFactory ,后者再包装sql.DB 您可以创建一个SessionFactory通过调用depot.Open函数传递同样的参数将传递给sql.Open ,或创建一个sql.DB自己的值(即,如果要配置连接池),并通过这depot....