打造一款属于自己的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为完整项目):源码

时间: 2024-10-24 13:18:58

打造一款属于自己的web服务器——实现Session的相关文章

打造一款属于自己的web服务器——最后的一点完善

上一篇我们通过反射实现了动态加载多个controller,就功能上来说整个项目已经基本上完成了,但是目前我们仍然还有一些问题,例如模板支持不好.很多配置信息硬编码不好修改.此外,我们预期的目标是实现一个可嵌入的jar,以实现web服务,而就目前而言明显是不行的.那么我们现在就来解决这些问题. 一.使用velocity拓展模板    想要实现一套完善的模板还是比较麻烦的,所以目前我们考虑使用java支持的模板来实现,目前比较常用的有Freemaker,Velocity等,因为比较熟悉velocit

打造一款属于自己的web服务器——从简单开始

距离开篇已经过了很久,期间完善了一下之前的版本,目前已经能够完好运行,基本上该有的功能都有了,此外将原来的测试程序改为示例项目,新项目只需按照示例项目结构实现controller和view即可,详情见: easy-httpserver. demo-httpsrever.    这次我们将首先来实现一个简单版本,该版本只包括一些基本的功能,后续版本也将在此基础上一步步改进. 一.准备工作 俗话说的好,工欲善其事,必先利其器.我们在开始开发之前应做好如下准备(真的很简单): java开发环境,IDE

打造一款属于自己的web服务器——配置controller

这天一热,胖子的的世界就是一片凄惨啊,随便动动身子,就跟洗个澡似得,心情固然烦躁,一烦躁就很难静下心来写东西了......所以这一段没咋用心写,就稍微水点吧,同时,我又打算要减肥了!>_<!. 上一次我们介绍了session的实现,使web服务器(现在总觉得准确来说应该叫可独立部署的web框架,称不上服务器)具备了基本功能,但是仔细一想就会发现一个严重的问题:每当实现一个新的controller,那么就需要在invokController方法里边增加判断,以便url能够找到对应controll

web服务器集群session同步

在做了web集群后,你肯定会首先考虑session同步问题,因为通过负载均衡后,同一个IP访问同一个页面会被分配到不同的服务器上,如果session不同步的话,一个登录用户,一会是登录状态,一会又不是登录状态.所以本文就根据这种情况给出三种不同的方法来解决这个问题: 一.利用数据库同步session 在做多服务器session同步时我没有用这种方法,如果非要用这种方法的话,我想过二种方法: 1,用一个低端电脑建个数据库专门存放web服务器的session,或者,把这个专门的数据库建在文件服务器上

Linux下四款Web服务器压力测试工具(http_load、webbench、ab、siege)介绍

一.http_load程序非常小,解压后也不到100Khttp_load以并行复用的方式运行,用以测试web服务器的吞吐量与负载.但是它不同于大多数压力测试工具,它可以以一个单一的进程运行,一般不会把客户机搞死.还可以测试HTTPS类的网站请求. 下载地址:http://soft.vpser.net/test/http_load/http_load-12mar2006.tar.gz 安装#tar zxvf http_load-12mar2006.tar.gz#cd http_load-12mar

Java 并发专题 : Executor详细介绍 打造基于Executor的Web服务器

适配器模式,将一个类的接口转换成客户希望的另外一个接口.Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. 应用场景:系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配.适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况. 代码实现: //Adapter.h #include "stdafx.h" #include <iostream> class Adaptee

几款Web服务器性能压力测试工具

一.http_load 程序非常小,解压后也不到100Khttp_load以并行复用的方式运行,用以测试web服务器的吞吐量与负载.但是它不同于大多数压力测试工具,它可以以一个单一的进程运行,一般不会把客户机搞死.还可以测试HTTPS类的网站请求.下载地址:http_load-12mar2006.tar.gz 安装很简单 #tar zxvf http_load-12mar2006.tar.gz #cd http_load-12mar2006 #make && make install 基本

九款Web服务器性能压力测试工具

一.http_load 程序非常小,解压后也不到100Khttp_load以并行复用的方式运行,用以测试web服务器的吞吐量与负载.但是它不同于大多数压力测试工具,它可以以一个单一的进程运行,一般不会把客户机搞死.还可以测试HTTPS类的网站请求.下载地址:http_load-12mar2006.tar.gz安装很简单 #tar zxvf http_load-12mar2006.tar.gz#cd http_load-12mar2006#make && make install 基本用法:

我叫Tomcat:一款web服务器

我叫Tomcat:一款web服务器 如何将我们的 Java 代码,运行在网络上,初学时,首先接触到的一般都是Servlet以及Jsp(或略过Jsp)而 Tomcat 就是这两者的容器,帮你处理动态网页部分 (一) 从哪来,到哪去? (1) Tomcat和它的小伙伴 JBoss:Redhat 红帽,支持所有的 JavaEE 规则,适合大型项目,收费 Weblogic:Orcale,支持所有 JavaEE 规则,适合大型项目,收费 Websphere:IBM,支持所有 JavaEE 规则,适合大型项