上一次我們已經實現了一個簡單的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()); } }
可以看出這兩部分代碼十分簡單,下邊看一下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); }
最后看一下定時任務的實現:

/** * 定時清理過期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結束"); } }
此次改動的代碼就這么多。
四、測試
下邊我們來測試一下是否有效。由於目前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為完整項目):源碼