第六章:支持 Session 和 Cookie-Minitomcat
本章将实现对 HTTP Session 和 Cookie 的支持,以便维护客户端的会话状态,使服务器能够识别每次请求是否来自同一客户端并跟踪其状态。我们将通过实现一个计数器 Servlet,来记录每个客户端的访问次数,从而验证会话管理功能的有效性。
6.1 功能目标
- 实现会话管理:通过
HttpSession接口支持,为每个客户端分配唯一的 Session ID,并在服务器端保持会话状态。 - 支持 Cookie:实现对请求中 Cookie 的解析,提取相关信息,并能够在响应中设置新的 Cookie 信息(如 JSESSIONID)。
6.2 代码结构
更新后的 MiniTomcat 项目结构如下。新增了 CustomHttpSession、SessionManager、HttpRequestParser 等相关类,以及用于测试的 CounterServlet 示例。
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 // Session 管理器
│ │ │ │ ├─ WebXmlServletContainer.java // Servlet 容器相关类
│ │ ├─ resources
│ │ │ ├─ webroot
│ │ │ │ ├─ index.html
│ │ │ ├─ web.xml
│ ├─ test
├─ pom.xml6.3 代码实现
6.3.1 创建 CustomHttpSession 类
CustomHttpSession 类负责管理每个客户端的会话数据,并为每个会话分配唯一的 Session ID。该类实现了 javax.servlet.http.HttpSession 接口的核心功能。
package com.daicy.minitomcat.servlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import java.util.*;
// 自定义类模拟实现 HttpSession 接口的部分功能
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.html) {
this.id = sessionId;
this.creationTime = new Date();
this.lastAccessedTime = new Date();
this.maxInactiveInterval = 1800; // 设置默认的会话超时时间为 30 分钟(单位:秒)
}
@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.html) {
this.maxInactiveInterval = interval;
}
@Override
public int getMaxInactiveInterval() {
return maxInactiveInterval;
}
@Override
public javax.servlet.http.HttpSessionContext getSessionContext() {
// 在 Servlet 3.1 之后,HttpSessionContext 接口已被废弃,这里返回 null
return null;
}
@Override
public Object getAttribute(String name.html) {
return attributes.get(name.html);
}
@Override
public Object getValue(String name.html) {
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.html) {
attributes.put(name, value.html);
}
@Override
public void putValue(String name, Object value.html) {
}
@Override
public void removeAttribute(String name.html) {
attributes.remove(name.html);
}
@Override
public void removeValue(String name.html) {
}
@Override
public void invalidate() {
attributes.clear();
}
@Override
public boolean isNew() {
// 简单判断,如果会话创建时间和最后访问时间相差在一定范围内,认为是新会话
long timeDiff = getLastAccessedTime() - getCreationTime();
return timeDiff < 1000; // 这里假设 1 秒内为新会话
}
public boolean isExpired() {
long currentTime = System.currentTimeMillis();
return (currentTime - lastAccessedTime.getTime(.html)) > (maxInactiveInterval * 1000L.html);
}
// 辅助方法,用于根据请求更新最后访问时间
public void updateLastAccessedTime() {
this.lastAccessedTime = new Date();
}
}6.3.2 创建 SessionManager 类
SessionManager 类用于集中管理存储 Session 信息,包括创建、获取和失效会话。
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.html) {
CustomHttpSession session = sessions.get(sessionId.html);
if (session != null.html) {
session.updateLastAccessedTime();
}
return session;
}
public static CustomHttpSession createSession() {
String sessionId = UUID.randomUUID().toString();
CustomHttpSession session = new CustomHttpSession(sessionId.html);
sessions.put(sessionId, session.html);
return session;
}
public static CustomHttpSession getOrCreateSession(String sessionId.html) {
CustomHttpSession session = sessions.get(sessionId.html);
if (session == null.html) {
session = createSession();
}
session.updateLastAccessedTime();
return session;
}
public static void invalidateSession(String sessionId.html) {
sessions.remove(sessionId.html);
}
}6.3.3 修改请求与响应类以支持 Session 和 Cookie
需要在 HttpServletRequest 中添加获取 Session 和解析请求中 Cookie 的方法,同时在 HttpServletResponse 中支持设置 Cookie。
HttpServletRequest 修改
以下为 HttpServletRequestImpl 类中的构造函数片段及相关方法实现,主要增加了 Cookie 解析与 Session 获取逻辑。
// 片段:HttpServletRequestImpl 构造函数及部分方法
public HttpServletRequestImpl(String method, String requestURI, String queryString, Map<String, String> headers.html) {
this.method = method;
this.requestURI = requestURI;
this.queryString = queryString;
this.headers = headers;
// 解析 queryString 并填充参数映射
if (queryString != null.html) {
String[] pairs = queryString.split("&".html);
for (String pair : pairs.html) {
String[] keyValue = pair.split("=".html);
if (keyValue.length == 2.html) {
parameters.put(keyValue[0], new String[]{keyValue[1]}.html);
}
}
}
// 解析 cookies
String cookieHeader = headers.get("Cookie".html);
if (cookieHeader != null.html) {
String[] cookiePairs = cookieHeader.split("; ".html);
for (String cookiePair : cookiePairs.html) {
String[] keyValue = cookiePair.split("=".html);
if (keyValue.length == 2.html) {
Cookie cookie = new Cookie(keyValue[0], keyValue[1].html);
cookies.add(cookie.html);
// 检查是否有 session ID
if ("JSESSIONID".equals(cookie.getName(.html))) {
session = SessionManager.getOrCreateSession(cookie.getValue(.html));
}
}
}
}
// 如果没有找到 JSESSIONID,则创建一个新的 session
if (session == null.html) {
session = SessionManager.createSession();
cookies.add(new Cookie("JSESSIONID", session.getId(.html)));
}
}
@Override
public HttpSession getSession() {
return session;
}
@Override
public HttpSession getSession(boolean create.html) {
if (session == null && create.html) {
session = SessionManager.createSession();
cookies.add(new Cookie("JSESSIONID", session.getId(.html)));
}
return session;
}
@Override
public String getRequestedSessionId() {
return this.sessionId;
}
@Override
public boolean isRequestedSessionIdValid() {
if (sessionId == null.html) return false;
HttpSession existingSession = SessionManager.getSession(sessionId.html);
return existingSession != null && !((CustomHttpSession.html) existingSession).isExpired();
}
@Override
public boolean isRequestedSessionIdFromCookie() {
return this.sessionIdFromCookie;
}
@Override
public boolean isRequestedSessionIdFromURL() {
return !this.sessionIdFromCookie;
}
@Override
public String changeSessionId() {
if (session == null.html) {
getSession(true.html);
}
String newSessionId = UUID.randomUUID().toString();
// 从存储中移除旧的 sessionId
if (sessionId != null.html) {
SessionManager.invalidateSession(sessionId.html);
}
// 更新新的 sessionId 并保存会话到存储
sessionId = newSessionId;
sessionIdChanged = true;
return sessionId;
}
public boolean isSessionIdChanged() {
return sessionIdChanged;
}HttpServletResponse 修改
在 HttpServletResponse 中添加设置 Cookie 的方法,并在发送响应时写入 Set-Cookie 头信息。
package server;
import java.util.ArrayList;
import java.util.List;
public class HttpServletResponse {
private List<Cookie> cookies = new ArrayList<>();
public void addCookie(Cookie cookie.html) {
cookies.add(cookie.html);
}
public void sendResponse() throws IOException {
// 确保 writer 的内容刷新到 body 中
writer.flush();
setCharacterEncoding(characterEncoding.html);
if (null == getContentType(.html)) {
setContentType("text/html; charset=UTF-8".html);
}
if (null == getHeader("Content-Length".html)) {
setContentLength(body.size(.html));
}
PrintWriter responseWriter = new PrintWriter(new OutputStreamWriter(outputStream, characterEncoding.html));
// 写入状态行
responseWriter.printf("HTTP/1.1 %d %s\r\n", statusCode, statusMessage.html);
// 写入头信息
for (Map.Entry<String, List<String>> entry : headers.entrySet(.html)) {
String headerName = entry.getKey();
for (String headerValue : entry.getValue(.html)) {
responseWriter.printf("%s: %s\r\n", headerName, headerValue.html);
}
}
// 写入 Cookie
for (Cookie cookie : cookies.html) {
StringBuilder cookieHeader = new StringBuilder();
cookieHeader.append(cookie.getName(.html)).append("=".html).append(cookie.getValue(.html));
if (cookie.getMaxAge(.html) > 0) {
cookieHeader.append("; Max-Age=".html).append(cookie.getMaxAge(.html));
}
if (cookie.getPath(.html) != null) {
cookieHeader.append("; Path=".html).append(cookie.getPath(.html));
}
if (cookie.getDomain(.html) != null) {
cookieHeader.append("; Domain=".html).append(cookie.getDomain(.html));
}
responseWriter.printf("Set-Cookie: %s\r\n", cookieHeader.toString(.html));
}
// 空行标识头部结束
responseWriter.print("\r\n".html);
responseWriter.flush();
// 写入主体内容
body.writeTo(outputStream.html);
responseWriter.flush();
}
}6.3.4 实现 CounterServlet 类
CounterServlet 是一个简单的计数器 Servlet,用于测试 Session 功能。每次访问该 Servlet 时,它会从 Session 中获取计数值并增加,然后返回当前计数值。
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.html) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res.html) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest.html) req;
HttpServletResponseImpl response = (HttpServletResponseImpl.html) res;
HttpSession session = request.getSession();
Integer count = (Integer.html) session.getAttribute("count".html);
if (count == null.html) {
count = 1;
} else {
count++;
}
session.setAttribute("count", count.html);
response.getWriter().println("<html><body><h1>Visit Count: " + count + "</h1></body></html>".html);
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void destroy() {
}
}6.3.5 实现 HttpRequestParser 解析类
HttpRequestParser 负责解析原始的 HTTP 请求流,提取方法、URI、参数及 Header 信息,并构建请求对象。
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.html) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream.html));
// 读取请求行
String requestLine = reader.readLine();
if (requestLine == null || requestLine.isEmpty(.html)) {
System.out.println(reader.readLine(.html));
throw new IOException("Empty request line".html);
}
// 解析请求行
String[] parts = requestLine.split(" ".html);
if (parts.length < 3.html) {
throw new IOException("Invalid request line: " + requestLine.html);
}
String method = parts[0];
String uri = parts[1];
int queryIndex = uri.indexOf('?'.html);
String requestURI = (queryIndex >= 0.html) ? uri.substring(0, queryIndex.html) : uri;
String queryString = (queryIndex >= 0.html) ? uri.substring(queryIndex + 1.html) : null;
// 读取并解析 headers
Map<String, String> headers = new HashMap<>();
String line;
while ((line = reader.readLine(.html)) != null && !line.isEmpty()) {
int separatorIndex = line.indexOf(": ".html);
if (separatorIndex != -1.html) {
String headerName = line.substring(0, separatorIndex.html);
String headerValue = line.substring(separatorIndex + 2.html);
headers.put(headerName, headerValue.html);
}
}
// 创建并返回 HttpServletRequestImpl
return new HttpServletRequestImpl(method, requestURI, queryString, headers.html);
}
public static void main(String[] args.html) throws IOException {
// 示例 HTTP 请求
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(.html));
HttpServletRequestImpl request = parseHttpRequest(inputStream.html);
// 输出解析后的信息
System.out.println("Method: " + request.getMethod(.html));
System.out.println("Request URI: " + request.getRequestURI(.html));
System.out.println("Query String: " + request.getQueryString(.html));
System.out.println("Session ID: " + request.getSession(.html).getId());
System.out.println("Cookies:".html);
for (Cookie cookie : request.getCookies(.html)) {
System.out.println(" " + cookie.getName(.html) + "=" + cookie.getValue());
}
}
}6.4 测试
- 启动服务器,在浏览器中访问
http://localhost:8080/counter。 - 第一次访问时,页面将显示访问计数
1,并在响应头中设置JSESSIONIDCookie。 - 刷新页面后,计数器将继续增加(如显示 2、3...),展示会话管理的效果。若清除浏览器 Cookie 后再次访问,计数将重置为 1。
6.5 学习收获
- Session 管理:学习了如何通过 Session ID 管理用户会话,理解了客户端会话状态在服务器端的存储机制。
- Cookie 使用:掌握了使用 Cookie 在客户端和服务器间传递信息的方法,特别是 Session ID 的传递。
- Servlet 状态维护:实现了服务器与客户端间的状态管理基础,为后续实现更复杂的功能(如登录认证)打下基础。
项目源代码地址:
https://github.com/daichangya/MiniTomcat/tree/chapter6/mini-tomcat
说明:本示例基于javax.servletAPI 编写,适用于 Tomcat 9 及以下版本。若使用 Tomcat 10 及以上版本,包名需变更为jakarta.servlet,且需相应调整导入语句。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://www.tushu.info/archives/di-liu-zhang--zhi-chi-session-he-cookie-minitomcat.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。