Java实现简单HTTP服务器+Servlet容器(缩减版)

0. 项目展示

实现用户登录:
请求 profile-action 时会根据请求头中的 cookie 信息和 session 服务器中的信息进行比对,如果匹配到则返回当前登录的用户信息,如果没有则临时重定向到登录页面。

当未登录时(即cookie中并没有信息时)请求 /profile-action :
在这里插入图片描述
对应的 servlet :

package com.zxf.webapps.dictionary;

import com.zxf.standard.ServletException;
import com.zxf.standard.http.HttpServlet;
import com.zxf.standard.http.HttpServletRequest;
import com.zxf.standard.http.HttpServletResponse;
import com.zxf.standard.http.HttpSession;

import java.io.IOException;

public class ProfileActionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        User user = (User) session.getAttribute("user");
        if (user == null) {
            resp.sendRedirect("login.html");
        } else {
            resp.setContentType("text/plain");
            resp.getWriter().println(user.toString());
        }
    }
}
package com.zxf.webapps.dictionary;

import com.zxf.standard.ServletException;
import com.zxf.standard.http.HttpServlet;
import com.zxf.standard.http.HttpServletRequest;
import com.zxf.standard.http.HttpServletResponse;
import com.zxf.standard.http.HttpSession;

import java.io.IOException;

public class LoginActionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        if (username.equals("ZXF") && password.equals("123")) {
            User user = new User(username, password);
            HttpSession session = req.getSession();
            session.setAttribute("user", user);

            resp.sendRedirect("profile-action");
        } else {
            resp.sendRedirect("login.html");
        }
    }
}

输入 用户名:ZXF 和 密码:123
在这里插入图片描述
重定向到了 /profile-action
在这里插入图片描述
这时再访问 /profile-action
在这里插入图片描述
可以看到并没有发生重定向,查看 cookie
在这里插入图片描述
cookie 中已经有 session-id ,所以服务器通过这个cookie可以知道是谁再访问即不再重定向。

1. 项目简介

该项目实现了HTTP服务器+servlet容器的缩减版,其功能相当于 Tomcat 的缩减版,可以实现动态资源的访问。
目前只支持 HTTP1.0 的版本,即在一个TCP连接上只可以传送一个HTTP请求和响应。
并且只支持 GET 方法。
字符集编码固定为 UTF-8。

2. 项目前置知识

2.1 Http协议

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写。
是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。
HTTP协议是应用层协议,即需要开发者编程实现的协议。
HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

作为 HTTP 服务器需要:

  • 解析HTTP请求
  • 组装HTTP响应

所以要熟悉请求和响应的格式:

请求的格式:
在这里插入图片描述
相应的格式:
在这里插入图片描述

HTTP content-type
Content-Type(内容类型),一般是指网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,Content-Type 标头告诉客户端实际返回的内容的内容类型。

语法格式:

Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something

HTTP状态码
当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码的信息头(server header)用以响应浏览器的请求。
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误

2.2 TCP协议

HTTP协议是基于TCP协议基础上工作的,因为TCP提供了可靠性传输。
TCP有三个阶段:

  • 握手阶段
  • 正常通信阶段
  • 挥手阶段

作为应用层的协议我们只关心正常通信阶段。
TCP协议作为传输层协议,向应用层进行数据交付,是根据端口(port)进行数据交付。
一个端口只能存在与一个进程中,TCP可以向进程进行数据交互。

2.3 Socket编程

Socket的英文原义是“孔”或“插座”。在网络编程中,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

Socket套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

基于java的socket网络编程实现

Server端Listen监听某个端口是否有连接请求,Client端向Server 端发出连接请求,Server端向Client端发回Accept接受消息。这样一个连接就建立起来了。Server端和Client端都可以通过Send,Write等方法与对方通信。

对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤:

1、创建Socket;

2、 打开连接到Socket的输入/出流;

3、按照一定的协议对Socket进行读/写操作;

4、关闭Socket。

具体实例请看我的另一篇博客:https://blog.csdn.net/qq_42843894/article/details/114793283

2.4 Java-IO

具体请看另一篇博客:
https://blog.csdn.net/qq_42843894/article/details/113764998

