打造一款屬於本身的web服務器——實現Session

    上一次咱們已經實現了一個簡單的web服務器版本,可以實現一些基本功能,可是在最後也提到了這個版本因爲不支持session並不能實現真正的動態交互,這一次咱們就來完成這一功能。 html

1、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會清除,不然你在過時時間內從新打開瀏覽器還可以繼續該會話,明顯是不合理(本版本就不考慮這個問題了)。

2、功能設計

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

3、實現代碼

    思路清晰,代碼實現就很是簡單了。這裏就再也不詳細介紹每部分代碼了,基本上看註釋就明白。
    首先看下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結束");
	}
}
    這次改動的代碼就這麼多。

4、測試

    下邊咱們來測試一下是否有效。因爲目前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測試方法同樣。 java

5、總結

    本次實現的功能應該說是點睛之筆,session的實現從根本上提供了動態交互的支持,如今咱們可以實現登錄之類的功能的。可是正如上邊提到的,如今整個項目還很死板,咱們目前只能使用一個controller ,想要實現多個則須要根據請求參數進行判斷,那麼下一版本咱們就來處理這一問題,咱們將經過註解配置多個controller,並經過反射來進行加載。
    最後獻上福利,learn-2源碼(對應的master爲完整項目):源碼
相關文章
相關標籤/搜索