I’LL BE HERE

In solitude, where we are least alone.

An Issue I Met When Using Gorilla Sessions Go Package

在使用 Gorilla Web Tookit 中的 Sessions 库来写 Handler 时,我遇到了一些问题。

有问题的程序

我想写一个小服务器,监听 127.0.0.1:8080 端口。当用户打开 http://127.0.0.1:8080 时,浏览器显示 Refresh Counter:1,当用户刷新网页时,浏览器显示 Refresh Counter:2。用户不断刷新浏览器,Refresh Counter 就会递增。

我的想法是,使用 Gorilla Sessions 库来实现。我的代码如下:


package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/securecookie"
	sn "github.com/gorilla/sessions"
	hr "github.com/julienschmidt/httprouter"
)

var store = sn.NewCookieStore(
	securecookie.GenerateRandomKey(64),
	securecookie.GenerateRandomKey(32),
)

func serveIndex(w http.ResponseWriter, r *http.Request, _ hr.Params) {
	session, err := store.Get(r, "refresh-counter")
	if err != nil {
		http.Error(
			w,
			err.Error(),
			http.StatusInternalServerError,
		)
		return
	}
	count, _ := session.Values["count"].(int)
	count += 1

	fmt.Fprintf(w, "Refresh Counter: %d", count)

	session.Values["count"] = interface{}(count)
	err = sn.Save(r, w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

}

func main() {
	router := hr.New()
	router.GET("/", serveIndex)

	log.Fatal(http.ListenAndServe(":8080", router))
}

但是运行以后,程序的行为并不同于我的期望。无论我如何刷新,浏览器内显示的内容总是 Refresh Counter:1

Session 是如何运作的

Session 是通过 Cookie 运作的。当服务器跑起来时,服务器会维护一个 Session 池,Session 池里储存着所有的 Session。

每个 Session 都有一个 Session ID,这个 ID 会储存在用户的浏览器(更准确的说法是 User Agent)里面。

一旦储存成功,此后,用户向服务器发出的每个请求,请求的头部都会带上诸如 Cookie: refresh-counter=aV12...fde 之类的字段。其中,refresh-counter 是 Session 的名字,aV12...fde 是 Session ID,它应该是一段很长的字符串。

顺带一提,用户可以通过浏览器的开发者面板来查看 Session ID。

只要服务器读了请求头部,就可以知道 Session ID,服务器就能根据这个 ID,从 Session 池中找出相应的 Session,得到诸如“用户刷新了多少次”之类的信息。

以上,就是 Session 的运作方式。

简化代码,复现问题

想要解决问题,首先,要找出问题出在哪里。根据 Session 的运作方式,请求的头部会有 Cookie 字段。

然而,我打开浏览器的开发者面板,发现,Cookie 字段并没有包含 Session ID。

也就是说,Cookie 并没有设置成功。

在以上程序中,设置 Cookie 的地方是 err = sn.Save(r, w) 这句话。文档中对 Save 的注释是:Save saves all sessions used during the current request。也就是说,库里面的某处,会遍历一堆 Session,然后调用每个 Session 的 Save 方法。

单个 Session 的 Save 方法会调用到这段代码:

func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
	session *Session) error {
	encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
		s.Codecs...)
	if err != nil {
		return err
	}
	http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
	return nil
}

也就是说,Gorilla Sessions 库是通过 http.SetCookie 来设置 Cookie 的。

函数 http.SetCookie 的代码是:

func SetCookie(w ResponseWriter, cookie *Cookie) {
	if v := cookie.String(); v != "" {
		w.Header().Add("Set-Cookie", v)
	}
}

也就是说,这个 Set-Cookie Header 没有加上。难道问题出在这里?

现在可以开始简化代码了。考虑如下代码中的 serveHello Handler:

package main

import (
    "fmt"
    hr "github.com/julienschmidt/httprouter"
    "log"
    "net/http"
    "time"
)

func serveHello(w http.ResponseWriter, r *http.Request, _ hr.Params) {
    fmt.Fprintf(w, "Response Body")
    w.Header().Add("X-Sample-Response-Header", "12345")
}

func main() {
    router := hr.New()
    router.GET("/hello", serveHello)
    log.Fatal(http.ListenAndServe(":9090", router))
}

问:这个 Handler 能够成功把 X-Sample-Response-Header 加上吗?如果不行的话,就说明这真的是问题所在。

解决方法以及修改后的程序

答:不行。

虽然现在还不知道为什么不行,但是解决方法已经显而易见了:先保存 Session,再写 Response Body 就行了。

原先的代码:

fmt.Fprintf(w, "Refresh Counter: %d", count)

session.Values["count"] = interface{}(count)
err = sn.Save(r, w)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}

修改后的代码:

session.Values["count"] = interface{}(count)
err = sn.Save(r, w)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}

fmt.Fprintf(w, "Refresh Counter: %d", count)

运行之后,发现成功。

接口 http.ResponseWriter 的 Write() 行为

TODO: Explicit/Implicit Flush

总结:先读 Request、然后写 Response Header,最后写 Response Body

  • 读 Request:
  • 写 Response Header:包括修改与保存 Sessions
  • 写 Response Body