jinzh notes
jinzh notes

Go实现Session

Go实现Session
内容纲要

前言

使用go来实现session功能,go的官方包或者一些网络框架并未给出一个现成的轮子,需要我们自己动手造一个

本文中的代码由我自网络上寻找样例,并结合我自己需求进行修改而成!

session

session并未在维基中有着明确的定义,因此引用一下百度的给出的定义

Session:在计算机中,尤其是在网络应用中,称为“会话控制”。

Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。

当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。

注意会话状态仅在支持cookie的浏览器中保留。

这里百度给出的定义有一个误区,session并不是在只支持cookie的浏览器中保留,也可以使用token方法实现,即将特殊额sessionid放置在链接中,不过从实际看,还是放置在cookie中更加好用

undefined

思路

简言之,session就是在服务器开辟一块空间(内存、硬盘、数据库),将之用于存放来访者的信息,相当于对来访者进行一次标记,通过这个使无状态的HTTP协议变成有状态的形式。

在开辟空间的同时,通过给cookie设定一个特别的和开辟空间所关联的字符串(sessionid),这样子在浏览器下次访问时就知道是那个用户了

为了方便复用,我们选择将session这个功能看做session(session本身)和provider(session管理器),即每个用户有一个session,provider用来管理当前domain下的所有session。

我们选择再在provider上抽象一层,抽象为manager,用于最外部提供接口实现多站点效果。

实现

session 管理设计

我们知道 session 管理涉及到如下几个因素

  • 全局 session 管理器
  • 保证 sessionid 的全局唯一性
  • 为每个客户关联一个 session
  • session 的存储(可以存储到内存、文件、数据库等)
  • session 过期处理

代码

基础接口抽象

抽象出两个接口实现

//session接口
type Session interface {
    //session的四个方法
    Set(key, value interface{}) error //设置session
    Get(key interface{}) interface{}  //获取session
    Delete(key interface{}) error     //删除session
    SessionID() string                //返回sessionID
}
//provider接口
type Provider interface {
    //session管理器
    SessionInit(sessionId string) (Session, error)
    SessionRead(sessionId string) (Session, error)
    SessionDestory(sessionId string) error
    GarbageCollector(maxLifeTime int64)
    SessionUpdate(sid string) error
}

Session接口基本功能四个:设置、获取、删除、返回sessionid,也就是设置的cookie值或者发送给前端的token

session本身是存放在服务器的内存中的,所以使用抽象出的provider作为底层结构,它拥有五个方法:session初始化、session读取、session销毁、GC、session时间更新

为了提高业务的复用性,我们选择再创建一个providers来存储provider,同时我们编写一个注册方法,以便根据provider的名字来找到对应的provider

func RegisterProvider(name string, provider Provider) {
    //注册方法
    if provider == nil {
        panic("session: Register provider is nil")
    }

    if _, p := providers[name]; p {
        panic("session: Register called twice for provide " + name)
    }

    providers[name] = provider
}

session管理器进一步封装

这里选择对providers进行进一步封装,构建一个结构体manager

type Manager struct {
    cookieName  string     //cookie的名称
    lock        sync.Mutex //锁,保证并发时数据的安全一致
    provider    Provider   //管理session
    maxLifeTime int64      //超时时间
}

这里设置一个锁用来保证并发下数据的安全性,cookieName可以使cookie的名字,也可以用作token字段的名字

manager结构体创建构造函数:

func NewManager(providerName, cookieName string, maxLifeTime int64) (*Manager, error) {
    provider, ok := providers[providerName]
    if !ok {
        //ok应该为真,如果ok为假则代表未注册在providers中
        return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", providerName)
    }

    if provider == nil {
        log.Fatal("error: provider is nil")
    }
    //返回一个 Manager 对象
    return &Manager{
        cookieName:  cookieName,
        maxLifeTime: maxLifeTime,
        provider:    provider,
    }, nil
}

可以在session的包中定义一个初始化函数init(),用来在包引入初始化所需要的变量。例如,我们可以在这里添加初始化一个固定的manager,作为公共变量供其他包使用,由于我们的manager结构体还支持声明cookie名字,这里就可以实现多个manager管理自己对应的cookie

