本步骤将实现对 HTTP Session 和 Cookie 的支持,以便维护客户端的会话状态,使每次请求能够识别为同一客户端并跟踪状态。我们将实现一个计数器 Servlet,用于记录每个客户端的访问次数。
6.1 功能目标
6.2 代码结构
更新后的 MiniTomcat 代码结构,新增了 CustomHttpSession
、SessionManager
、HttpRequestParser
相关类,以及 CounterServlet
示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| MiniTomcat ├─ src │ ├─ main │ │ ├─ java │ │ │ ├─ com.daicy.minitomcat │ │ │ │ ├─ servlet │ │ │ │ │ ├─ CustomServletOutputStream.java // 自定义的 Servlet 输出流类 │ │ │ │ │ ├─ CustomHttpSession.java // 自定义的 HttpSession │ │ │ │ │ ├─ HttpServletRequestImpl.java // HTTP 请求的实现类 │ │ │ │ │ ├─ HttpServletResponseImpl.java // HTTP 响应的实现类 │ │ │ │ │ ├─ ServletConfigImpl.java // Servlet 配置的实现类 │ │ │ │ │ ├─ ServletContextImpl.java // Servlet 上下文的实现类 │ │ │ │ ├─ CounterServlet.java // session功能 Servlet 示例类 │ │ │ │ ├─ HelloServlet.java // Servlet 示例类 │ │ │ │ ├─ HttpConnector.java // 连接器类 │ │ │ │ ├─ HttpProcessor.java // 请求处理器 │ │ │ │ ├─ HttpServer.java // 主服务器类 │ │ │ │ ├─ HttpRequestParser.java // HttpRequest信息解析类 │ │ │ │ ├─ ServletLoader.java // Servlet 加载器 │ │ │ │ ├─ ServletProcessor.java // Servlet 处理器 │ │ │ │ ├─ StaticResourceProcessor.java// 静态资源处理器 │ │ │ │ ├─ SessionManager.java // SessionManager │ │ │ │ ├─ WebXmlServletContainer.java // Servlet 容器相关类 │ │ ├─ resources │ │ │ ├─ webroot │ │ │ │ ├─ index.html │ │ │ ├─ web.xml │ ├─ test ├─ pom.xml
|
6.3 代码实现
6.3.1 创建 HttpSession
类
HttpSession
类负责管理每个客户端的会话数据,并为每个会话分配唯一的 Session ID
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| package com.daicy.minitomcat.servlet;
import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import java.util.*;
public class CustomHttpSession implements HttpSession {
private String id; private Date creationTime; private Date lastAccessedTime; private int maxInactiveInterval; private Map<String, Object> attributes = new HashMap<>();
public CustomHttpSession(String sessionId) { this.id = sessionId; this.creationTime = new Date(); this.lastAccessedTime = new Date(); this.maxInactiveInterval = 1800; }
@Override public String getId() { return id; }
@Override public long getCreationTime() { return creationTime.getTime(); }
@Override public long getLastAccessedTime() { return lastAccessedTime.getTime(); }
@Override public ServletContext getServletContext() { return null; }
@Override public void setMaxInactiveInterval(int interval) { this.maxInactiveInterval = interval; }
@Override public int getMaxInactiveInterval() { return maxInactiveInterval; }
@Override public javax.servlet.http.HttpSessionContext getSessionContext() { return null; }
@Override public Object getAttribute(String name) { return attributes.get(name); }
@Override public Object getValue(String name) { return null; }
@Override public Enumeration<String> getAttributeNames() { return new Enumeration<String>() { private final Iterator<String> iterator = attributes.keySet().iterator();
@Override public boolean hasMoreElements() { return iterator.hasNext(); }
@Override public String nextElement() { return iterator.next(); } }; }
@Override public String[] getValueNames() { return new String[0]; }
@Override public void setAttribute(String name, Object value) { attributes.put(name, value); }
@Override public void putValue(String name, Object value) {
}
@Override public void removeAttribute(String name) { attributes.remove(name); }
@Override public void removeValue(String name) {
}
@Override public void invalidate() { attributes.clear(); }
@Override public boolean isNew() { long timeDiff = getLastAccessedTime() - getCreationTime(); return timeDiff < 1000; }
public boolean isExpired() { long currentTime = System.currentTimeMillis(); return (currentTime - lastAccessedTime.getTime()) > (maxInactiveInterval * 1000L); }
public void updateLastAccessedTime() { this.lastAccessedTime = new Date(); } }
|
6.3.2 创建 SessionManager
类
SessionManager
类用于管理存储 Session 信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.daicy.minitomcat;
import com.daicy.minitomcat.servlet.CustomHttpSession;
import java.util.HashMap; import java.util.Map; import java.util.UUID;
public class SessionManager { private static final Map<String, CustomHttpSession> sessions = new HashMap<>();
public static CustomHttpSession getSession(String sessionId) { CustomHttpSession session = sessions.get(sessionId); if (session != null) { session.updateLastAccessedTime(); } return session; }
public static CustomHttpSession createSession() { String sessionId = UUID.randomUUID().toString(); CustomHttpSession session = new CustomHttpSession(sessionId); sessions.put(sessionId, session); return session; }
public static CustomHttpSession getOrCreateSession(String sessionId) { CustomHttpSession session = sessions.get(sessionId); if (session == null) { session = createSession(); } session.updateLastAccessedTime(); return session; }
public static void invalidateSession(String sessionId) { sessions.remove(sessionId); } }
|
6.3.3 修改 HttpServletRequest
和 HttpServletResponse
支持 Session 和 Cookie
在 HttpServletRequest
中添加获取 Session
和解析请求中 Cookie
的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| public HttpServletRequestImpl(String method, String requestURI, String queryString, Map<String, String> headers) { this.method = method; this.requestURI = requestURI; this.queryString = queryString; this.headers = headers;
if (queryString != null) { String[] pairs = queryString.split("&"); for (String pair : pairs) { String[] keyValue = pair.split("="); if (keyValue.length == 2) { parameters.put(keyValue[0], new String[]{keyValue[1]}); } } }
String cookieHeader = headers.get("Cookie"); if (cookieHeader != null) { String[] cookiePairs = cookieHeader.split("; "); for (String cookiePair : cookiePairs) { String[] keyValue = cookiePair.split("="); if (keyValue.length == 2) { Cookie cookie = new Cookie(keyValue[0], keyValue[1]); cookies.add(cookie); if ("JSESSIONID".equals(cookie.getName())) { session = SessionManager.getOrCreateSession(cookie.getValue()); } } } } if (session == null) { session = SessionManager.createSession(); cookies.add(new Cookie("JSESSIONID", session.getId())); } }
@Override public HttpSession getSession() { return session; }
@Override public HttpSession getSession(boolean create) { if (session == null && create) { session = SessionManager.createSession(); cookies.add(new Cookie("JSESSIONID", session.getId())); } return session; }
@Override public String getRequestedSessionId() { return this.sessionId; }
@Override public boolean isRequestedSessionIdValid() { if (sessionId == null) return false; HttpSession existingSession = SessionManager.getSession(sessionId); return existingSession != null && !((CustomHttpSession) existingSession).isExpired(); }
@Override public boolean isRequestedSessionIdFromCookie() { return this.sessionIdFromCookie; }
@Override public boolean isRequestedSessionIdFromURL() { return !this.sessionIdFromCookie; }
@Override public String changeSessionId() { if (session == null) { getSession(true); } String newSessionId = UUID.randomUUID().toString();
if (sessionId != null) { SessionManager.invalidateSession(sessionId); }
sessionId = newSessionId; sessionIdChanged = true; return sessionId; }
public boolean isSessionIdChanged() { return sessionIdChanged; }
|
在 HttpServletResponse
中添加设置 Cookie
的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| package server;
import java.util.ArrayList; import java.util.List;
public class HttpServletResponse { private List<Cookie> cookies = new ArrayList<>();
public void addCookie(Cookie cookie) { cookies.add(cookie); }
public void sendResponse() throws IOException { writer.flush(); setCharacterEncoding(characterEncoding); if(null == getContentType()){ setContentType("text/html; charset=UTF-8"); } if(null == getHeader("Content-Length")){ setContentLength(body.size()); } PrintWriter responseWriter = new PrintWriter(new OutputStreamWriter(outputStream,characterEncoding));
responseWriter.printf("HTTP/1.1 %d %s\r\n", statusCode, statusMessage);
for (Map.Entry<String, List<String>> entry : headers.entrySet()) { String headerName = entry.getKey(); for (String headerValue : entry.getValue()) { responseWriter.printf("%s: %s\r\n", headerName, headerValue); } } for (Cookie cookie : cookies) { StringBuilder cookieHeader = new StringBuilder(); cookieHeader.append(cookie.getName()).append("=").append(cookie.getValue()); if (cookie.getMaxAge() > 0) { cookieHeader.append("; Max-Age=").append(cookie.getMaxAge()); } if (cookie.getPath() != null) { cookieHeader.append("; Path=").append(cookie.getPath()); } if (cookie.getDomain() != null) { cookieHeader.append("; Domain=").append(cookie.getDomain()); } responseWriter.printf("Set-Cookie: %s\r\n", cookieHeader.toString()); }
responseWriter.print("\r\n"); responseWriter.flush();
body.writeTo(outputStream);
responseWriter.flush();
} }
|
6.3.4 实现 CounterServlet
类
CounterServlet
是一个简单的计数器 Servlet,用于测试 Session 功能,每次访问该 Servlet 时,增加计数并返回当前计数值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| package com.daicy.minitomcat;
import com.daicy.minitomcat.servlet.HttpServletResponseImpl;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.IOException;
public class CounterServlet implements Servlet { @Override public void init(ServletConfig config) throws ServletException {
}
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponseImpl response = (HttpServletResponseImpl) res; HttpSession session = request.getSession(); Integer count = (Integer) session.getAttribute("count"); if (count == null) { count = 1; } else { count++; } session.setAttribute("count", count); response.getWriter().println("<html><body><h1>Visit Count: " + count + "</h1></body></html>"); }
@Override public String getServletInfo() { return ""; }
@Override public void destroy() {
} }
|
6.3.5 实现HttpRequestParser解析类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| package com.daicy.minitomcat;
import com.daicy.minitomcat.servlet.HttpServletRequestImpl;
import javax.servlet.http.Cookie; import java.io.*; import java.util.Enumeration; import java.util.HashMap; import java.util.Map;
public class HttpRequestParser { public static HttpServletRequestImpl parseHttpRequest(InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String requestLine = reader.readLine(); if (requestLine == null || requestLine.isEmpty()) { System.out.println(reader.readLine()); throw new IOException("Empty request line"); }
String[] parts = requestLine.split(" "); if (parts.length < 3) { throw new IOException("Invalid request line: " + requestLine); } String method = parts[0]; String uri = parts[1]; int queryIndex = uri.indexOf('?'); String requestURI = (queryIndex >= 0) ? uri.substring(0, queryIndex) : uri; String queryString = (queryIndex >= 0) ? uri.substring(queryIndex + 1) : null;
Map<String, String> headers = new HashMap<>(); String line; while ((line = reader.readLine()) != null && !line.isEmpty()) { int separatorIndex = line.indexOf(": "); if (separatorIndex != -1) { String headerName = line.substring(0, separatorIndex); String headerValue = line.substring(separatorIndex + 2); headers.put(headerName, headerValue); } }
return new HttpServletRequestImpl(method, requestURI, queryString, headers); }
public static void main(String[] args) throws IOException { String httpRequest = "GET /hello?name=world HTTP/1.1\r\n" + "Host: localhost\r\n" + "User-Agent: TestAgent\r\n" + "Accept: */*\r\n" + "Cookie: sessionId=abc123; theme=light\r\n\r\n"; InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes());
HttpServletRequestImpl request = parseHttpRequest(inputStream);
System.out.println("Method: " + request.getMethod()); System.out.println("Request URI: " + request.getRequestURI()); System.out.println("Query String: " + request.getQueryString()); System.out.println("Session ID: " + request.getSession().getId()); System.out.println("Cookies:"); for (Cookie cookie : request.getCookies()) { System.out.println(" " + cookie.getName() + "=" + cookie.getValue()); } } }
|
6.4 测试
启动服务器并访问 http://localhost:8080/counter
。
第一次访问时,页面将显示访问计数 1
,并在响应头中设置 JSESSIONID
Cookie。
刷新页面后,计数器将继续增加,展示会话管理的效果。
6.5 学习收获
Session 管理:学习了如何通过 Session ID 管理用户会话,理解了客户端会话状态的存储。
Cookie 使用:掌握了使用 Cookie 在客户端和服务器间传递信息的方法。
Servlet 状态维护:实现了服务器与客户端间的状态管理基础,为后续实现更复杂的功能打下基础。
项目源代码地址:
https://github.com/daichangya/MiniTomcat/tree/chapter6/mini-tomcat