打造一款屬於自己的web服務器——實現Session


    上一次我們已經實現了一個簡單的web服務器版本,能夠實現一些基本功能,但是在最后也提到了這個版本由於不支持session並不能實現真正的動態交互,這一次我們就來完成這一功能。

一、Session實現原理

    凡是搞過web開發的都知道,多數情況下瀏覽器請求服務器使用的是http請求,而http請求是無狀態的,也就是說每次請求服務器都會新建連接,當得到響應后連接就關閉了,雖然http1.1支持持久連接(keep-alive),但是其最用主要是避免每次重建連接,而非解決用戶在線狀態等業務上的需求。而如果服務器想知道客戶端的狀態或是識別客戶端,那么就不能像長連接那樣通過連接本身實現,而是要通過每次請求時的數據來判斷。
    我們首先來看一下下圖:

    從上圖我們可以很清楚的看出session是如何實現的,一般在客戶端第一次請求的時候,服務器會生成一個session_id(不同服務器可能名字不同,其值是一個唯一串)作為會話標示,同時服務器會生成一個session對象,用來存儲該會話相關的數據。在響應時在請求頭通過Set-Cookie(用法)可在客戶端cookies中添加session_id。之后的訪問中,每次服務器都會檢測session_是否存在並能找到對應session對象,以此來識別客戶端。
    這里還有一個問題就是,如果客戶端關閉了怎么辦?服務器如何知道?實際上服務器並不需要去關心客戶端是否失敗,通常的做法是給session設置過期時間,每次請求時重置過期時間,如果在過期前一直無請求,則清除該session,這樣會話就相當於結束了。這里還需注意一點是,實際情況下設置的客戶端session_id一定要是臨時cookie,這樣在關閉瀏覽器時session_id會清除,否則你在過期時間內重新打開瀏覽器還能夠繼續改會話,明顯是不合理(本版本就不考慮這個問題了)。

二、功能設計

    和之前一樣,我們先來設計一下應該如何在我們的項目中實現。首先,我們來確定一下數據結構。session本身就不必多說了,核心是一個map,存儲數據,同時我們還需要記錄每個session的最后訪問時間,以便處理過期問題。
    那么session集合我們怎么存儲呢?大家都知道每個web程序啟動都會生成一些內置對象,session相當於會話級別的(作用范圍是一個會話內),那么還有一個web應用級別的,在該web程序全局可訪問。由於session集合在應用多個層次都需要訪問,因此我們需要實現一個單例的ApplicationContext,處理全局數據,同時處理session的創建和訪問。
    接下來我們來設計下如何處理session。首先根據上邊介紹,我們應該在接收請求后即判斷並生成session,以保證后續業務能獲取session,因此我們應該在EHHttpHandler的handler()方法開始就完成這些操作。此外,由於之前設計的在調用controller時我們只傳了一個map參數集合,這樣在controller中無法獲取session,因此調用controller我們將session放入map中(這只是簡單做法,比較好的做法是對參數進行封裝,這樣如果以后需要拓展參數類型,只需要修改封裝后的類即可)。
    隨后我們還有實現一個定時任務,定期清理過期session。

三、實現代碼

    思路清晰,代碼實現就非常簡單了。這里就不再詳細介紹每部分代碼了,基本上看注釋就明白。
    首先看下Session和ApplicationContext的代碼(話說就沒人提議 @紅薯 加個代碼折疊的功能嗎):

/**
 * session數據
 * @author guojing
 * @date 2014-3-17
 */
public class HttpSession {
    Map<String, Object> map = new HashMap<String, Object>();
    Date lastVisitTime = new Date(); // 最后訪問時間

    public void addAttribute(String name, Object value) {
        map.put(name, value);
    }

    public Object getAttribute(String name) {
        return map.get(name);
    }

    public Map<String, Object> getAllAttribute() {
        return map;
    }

    public Set<String> getAllNames() {
        return map.keySet();
    }

    public boolean containsName(String name) {
        return map.containsKey(name);
    }

