首页 > 代码库 > 打造一款属于自己的web服务器——实现Session
打造一款属于自己的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()); } }
可以看出这两部分代码十分简单,下边看一下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方法的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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为完整项目):源码