func init() {
    GlobalSession, err = session.NewManager("user", "sessionId", 3600)
    if err != nil {
        log.Fatal("error", err)
    }
    //go GlobalSession.GarbageCollector()
    //这里执行的gc
}

创建一个session()方法用于获取sessionid,如下:

//返回session
func (manager *Manager) Session(r *http.Request) Session {
    //此处不需要加锁
    //仅涉及读操作
    cookie := r.URL.Query().Get(SessionName)
    if cookie == "" {
        return nil
    } else {
        sid, _ := url.QueryUnescape(cookie)
        session, err := manager.provider.SessionRead(sid)
        if err != nil {
            return nil
        }
        return session
    }
}

我这里使用的是token方式,如果是cookie的话只需要把上面的获取url参数更改为获取cookie即可

现在为manager创建一个用于处理连接初始化session的方法(设置cookie或者返回token):

//判断当前请求的cookie中是否存在有效的session,存在返回,否则创建
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) Session {
    manager.lock.Lock() //加锁
    defer manager.lock.Unlock()
    var (
        session Session
        err     error
    )
    cookie := r.URL.Query().Get(SessionName)
    if cookie == "" {
        //创建一个
        sid := SessionId()
        session, _ = manager.provider.SessionInit(sid)
        // cookie := http.Cookie{
        //  Name:     manager.cookieName,
        //  Value:    url.QueryEscape(sid), //转义特殊符号@#¥%+*-等
        //  Path:     path,
        //  Domain:   domain,
        //  HttpOnly: true,
        //  MaxAge:   int(manager.maxLifeTime),
        //  Expires:  time.Now().Add(time.Duration(manager.maxLifeTime)),
        //  //MaxAge和Expires都可以设置cookie持久化时的过期时长,Expires是老式的过期方法,
        //  // 如果可以,应该使用MaxAge设置过期时间,但有些老版本的浏览器不支持MaxAge。
        //  // 如果要支持所有浏览器,要么使用Expires,要么同时使用MaxAge和Expires。
        // }
        // http.SetCookie(w, &cookie)
    } else {
        sid, _ := url.QueryUnescape(cookie) //反转义特殊符号
        session, err = manager.provider.SessionRead(sid)
        if err != nil {
            //当前的session获取为nil
            session, _ = manager.provider.SessionInit(sid)
        }
    }
    return session
}

以上为实现token的格式,cookie部分被我注释掉了,大致思路就是当有连接时,先读取request的url参数,如果没有就初始化,如果有就尝试在provider查找,没查找到就直接用传递过来的token进行初始化

接下来是销毁部分:

//销毁session 同时删除cookie
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
    // 获取sessionid
    cookie := r.URL.Query().Get(SessionName)
    if cookie == "" {
        return
    } else {
        manager.lock.Lock()
        defer manager.lock.Unlock()
        sid, _ := url.QueryUnescape(cookie)
        manager.provider.SessionDestory(sid)
        // expiration := time.Now()
        // cookie := http.Cookie{
        //  Name:     manager.cookieName,
        //  Value:    "",
        //  Path:     path,
        //  Domain:   domain,
        //  HttpOnly: true,
        //  Expires:  expiration,
        //  MaxAge:   1,
        // }
        // http.SetCookie(w, &cookie)
    }
}

最后是GC:

//manager的回收器
func (manager *Manager) GarbageCollector() {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    manager.provider.GarbageCollector(manager.maxLifeTime)
    //使用time包中的计时器功能,它会在session超时时自动调用GC方法
    time.AfterFunc(time.Duration(manager.maxLifeTime), func() {
        manager.GarbageCollector()
    })
}

接口实现

剩下部分就是自由发挥实现两个接口的功能,这里不再赘述,直接上我写完的代码!

session接口实现

type SessionStore struct {
    //实现session
    sid              string                      //session id 唯一标示
    LastAccessedTime time.Time                   //最后访问时间
    value            map[interface{}]interface{} //session 里面存储的值
    provider         Provider
}