    public Map<String, Object> getMap() {
        return map;
    }

    public void setMap(Map<String, Object> map) {
        this.map = map;
    }

    public Date getLastVisitTime() {
        return lastVisitTime;
    }

    public void setLastVisitTime(Date lastVisitTime) {
        this.lastVisitTime = lastVisitTime;
    }

}
/**
 * 全局數據和會話相關數據,單例
 * @author guojing
 * @date 2014-3-17
 */
public class ApplicationContext {
    private Map<String, Object> appMap = new HashMap<String, Object>(); // ApplicationContext全局數據

    /**
     * 這里自己也有點搞不清sessionMap是不是有必要考慮線程安全,還請指教
     */
    private ConcurrentMap<String, HttpSession> sessionMap = new ConcurrentHashMap<String, HttpSession>(); // session數據

    private ApplicationContext(){
    }

    /**
     * 內部類實現單例
     */
    private static class ApplicationContextHolder {
        private static ApplicationContext instance = new ApplicationContext();
    }
    
    public static ApplicationContext getApplicationContext() {
        return ApplicationContextHolder.instance;
    }

    public void addAttribute(String name, Object value) {
        ApplicationContextHolder.instance.appMap.put(name, value);
    }

    public Object getAttribute(String name) {
        return ApplicationContextHolder.instance.appMap.get(name);
    }

    public Map<String, Object> getAllAttribute() {
        return ApplicationContextHolder.instance.appMap;
    }

    public Set<String> getAllNames() {
        return ApplicationContextHolder.instance.appMap.keySet();
    }

    public boolean containsName(String name) {
        return ApplicationContextHolder.instance.appMap.containsKey(name);
    }

    public void addSession(String sessionId) {
        HttpSession httpSession = new HttpSession();
        httpSession.setLastVisitTime(new Date());
        ApplicationContextHolder.instance.sessionMap.put(sessionId, httpSession);
    }

    /**
     * 獲取session
     */
    public HttpSession getSession(HttpExchange httpExchange) {
        String sessionId = getSessionId(httpExchange);
        if (StringUtil.isEmpty(sessionId)) {
            return null;
        }
        HttpSession httpSession = ApplicationContextHolder.instance.sessionMap.get(sessionId);
        if (null == httpSession) {
            httpSession = new HttpSession();
            ApplicationContextHolder.instance.sessionMap.put(sessionId, httpSession);
        }
        return httpSession;
    }

    /**
     * 獲取sessionId
     */
    public String getSessionId(HttpExchange httpExchange) {
        String cookies = httpExchange.getRequestHeaders().getFirst("Cookie");
        String sessionId = "";
        if (StringUtil.isEmpty(cookies)) {
            cookies = httpExchange.getResponseHeaders().getFirst("Set-Cookie");
        }
        
        if (StringUtil.isEmpty(cookies)) {
            return null;
        }

        String[] cookiearry = cookies.split(";");
        for(String cookie : cookiearry){
            cookie = cookie.replaceAll(" ", "");
            if (cookie.startsWith("EH_SESSION=")) {
                sessionId = cookie.replace("EH_SESSION=", "").replace(";", "");
            }
        }
        
        return sessionId;
    }

    /**
     * 獲取所有session
     */
    public ConcurrentMap<String, HttpSession> getAllSession() {
        return ApplicationContextHolder.instance.sessionMap;
    }

    /**
     * 設置session最后訪問時間
     */
    public void setSessionLastTime(String sessionId) {
        HttpSession httpSession = ApplicationContextHolder.instance.sessionMap.get(sessionId);
        httpSession.setLastVisitTime(new Date());
    }
}
View Code

    可以看出這兩部分代碼十分簡單,下邊看一下handle中如何處理session:

