簡単なGo session実現

8177 ワード

もっと読む
Goのnet/httpパッケージはhttpプログラミングを実現しましたが、会話すなわちsessionは自分で実行する必要があります。本論文ではメモリ記憶に基づいて簡単なsession管理を実現し、いくつかの言語の重要な基礎概念(例えば、伝値アドレス)を説明し、Go 1.5.2に基づく。
分析
簡単に言えばsession管理の難点は、期限が切れたsessionが自動的に廃棄され、メモリの漏洩を避けるために回収されることであるが、この廃棄過程は他の非期限sessionの正常な使用に影響を与えないようにすることである。
通常は全てのセッションが一つのmapに格納され、session idをキーとしているので、このようにして素早くidからセッションを取得することができる。ただし、期限切れのセッションは即時にクリアできるので、map以外にlinked listを追加することを考慮して、すべてのsessionをアクティブ度にリンクします。sessionを取得するたびにチェーンヘッドにアップグレードします。そうすると、チェーンの最後には活発でないセッションが徐々に蓄積され、期限切れの清理はチェーンの最後から迅速に実行できます。
このようにmap valueをlistの要素に変えます。Go container/list双方向リストを使用して、リスト全体を巡回することなく、すばやく移動することができます。
デザイン
ディレクトリ構造は以下の通りです。

$GOPATH/src/sample/memses
    main.go
$GOPATH/src/sample/memses/session
    session.go
main.goはメインプログラムで、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は一般的に固定されています。修正されません。インターフェース{}はJavaのObjectに似ていますが、int/float 32などでもいいです。
セッションマネージャ:

type ISessionManager interface {
	Get(id string) ISession // get or create a session (with new id)
}
sessionマネージャはsessionを取得し、session idに入る。もしあるなら、それを返します。ないか、または期限が切れたら、新しいのを作って戻ってきます。
実現する
一つのセッションに必要なすべてのデータを一つの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は、同時に使用されることがある(例えば、1ページで2つのバックグラウンド要求を開始する)ので、アクセス同期は、sync RWロックによって行われる。Go言語に注意すると、通常は、チャンネル/goroutineによって、一つのデータが一つのgoroutineによってしかアクセスできないようにして、複数のgoroutineを回避して同時に同じデータにアクセスすることを提案していますが、一つのmapに対する読み書きと制御を細分化する場合、簡単なRWロックがより適切に見えるようになります。
ロックフィールドのタイプは、sync.RWMutexではなく、*sync.RWMuttexであり、違いは何ですか?godoc faq菗pass_を参照してください。by_value:Go関数の呼び出し時に、すべてのパラメータ、結果の伝達はアドレスではなく、転送値です。このようにses.lock.XX方法を呼び出すと、*sync.RWMutexのコピー-ポインタコピーは元のlockオブジェクトをそのまま使用し、sync.RWMutexコピーであれば元のlockオブジェクトではないと伝えられる。RWMustex godocには「An RWMustex must not be copied after first use.」が要求されます。
同様にfaq_Ci_referencesを参照してください。Go map_pace_chanelは、実際にポインタです。つまり、mapオブジェクト(mapポインタではない)の内部に実際に格納されているのは、実際のデータを指すポインタです。だから、map_slice_chanel伝値も可能です。ただし、Go arrayは実際の値のオブジェクトであり、大きなarrayを転送する場合は、転送のポインタに変更したほうがいいです。
また、小さいstructはコピーが「安い」ので、値を伝えられます。最後に統一したほうがいいです。同じオブジェクトのすべての方法はいずれも値を伝えたり、住所を伝えたりします。
session訪問時間ses.timeについては、managerを通じてsessionを取得したときにのみ更新するように簡略化し、その後のsession.get/set/.は更新しない。
ses.mgrフィールドは、session.Invalidate方法を実現するために使用されます。
Goの中の1つのタイプが、あるインターフェースのすべての方法を実現すると、このタイプは、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メソッドでは合併を防ぐためにロックします。defer Ulockは、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です。同様に、list elementは、急速にチェーン内を移動し、削除することができます。同様にRWロックで同時制御します。
ISessitionManagerの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 astertion“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を参照してください。goroutineは最初は数kのメモリを占めています。プログラムは100以上の千goroutineに使用できます。
ユニットテスト
Goは便利なユニットテスト編纂を提供し、session.goと同じディレクトリでsessionを作成します。test.goは、それぞれの「Fnc TestXX(*testing.T)」方法をテスト方法として、go testによってテストを実行します。
Goの中に同じカバンのタイプがお互いに見えます。prvateは存在しません。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を格納します。このクッキーはブラウザに送信されます。ブラウザは後で要求するたびに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
}
上記のget Session法はテスト用にのみ使用されており、生産にはまだいくつかの潜在的な問題(例えばドメイン名変換ip)があるかもしれない。
プログラムを実行して、展示ページにアクセスして、毎回更新するsession idと同じであることを確認します。また、set値を設定し、現在のsessionを廃棄することもできます。
  • memses.zip(3.8 KB)
  • ダウンロード回数:4