2.5 Tomcat 相关

Tomcat 就是一个所谓的 Web Container,内部实现了一个 HTTP 服务器
同时会根据不同的 URL,区分出是静态内容还是动态内容
如果是动态内容,则根据 web.xml 中的配置,找到合适的对象进行处理
我们自己写的代码只是这个环节中的一个步骤,不再需要从 main 入口开始实现了
我们最终文件夹结构要按照之前的方式布局,这样 tomcat 才可以正确的找到对应的文件
我们在 Servlet 中写的代码,其实都是在一个多线程环境下运行的,要注意保护线程安全问题
每个 Servlet 对象,在其生命过程中, init() 在启动时被调用一次,destroy() 在退出时被调用一次,service() 在每次请求的处理过程中都会调用一次

详细内容看我的另一篇博客:
https://blog.csdn.net/qq_42843894/article/details/114944027

2.6 servlet 相关

定位图:
在这里插入图片描述
详细内容看我的博客:
https://blog.csdn.net/qq_42843894/article/details/114983073
https://blog.csdn.net/qq_42843894/article/details/115049470

3. 项目总体设计

我们参考 Tomcat 的实现方式进行项目设计

项目总体分为三个包:

  1. com.zxf.standard.** --> 仿照Java官方的Servlet标准设计的缩减版标准(提供 interface + abs class)
  2. com.zxf.tomcat.** --> 实现上述标准,完成HTTP服务器 + servlet容器(这部分是项目的主要部分)
  3. com.zxf.webapps.** --> 存放web应用的目录,即类似Tomcat的webapps目录

如图:
在这里插入图片描述

4. 项目详细设计

4.1 com.zxf.standard 内容设计

总览:
在这里插入图片描述
关系图:
在这里插入图片描述

Servlet 接口
参考官方的设计:

在这里插入图片描述
我们暂时只实现部分方法:
只实现其中初始化,销毁,提供服务的方法
剩下的类和接口都是像这样只实现部分方法
在这里插入图片描述
ServletRequest 接口
这里暂时只实现获取指定值的方法

package com.zxf.standard;

public interface ServletRequest {

    String getParameter(String name);

}

ServletResponse 接口

package com.zxf.standard;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

public interface ServletResponse {
    OutputStream getOutputStream() throws IOException;

    PrintWriter getWriter() throws IOException;

    void setContentType(String type);
}

ServletException 类
异常类设计

package com.zxf.standard;


public class ServletException extends Exception {
    public ServletException() {
        super();
    }

    public ServletException(String message) {
        super(message);
    }

    public ServletException(String message, Throwable cause) {
        super(message, cause);
    }

    public ServletException(Throwable cause) {
        super(cause);
    }
}

Cookie 类

package com.zxf.standard.http;


public class Cookie {

    private String name;
    private String value;

    public String getName() {
        return name;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public Cookie(String name, String value) {
        this.name = name;
        this.value = value;
    }
}

HttpSession 接口

package com.zxf.standard.http;

public interface HttpSession {
    Object getAttribute(String name);

    void removeAttribute(String name);

    void setAttribute(String name, Object value);
}

HttpServlet 类

package com.zxf.standard.http;

import com.zxf.standard.Servlet;
import com.zxf.standard.ServletException;
import com.zxf.standard.ServletRequest;
import com.zxf.standard.ServletResponse;

import java.io.IOException;


public abstract class HttpServlet implements Servlet {
    @Override
    public void init() throws ServletException {

    }

    @Override
    public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
        if (req instanceof HttpServletRequest && resp instanceof HttpServletResponse) {
            HttpServletRequest httpReq = (HttpServletRequest)req;
            HttpServletResponse httpResp = (HttpServletResponse)resp;

            service(httpReq, httpResp);
        } else {
            throw new ServletException("不支持非 HTTP 协议");
        }
    }

    @Override
    public void destroy() {

    }

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (req.getMethod().equals("GET")) {
            doGet(req, resp);
        } else {
            resp.sendError(405);
        }
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.sendError(405);
    }
}

HttpServletRequest 接口

package com.zxf.standard.http;

import com.zxf.standard.ServletRequest;

