A simple Go session implementation

Go's net/http package implements http programming, but the session or session needs to be implemented by the application itself. This article implements a simple session management based on memory storage, and explains some important basic concepts of the language (such as pass-by-value), based on Go 1.9.2.

Analysis
In short, the difficulty of session management is that expired sessions must be automatically destroyed to reclaim memory to avoid memory leaks, but the destruction process should not affect the normal use of other non-expired sessions.
Usually all sessions are stored in a map, with session id as the key, so that the session can be quickly obtained by id. However, the expired sessions must be cleared in time, so consider adding a linked list outside the map to link all sessions according to their activeness: each time a session is acquired, it will be promoted to the head of the chain, so that the tail of the chain will gradually accumulate all the unrelated sessions. For active sessions, expiration cleanup can be performed quickly from the end of the chain.
This changes the map value to the elements of the list. Use a Go container/list doubly-directed list: Knowing an element of the list, you can quickly move it around without traversing the entire list.

The design
directory structure is as follows:
$GOPATH/src/sample/memses
    main.go
$GOPATH/src/sample/memses/session
    session.go

main.go is the main program, and the session directory is the session implementation.

First define the session type:
type ISession interface {
	Id() string
	Get(key string) interface{}
	Set(key string, value interface{})
	Remove(key string)
	Invalidate()
}

Go, like Java, also uses interfaces to define types. Session has get/set/delete, which can be actively expired. The session id is generally fixed and will not be modified. interface{} is similar to Java's Object, but can also be int/float32, etc.

session manager:
type ISessionManager interface {
	Get(id string) ISession // get or create a session (with new id)
}

The session manager is used to obtain a session, passing in the session id. If there is, return it, if not or it has expired, create a new one and return it.

To implement
all the data needed for a session, we define it on a struct:
type ses struct {
	MSc * sesmgr
	id   string
	lock *sync.RWMutex // for smap,time
	smap map[string]interface{}
	time time.Time //access time
}

Session data is stored in smap. Since the session may be used concurrently (for example, a page initiates 2 background requests at the same time), access synchronization is performed through the sync RW lock. Note that the Go language usually recommends channel/goroutine to make a data only accessed by one goroutine to avoid multiple goroutines accessing the same data concurrently, but this suggestion is usually used for program flow-level control, when refined to a map When reading and writing do concurrency control, a simple RW lock seems more appropriate.
The type of the lock field is *sync.RWMutex instead of sync.RWMutex, what's the difference? See godoc faq#pass_by_value: When calling a Go function, all parameters and results are passed by value instead of by reference. In this way, when calling the ses.lock.XX method, a copy of *sync.RWMutex is passed - the pointer copy does not affect the original lock object that is still used, and if it is a sync.RWMutex copy, it is no longer the original lock object. The RWMutex godoc says "An RWMutex must not be copied after first use.".
See also faq#references, Go map|slice|channel is actually a pointer: that is, a map object (non-map pointer) actually stores a pointer to the actual data, so map|slice|channel value is also possible. But note that Go arrays are actual value objects, and when passing a larger array it is better to pass a pointer to it instead.
Also, small structs can still be passed by value because they are 'cheap' to copy. In the end, it is best to unify: all methods of the same object are either passed by value or passed by reference.

Regarding the session access time ses.time, our implementation will be simplified to update it only when the session is obtained through the manager, and will not be updated when the subsequent session.get/set/.. is used.
The ses.mgr field is used to implement the session.Invalidate method.

In Go, if a type implements all the methods of an interface, the type also implements the interface, and there is no need for explicit 'implements' like in Java. ses struct implements the ISession interface:
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)
}

As in the Get method above, Lock is required to prevent concurrency. defer Unlock will execute before the method returns (the return value has been obtained), similar to Java's try/finally.

Then there is the implementation struct of the session manager:
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
}

Use doubly linked list outside of map: map value is the element of linked list, value of linked list element is ses struct. The known id finds the list element from the map and its value is ses (ISession). The same list element can quickly move and delete in the linked list. Also use RW locks to control concurrency.

The Get method implementation of ISessionManager:
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
}

If it is found in smap and it has not expired, move to the head of the linked list and return, otherwise delete the expired one and create a new one and return.
The type of List.Element.Value is interface{}. "e.Value.(ses)" is a type assertion "x.(T)": x needs to be an interface type, and the dynamic type of x should = T when T is not an interface type.
new(xx) returns a pointer, and make(xx) returns a value.

Start at the end of the linked list when cleaning. In order to avoid taking too long to clean up once, set the upper limit of the number of cleanups. When the number exceeds and the linked list is not empty, the next cleanup will be advanced. However, when the linked list has been emptied or the remaining ones are not expired, it is necessary to avoid starting the next cleanup frequently:
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
	}
}

The return value here = the waiting time until the next cleanup, start a goroutine when the session manager is created to continue cleanup:
	go func() {
		for {
			time.Sleep(sm.gcOnce())
		}
	}()

go funcxx starts a goroutine. A goroutine is a more lightweight thread, which can be imagined as a Java thread that can schedule multiple goroutines. See faq#goroutines, a goroutine occupies only a few kilobytes of memory initially, and a program can use hundreds or thousands of goroutines.

Unit test
Go provides convenient unit test writing. Create session_test.go in the same directory as session.go. Each "func TestXX(*testing.T)" method in it is a test method, and the test is run through go test.

Types under the same package in Go are visible to each other, and there is no private. Since the test class and session.go are in the same package, it is convenient for us to write a utility method that creates a session manager but does not initiate automatic cleanup:
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
}


Simple test:
func Test1(t *testing.T) {
	sm := createMgr(time.Minute)
	//1. one
	s := sm.Get("")
	if sm.list.Len() != 1 {
		t.Errorf("one: len != 1")
	}
	//。。。
}

The "t.Errorf" method reports a failure.

Test cleanup by calling the gcOnce method:
	sm = createMgr(10 * time.Second) //10s timeout
	id1 = sm.Get("").Id()
	sm.gcOnce()
	if sm.list.Len() != 1 {
		t.Errorf("gc: should gc none")
	}


The browser test
needs to actually verify the session in the browser: main.go uses net/http to start an http server, and when it receives a browser request (similar to a Java servlet), it stores the id of the generated session through a fixed-name cookie. The cookie will be sent to the browser, and the browser will send it to the server for each subsequent request:
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
}

Note that the above getSession method is only for testing, and there may be some potential problems in production (such as domain name changing ip).

Run the program, visit the display page to confirm that the session id of each refresh is the same, and you can also set the value and destroy the current session.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326520418&siteId=291194637