打造一款属于本身的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为完整项目):源码
相关文章
相关标签/搜索