public interface HttpServletRequest extends ServletRequest {
    Cookie[] getCookies();

    String getHeader(String name);

    String getMethod();

    String getContextPath();
    String getServletPath();
    String getRequestURI();

    HttpSession getSession();
}

HttpServletResponse 接口

package com.zxf.standard.http;

import com.zxf.standard.ServletResponse;

public interface HttpServletResponse extends ServletResponse {
    void addCookie(Cookie cookie);

    void sendError(int sc);

    void sendRedirect(String location);

    void setHeader(String name, String value);

    void setStatus(int sc);
}

4.2 com.zxf.webapps 内容设计

真实中的一个 webapp 是有哪些内容组成?

  1. 静态资源
  2. 配置文件 —> web.xml
  3. 自己写的Java代码
  4. 需要的第三方jar包 (暂时先不处理)

标准的目录结构:
在这里插入图片描述
这里我们自己定义结构:
在这里插入图片描述
将静态资源和配置文件统一放在外部的webapps下,这里为了简单实现基本功能不使用xml文件来解析,使用自定义的web.conf文件来解析 url 和 servlet 的对应关系

web.conf 配置规则如下:
我们可以编写自己的读取方式
在这里插入图片描述
servlet的内容就像平时一样编写
我们可以根据请求的路径拼接出资源的本地路径。

4.3 com.zxf.tomcat 内容设计 (重点设计)

这段内容的逻辑:

找到所有的 Servlet 对象,进行初始化
httpServlet.init();

处理服务器逻辑
while(true) {
	获取 HttpServletRequest 对象 req;
	实例化 HttpServletResponse 对象 resp;
	找到对应的,要处理请求的 HttpServlet 对象 httpServlet
	httpServlet.service(req, resp);
	组织响应格式,发送Http 响应;
}

HttpServer类设计详解

该类的main方法实现主要逻辑

public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, ServletException {
        // 1. 找到所有的 Servlet 对象,进行初始化
        initServer();

        // 2. 处理服务器逻辑
        startServer();

        // 3. 找到所有的 Servlet 对象,进行销毁
        destroyServer();
    }

4.3.1 initServer() 方法设计

其中的一个 context 意为一个web应用

private static void initServer() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, ServletException {
        // 第一步:扫描出所有的 context
        scanContexts();
        // 第二步:解析每个 Context 下的配置文件
        parseContextConf();
        // 第三步:加载每个 Servlet 类
        loadServletClasses();
        // 第四步:实例化每个 servlet 对象
        instantiateServletObjects();
        // 第五步:执行每个 servlet 对象的初始化
        initializeServletObjects();
    }

第一步:扫描出所有的 context
scanContexts();

   // 这里定义了web应用的资源绝对路径,用于查找时用
    public static final String WEBAPPS_BASE = "/home/distance/LinuxProject/div-httpServer/webapps";
    // 定义一个web应用的集合,里面存放所有扫描到的web应用
    // 这里把web应用抽象为一个类,便于管理
    public static final List<Context> contextList = new ArrayList<>();
    // 这里统一定义了配置文件解析器,在解析时调用该解析器解析配置文件
    private static final ConfigReader configReader = new ConfigReader();
    // 这里定义了一个默认的web应用,用来统一处理资源没有找到的情况
    public static final DefaultContext defaultContext = new DefaultContext(configReader);
    private static void scanContexts() {
        System.out.println("第一步:扫描出所有个 contexts");
        File webappsRoot = new File(WEBAPPS_BASE);
        File[] files = webappsRoot.listFiles();
        if (files == null) {
            throw new RuntimeException();
        }

        for (File file : files) {
            if (!file.isDirectory()) {
                // 不是目录,就不是 web 应用
                continue;
            }

            String contextName = file.getName();
            System.out.println(contextName);
            Context context = new Context(configReader, contextName);

            contextList.add(context);
        }
    }

第二步:解析每个 Context 下的配置文件
parseContextConf();

	private static void parseContextConf() throws IOException {
        System.out.println("第二步:解析每个 Context 下的配置文件");
        // 依次读取每个 web应用下的配置文件
        for (Context context : contextList) {
        // 调用context中的readConfigFile方法解析配置
        // 得到 url 和 servlet的对应关系
            context.readConfigFile();
        }
    }

第三步:加载每个 Servlet 类
loadServletClasses();

	private static void loadServletClasses() throws ClassNotFoundException {
        System.out.println("第三步:加载每个 Servlet 类");
        for (Context context : contextList) {
        // 依次调用 loadServletClasses 加载每个 context 的每个 Servlet 类
            context.loadServletClasses();
        }
    }

第四步:实例化每个 servlet 对象
instantiateServletObjects()

	private static void instantiateServletObjects() throws InstantiationException, IllegalAccessException {
        System.out.println("第四步:实例化每个 servlet 对象");
        for (Context context : contextList) {
            context.instantiateServletObjects();
        }
    }

第五步:执行每个 servlet 对象的初始化
initializeServletObjects()

	private static void initializeServletObjects() throws ServletException {
        System.out.println("第五步:执行每个 servlet 对象的初始化");
        for (Context context : contextList) {
            context.initServletObjects();
        }

		// defaultServlet 为请求资源为静态资源时的处理类
        defaultServlet.init();
        // notFoundServlet 为请求资源不存在的处理类
        notFoundServlet.init();
    }

context 类设计

package com.zxf.tomcat;

import com.zxf.standard.Servlet;
import com.zxf.standard.ServletException;

import java.io.IOException;
import java.util.*;

public class Context {
	// 配置文件解析器
    private final ConfigReader reader;
    // web应用名,即文件名
    private final String name;
    // 这个属性存储解析得到的配置内容,即 url 和 servlet的对应关系
    private Config config;
    // 每个 Context 有自己的类加载器
    // 平时写的 web 应用中的代码,都是由自己 Context 的类加载器进行加载,为了做到加载类的版本互不干扰
    // 利用反射拿到类加载器
    private final ClassLoader webappClassLoader = Context.class.getClassLoader();

    public Context(ConfigReader reader, String name) {
        this.reader = reader;
        this.name = name;
    }

    public String getName() {
        return name;
    }

	// 解析配置文件
    public void readConfigFile() throws IOException {
        this.config = reader.read(name);
    }

	// 定义一个类的集合,存储加载到的类信息
    List<Class<?>> servletClassList = new ArrayList<>();
    public void loadServletClasses() throws ClassNotFoundException {
        // 这里用set存储类的名称,防止加载相同的类
        Set<String> servletClassNames = new HashSet<>(config.servletNameToServletClassNameMap.values());
        for (String servletClassName : servletClassNames) {
            Class<?> servletClass = webappClassLoader.loadClass(servletClassName);
            // 将加载到的类加入集合中
            servletClassList.add(servletClass);
        }
    }

	// 定义一个 servlet 类的集合
    List<Servlet> servletList = new ArrayList<>();
    public void instantiateServletObjects() throws IllegalAccessException, InstantiationException {
        for (Class<?> servletClass : servletClassList) {
        	// 调用该类的无参构造方法,进行实例化对象
            Servlet servlet = (Servlet)servletClass.newInstance();  
            servletList.add(servlet);
        }
    }

	// 执行每个 servlet 的初始化方法
    public void initServletObjects() throws ServletException {
        for (Servlet servlet : servletList) {
            servlet.init();
        }
    }
	// 执行每个 servlet 的销毁方法
    public void destroyServlets() {
        for (Servlet servlet : servletList) {
            servlet.destroy();
        }
    }

	// 这里是通过 Request 中解析出来的 ServletPath 来找到 相应的servlet类
	// 没有找到则返回 null
    public Servlet get(String servletPath) {
        String servletName = config.urlToServletNameMap.get(servletPath);
        String servletClassName = config.servletNameToServletClassNameMap.get(servletName);
        for (Servlet servlet : servletList) {
        	// 通过反射拿到类名并进行比较
            String currentServletClassName = servlet.getClass().getCanonicalName();
            if (currentServletClassName.equals(servletClassName)) {
                return servlet;
            }
        }

        return null;
    }
}

Config 类设计
这个类定义了解析得到的配置内容,即 url 和 servlet的对应关系

package com.zxf.tomcat;

import java.util.LinkedHashMap;
import java.util.Map;

public class Config {
	// 定义两组对应关系
    public Map<String, String> servletNameToServletClassNameMap;
    // 这里并没有考虑多个url对应同一个servlet的情况,是一个缺陷
    public LinkedHashMap<String, String> urlToServletNameMap;

    public Config(Map<String, String> servletNameToServletClassNameMap, LinkedHashMap<String, String> urlToServletNameMap) {
        this.servletNameToServletClassNameMap = servletNameToServletClassNameMap;
        this.urlToServletNameMap = urlToServletNameMap;
    }
}

ConfigReader 类设计
用来读取配置文件的工具类
即建立自己的规则读取自定义的web.conf配置文件

package com.zxf.tomcat;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;

public class ConfigReader {
    public Config read(String name) throws IOException {
        Map<String, String> servletNameToServletClassNameMap = new HashMap<>();
        LinkedHashMap<String, String> urlToServletNameMap = new LinkedHashMap<>();

        String filename = String.format("%s/%s/WEB-INF/web.conf", HttpServer.WEBAPPS_BASE, name);

		// 这里标记读取的状态,表示当前读取的内容是什么
        String stage = "start"; // "servlets"/"mappings"

        // 进行文本文件内容的读取
        try (InputStream is = new FileInputStream(filename)) {
            Scanner scanner = new Scanner(is, "UTF-8");
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine().trim();
                if (line.isEmpty() || line.startsWith("#")) {
                    // 如果是空行、或者注释行,跳过
                    continue;
                }

                switch (stage) {
                    case "start":
                        if (line.equals("servlets:")) {
                            stage = "servlets";
                        }
                        break;
                    case "servlets":
                        if (line.equals("servlet-mappings:")) {
                            stage = "mappings";
                        } else {
                            // 进行 ServletName => ServletClassName 的解析
                            String[] parts = line.split("=");
                            String servletName = parts[0].trim();
                            String servletClassName = parts[1].trim();
                            servletNameToServletClassNameMap.put(servletName, servletClassName);
                        }
                        break;
                    case "mappings":
                        // 进行 URL => ServletName 的解析
                        String[] parts = line.split("=");
                        String url = parts[0].trim();
                        String servletName = parts[1].trim();
                        urlToServletNameMap.put(url, servletName);
                        break;
                }
            }
        }

        return new Config(servletNameToServletClassNameMap, urlToServletNameMap);
    }
}

4.3.2 startServer() 方法设计

利用Socket编程监听8080端口
每有一个TCP连接就提交一任务到线程池去处理 这次Http的请求和相应

	private static void startServer() throws IOException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ServerSocket serverSocket = new ServerSocket(8080);

        // 2. 每次循环,处理一个请求
        while (true) {
            Socket socket = serverSocket.accept();
            Runnable task = new RequestResponseTask(socket);
            threadPool.execute(task);
        }

    }

RequestResponseTask 的run()方法逻辑:

  1. 解析并得到请求对象
  2. 实例化一个响应对象
  3. 根据 request.getContextPath() 找到哪个 Context 进行处理
  4. 根据 request.getServletPath() 找到 Context 中的哪个 HttpServlet 进行处理
  5. 调用 servlet.service(request, response),交给业务处理
  6. 根据 response 对象中的数据,发送 HTTP 响应

在这里插入图片描述

package com.zxf.tomcat;

import com.zxf.standard.Servlet;
import com.zxf.standard.http.Cookie;
import com.zxf.tomcat.http.HttpRequestParser;
import com.zxf.tomcat.http.Request;
import com.zxf.tomcat.http.Response;

import java.io.*;
import java.net.Socket;
import java.util.Map;

public class RequestResponseTask implements Runnable {

	// 这里定义了一个 HttpRequest 的解析器
    private static final HttpRequestParser parser = new HttpRequestParser();

    private final Socket socket;

    public RequestResponseTask(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 1. 解析并得到请求对象
            Request request = parser.parse(socket.getInputStream());
            System.out.println(request);

            // 2. 实例化一个响应对象
            Response response = new Response();
            
            // 3. 根据 request.getContextPath() 找到哪个 Context 进行处理
            // 让 handleContext 先指向 没有找到时默认处理的webapp,如果找到就 重新指向对应的 webapp
            Context handleContext = HttpServer.defaultContext;
            for (Context context : HttpServer.contextList) {
                if (context.getName().equals(request.getContextPath())) {
                    handleContext = context;
                    break;
                }
            }
            
            // 4. 根据 request.getServletPath() 找到 Context 中的哪个 HttpServlet 进行处理
            Servlet servlet =  handleContext.get(request.getServletPath());
            // 如果没有找到相应的 servlet 则按照 静态资源处理
            if (servlet == null) {
                servlet = HttpServer.defaultServlet;
            }
            
            // 5. 调用 servlet.service(request, response),交给业务处理
            servlet.service(request, response);
            System.out.println(response);
            
           // 6. 根据 response 对象中的数据,发送 HTTP 响应
            sendResponse(socket.getOutputStream(), request, response);

            socket.close();
        } catch (Exception exc) {
            exc.printStackTrace(System.out);
        }
    }
	
	// 组装响应头信息
    private void sendResponse(OutputStream outputStream, Request request, Response response) throws IOException {
        // 保存 session
        // 1. 种 cookie
        // 2. 保存成本地文件
        if (request.session != null && !request.session.sessionData.isEmpty()) {
            Cookie cookie = new Cookie("session-id", request.session.sessionId);
            response.addCookie(cookie);
            request.session.saveSessionData();
        }

        Writer writer = new OutputStreamWriter(outputStream, "UTF-8");
        PrintWriter printWriter = new PrintWriter(writer);
        // 种 cookie
        for (Cookie cookie : response.cookieList) {
            response.setHeader("Set-Cookie", String.format("%s=%s", cookie.getName(), cookie.getValue()));
        }

        printWriter.printf("HTTP/1.0 %d\r\n", response.status);
        for (Map.Entry<String, String> entry : response.headers.entrySet()) {
            String name = entry.getKey();
            String value = entry.getValue();

            printWriter.printf("%s: %s\r\n", name, value);
        }
        printWriter.printf("\r\n");
        response.bodyPrintWriter.flush();
        response.bodyOutputStream.flush();
        printWriter.flush();

        byte[] bytes = response.bodyOutputStream.toByteArray();
        outputStream.write(bytes);
        outputStream.flush();
    }
}

Request 类设计

package com.zxf.tomcat.http;

import com.zxf.standard.http.Cookie;
import com.zxf.standard.http.HttpServletRequest;
import com.zxf.standard.http.HttpSession;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public class Request implements HttpServletRequest {
    private final String method;
    private final String requestURI;
    private final String contextPath;
    private final String servletPath;
    // 存储参数信息
    private final Map<String, String> parameters;
    // 存储请求头信息
    private final Map<String, String> headers;
    private final List<Cookie> cookieList;
    public HttpSessionImpl session = null;

    @Override
    public String toString() {
        return String.format("Request{%s %s %s %s %s}", method, requestURI, parameters, headers, session);
    }

    public Request(String method, String requestURI, String contextPath, String servletPath, Map<String, String> parameters, Map<String, String> headers, List<Cookie> cookieList) throws IOException, ClassNotFoundException {
        this.method = method;
        this.requestURI = requestURI;
        this.contextPath = contextPath;
        this.servletPath = servletPath;
        this.parameters = parameters;
        this.headers = headers;
        this.cookieList = cookieList;
        for (Cookie cookie : cookieList) {
        	// 如果此处的请求的cookie信息中含有session-id,就根据内容在session中查找并返回
            if (cookie.getName().equals("session-id")) {
                String sessionId = cookie.getValue();
                session = new HttpSessionImpl(sessionId);
                break;
            }
        }
    }

    @Override
    public Cookie[] getCookies() {
        return cookieList.toArray(new Cookie[0]);
    }

    @Override
    public String getHeader(String name) {
        return headers.get(name);
    }

    @Override
    public String getMethod() {
        return method;
    }

    @Override
    public String getContextPath() {
        return contextPath;
    }

    @Override
    public String getServletPath() {
        return servletPath;
    }

    @Override
    public String getRequestURI() {
        return requestURI;
    }

    @Override
    public HttpSession getSession() {
        if (session != null) {
            return session;
        }

        session = new HttpSessionImpl();
        return session;
    }

    @Override
    public String getParameter(String name) {
        return parameters.get(name);
    }
}

HttpRequestParser 类设计
进行请求解析

package com.zxf.tomcat.http;

import com.zxf.standard.http.Cookie;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.*;

public class HttpRequestParser {
    public Request parse(InputStream socketInputStream) throws IOException, ClassNotFoundException {
        Scanner scanner = new Scanner(socketInputStream, "UTF-8");

		// 拿到请求方法
        String method = scanner.next().toUpperCase();
        // 拿到请求路径
        String path = scanner.next();
        Map<String, String> parameters = new HashMap<>();
        String requestURI = path;
        // 用  ? 切分请求路径,拿到 requestURI,再用 & 切分参数部分拿到 parameters
        int i = path.indexOf("?");
        if (i != -1) {
            requestURI = path.substring(0, i);
            String queryString = path.substring(i + 1);
            for (String kv : queryString.split("&")) {
                String[] kvParts = kv.split("=");
                String name = URLDecoder.decode(kvParts[0].trim(), "UTF-8");
                String value = URLDecoder.decode(kvParts[1].trim(), "UTF-8");
                parameters.put(name, value);
            }
        }
        int j = requestURI.indexOf('/', 1);
        
        // 拿到 contextPath 和 servletPath
        String contextPath = "/";
        String servletPath = requestURI;
        if (j != -1) {
            contextPath = requestURI.substring(1, j);
            servletPath = requestURI.substring(j);
        }

		// 请求协议的版本部分
        String version = scanner.nextLine();

        Map<String, String> headers = new HashMap<>();
        List<Cookie> cookieList = new ArrayList<>();

		// 请求头解析
        String headerLine;
        while (scanner.hasNextLine() && !(headerLine = scanner.nextLine().trim()).isEmpty()) {
            String[] parts = headerLine.split(":");
            String name = parts[0].trim().toLowerCase();
            String value = parts[1].trim();
            headers.put(name, value);

			// 如果是 cookie 的信息则切分加入 cookie 信息中
            if (name.equals("cookie")) {
                String[] kvParts = value.split(";");
                for (String kvPart : kvParts) {
                    if (kvPart.trim().isEmpty()) {
                        continue;
                    }

                    String[] split = kvPart.split("=");
                    String cookieName = split[0].trim();
                    String cookieValue = split[1].trim();
                    Cookie cookie = new Cookie(cookieName, cookieValue);
                    cookieList.add(cookie);
                }
            }
        }

		// 解析结束后 返回一个 请求类
        return new Request(method, requestURI, contextPath, servletPath, parameters, headers, cookieList);
    }
}

Response 类设计

package com.zxf.tomcat.http;

import com.zxf.standard.http.Cookie;
import com.zxf.standard.http.HttpServletResponse;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Response implements HttpServletResponse {
    public int status = 200;
    public final List<Cookie> cookieList;
    // 存放响应头信息
    public final Map<String, String> headers;
    // 用于从 servlet 的读取相应的响应体内容
    public final PrintWriter bodyPrintWriter;
    // 用于写入 Socket 的缓冲中
    public final ByteArrayOutputStream bodyOutputStream;
    

    @Override
    public String toString() {
        return String.format("Response{%d %s %s}", status, headers, bodyOutputStream.toString());
    }

    public Response() throws UnsupportedEncodingException {
        cookieList = new ArrayList<>();
        headers = new HashMap<>();
        bodyOutputStream = new ByteArrayOutputStream(1024);
        Writer writer = new OutputStreamWriter(bodyOutputStream, "UTF-8");
        bodyPrintWriter = new PrintWriter(writer);
    }

    @Override
    public void addCookie(Cookie cookie) {
        cookieList.add(cookie);
    }

    @Override
    public void sendError(int sc) {
        // TODO
    }

    @Override
    public void sendRedirect(String location) {
        setStatus(307);
        setHeader("Location", location);
    }

	// 设置响应头
    @Override
    public void setHeader(String name, String value) {
        headers.put(name, value);
    }

    @Override
    public void setStatus(int sc) {
        status = sc;
    }

    // 写入响应体(byte)
    @Override
    public OutputStream getOutputStream() throws IOException {
        return bodyOutputStream;
    }

    // 写入响应体(text)
    @Override
    public PrintWriter getWriter() throws IOException {
        return bodyPrintWriter;
    }

    @Override
    public void setContentType(String type) {
        if (type.startsWith("text/")) {
            type = type + "; charset=utf-8";
        }
        setHeader("Content-Type", type);
    }
}

DefaultServlet 类设计
静态资源类设计

package com.zxf.tomcat.servlets;

import com.zxf.standard.ServletException;
import com.zxf.standard.http.HttpServlet;
import com.zxf.standard.http.HttpServletRequest;
import com.zxf.standard.http.HttpServletResponse;
import com.zxf.tomcat.HttpServer;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class DefaultServlet extends HttpServlet {
    private final String welcomeFile = "/index.html";
    private final Map<String, String> mime = new HashMap<>();
    private final String defaultContentType = "text/plain";

	// 初始化时定义 ContentType 的类型
    @Override
    public void init() throws ServletException {
        mime.put("htm", "text/html");
        mime.put("html", "text/html");
        mime.put("jpg", "image/jpeg");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("由我处理静态资源");
        String contextPath = req.getContextPath();
        String servletPath = req.getServletPath();

		// 如果servletPath 是 / 则自动转到 欢迎页面
        if (servletPath.equals("/")) {
            servletPath = welcomeFile;
        }

        String filename = String.format("%s/%s/%s", HttpServer.WEBAPPS_BASE, contextPath, servletPath);
        File file = new File(filename);
        if (!file.exists()) {
            // 404 的方式处理
            // req.getDispatcher().forward("404");
            // 转发给其他 servlet 进行处理,不经过 HTTP 协议
            HttpServer.notFoundServlet.service(req, resp);
            return;
        }

        String contentType = getContentType(servletPath);
        resp.setContentType(contentType);

        OutputStream outputStream = resp.getOutputStream();
        try (InputStream inputStream = new FileInputStream(file)) {
            byte[] buffer = new byte[1024];
            int len = -1;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            outputStream.flush();
        }
    }

    private String getContentType(String servletPath) {
        String contentType = defaultContentType;
        // 根据文件后缀名 设置 contentType信息
        int i = servletPath.lastIndexOf('.');
        if (i != -1) {
            String extension = servletPath.substring(i + 1);
            contentType = mime.getOrDefault(extension, defaultContentType);
        }

        return contentType;
    }
}

4.3.3 com.zxf.tomcat 内容总结

在这里插入图片描述
HttpRequestParser --> 负责从 socket.inputStream 解析出 Http 请求对象
HttpSessionImpl --> 实现保存文件中的 session 功能
DefaultServlet --> 处理静态资源
NotFoundServlet --> 处理404情况
Config、ConfigReader --> 负责解析 web.conf 并最终得到 url 和 servlet 的对应关系
Context、DefaultContext --> 每个 web应用都是一个 context 对象,DefaultContext 处理没有找到时的请求
HTTPServer --> 处理主要逻辑
ResponseRequestTask --> 一个请求–响应周期

5. 项目总结

该项目实现了基本的 Http服务器 和 servlet 容器功能,但相对与真实的 Tomcat 还有很多缺陷:

  1. 只支持 HTTP1.0 协议(一条TCP只处理一次请求响应周期)
  2. 只支持GET方法
  3. 字符集编码,固定成 UTF-8
  4. TCP server 是BIO形式,真实的 Tomcat 基本使用NIO、NIO2
  5. JDK提供的线程池也并不高效
  6. 只实现了部分的 servlet 标准
  7. 并没有设计特定的类加载器,没法做到每个webapp隔离引入第三方jar包
  8. 没有设计日志功能(不利于排错)

6. 源码地址

https://github.com/Madrid-7/LinuxProject/tree/main/div-httpServer

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值