func NewSession() *FromMemory {
    return &FromMemory{
        list:     list.New(),
        sessions: make(map[string]*list.Element),
    }
}

//设置
func (st *SessionStore) Set(key, value interface{}) error {
    st.value[key] = value
    st.provider.SessionUpdate(st.sid)
    return nil
}

//获取session
func (st *SessionStore) Get(key interface{}) interface{} {
    st.provider.SessionUpdate(st.sid)
    if v, ok := st.value[key]; ok {
        return v
    } else {
        return nil
    }
}

//删除
func (st *SessionStore) Delete(key interface{}) error {
    delete(st.value, key)
    st.provider.SessionUpdate(st.sid)
    return nil
}

//获取sessionId
func (st *SessionStore) SessionID() string {
    return st.sid
}

provider接口实现

//session来自内存 实现
type FromMemory struct {
    lock     sync.Mutex               //用来锁
    sessions map[string]*list.Element //用来存储在内存
    list     *list.List               //用来做 gc
}

// 管理器初始化一个session
func (frommemory *FromMemory) SessionInit(sid string) (Session, error) {
    frommemory.lock.Lock()
    defer frommemory.lock.Unlock()
    v := make(map[interface{}]interface{}, 0)
    newsess := &SessionStore{
        sid:              sid,
        LastAccessedTime: time.Now(),
        value:            v,
        provider:         frommemory,
    }
    element := frommemory.list.PushBack(newsess)
    frommemory.sessions[sid] = element
    return newsess, nil
}

// 管理器根据sessionId读取一个session
func (frommemory *FromMemory) SessionRead(sid string) (Session, error) {
    element, ok := frommemory.sessions[sid]
    if ok {
        return element.Value.(*SessionStore), nil
    } else {
        return nil, errors.New("session: this sessionId not exist")
    }
}

// 管理器销毁一个sessionId
func (frommemory *FromMemory) SessionDestory(sid string) error {
    if element, ok := frommemory.sessions[sid]; ok {
        delete(frommemory.sessions, sid)
        frommemory.list.Remove(element)
        return nil
    }
    return nil
}

// 管理器回收sessionId函数
func (frommemory *FromMemory) GarbageCollector(maxLifeTime int64) {
    frommemory.lock.Lock()
    defer frommemory.lock.Unlock()
    for {
        element := frommemory.list.Back()
        if element == nil {
            break
        }
        if (element.Value.(*SessionStore).LastAccessedTime.Unix() + maxLifeTime) < time.Now().Unix() {
            frommemory.list.Remove(element)
            delete(frommemory.sessions, element.Value.(*SessionStore).sid)
        } else {
            break
        }
    }
}

// 管理器更新session的存活时间
func (frommemory *FromMemory) SessionUpdate(sid string) error {
    frommemory.lock.Lock()
    defer frommemory.lock.Unlock()
    if element, ok := frommemory.sessions[sid]; ok {
        element.Value.(*SessionStore).LastAccessedTime = time.Now()
        frommemory.list.MoveToFront(element)
        return nil
    }
    return nil
}

包的init方法


var (
    sessionName []string = []string{
        "user",
    }
    //这里用于实现多个cookie或者token的注册
    providers        = make(map[string]Provider)
    //domain    string = ""
    //path      string = "/"
)

func init() {
    for _, name := range sessionName {
        RegisterProvider(name, NewSession())
    }
    //在provider中注册

}

实际使用

直接在要使用的包定义一个全局变量,例如:

var GlobalSession *session.Manager
var err error

func init() {
    GlobalSession, err = session.NewManager("user", "sessionId", 3600)
    if err != nil {
        log.Fatal("error", err)
    }
    go GlobalSession.GarbageCollector()
}

上面是用于来目的包中初始化manager,后续操作只需要使用定义的GlobalSession变量即可!

影翼

文章作者

发表评论

textsms
account_circle
email

jinzh notes

Go实现Session
前言 使用go来实现session功能,go的官方包或者一些网络框架并未给出一个现成的轮子,需要我们自己动手造一个 本文中的代码由我自网络上寻找样例,并结合我自己需求进行修改而成! sess…
扫描二维码继续阅读
2022-02-13