public void handle(HttpExchange httpExchange) throws IOException {
        try {
            String path = httpExchange.getRequestURI().getPath();
            log.info("Receive a request,Request path:" + path);
            
            // 設置sessionId
            String sessionId = ApplicationContext.getApplicationContext()
                    .getSessionId(httpExchange);
            if (StringUtil.isEmpty(sessionId)) {
                sessionId = StringUtil.creatSession();
                ApplicationContext.getApplicationContext().addSession(sessionId);
            }
            
            //.....其他代碼省略
        } catch (Exception e) {
            httpExchange.close();
            log.error("響應請求失敗:", e);
        }
    }

    /**
     * 調用對應Controller處理業務
     * @throws UnsupportedEncodingException 
     */
    private ResultInfo invokController(HttpExchange httpExchange) throws UnsupportedEncodingException {
        // 獲取參數
        Map<String, Object> map = analysisParms(httpExchange);
        IndexController controller = new IndexController();
        
        // 設置session
        HttpSession httpSession = ApplicationContext.getApplicationContext().getSession(
                httpExchange);
        log.info(httpSession);
        map.put("session", httpSession);
        
        return controller.process(map);
    }
View Code

    最后看一下定時任務的實現:

/**
 * 定時清理過期session
 * @author guojing
 * @date 2014-3-17
 */
public class SessionCleanTask extends TimerTask {
    private final Log log = LogFactory.getLog(SessionCleanTask.class);

    @Override
    public void run() {
        log.info("清理session......");
        ConcurrentMap<String, HttpSession> sessionMap = ApplicationContext.getApplicationContext()
                .getAllSession();
        
        Iterator<Map.Entry<String, HttpSession>> it = sessionMap.entrySet().iterator();
        while (it.hasNext()) {
            ConcurrentMap.Entry<String, HttpSession> entry= (Entry<String, HttpSession>) it.next();
            HttpSession httpSession= entry.getValue();
            
            Date nowDate = new Date();
            int diff = (int) ((nowDate.getTime() - httpSession.getLastVisitTime().getTime())/1000/60);
            
            if (diff > Constants.SESSION_TIMEOUT) {
                it.remove();
            }
        }

        log.info("清理session結束");
    }
}
View Code

    此次改動的代碼就這么多。

四、測試

    下邊我們來測試一下是否有效。由於目前controller是寫死的,只有一個IndexController可用,那么我們就將就着用這個來測試吧,我們先來改一下其process方法的代碼:

	public ResultInfo process(Map<String, Object> map){
		ResultInfo result =new ResultInfo();
		
		// 這里我們判斷請求中是否有name參數,如果有則放入session,沒有則從session中取出name放入map
		HttpSession session = (HttpSession) map.get("session");
		if (map.get("name") != null) {
			Object name = map.get("name");
			session.addAttribute("name", name);
		} else {
			Object name = session.getAttribute("name");
			if (name != null) {
				map.put("name", name);
			}
		}
		
		result.setView("index");
		result.setResultMap(map);
		return result;
	}

    可以看到我們增加了一段代碼,作用見注釋。然后我們啟動服務器,先訪問 http://localhost:8899/page/index.page,請求結果如下(我那高大上的logo就不截了^_^):

    可以看到name由於沒有值,所以未解析,再來訪問 http://localhost:8899/page/index.page?name=guojing,結果如下:

    這次發現有值了,但是看代碼我們知道這應該是請求參數的值,並非從session中取得,我們再來訪問 http://localhost:8899/page/index.page ,這次應該會從session中取值,因此照樣能輸出guojing,結果如下:

    說明session已經起作用了,你還可以等sesion清理后看下是否還有效。ApplicationContext測試方法一樣。

五、總結

    本次實現的功能應該說是點睛之筆,session的實現從根本上提供了動態交互的支持,現在我們能夠實現登陸之類的功能的。但是正如上邊提到的,現在整個項目還很死板,我們目前只能使用一個controller,想要實現多個則需要根據請求參數進行判斷,那么下一版本我們就來處理這一問題,我們將通過注解配置多個controller,並通過反射來進行加載。
    最后獻上福利,learn-2源碼(對應的master為完整項目):源碼



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM