轻量级 HTTP 服务器

项目简介

该项目简单的进行测试过,能够支持自己所写的小型 Servlet Web 项目的运行。

基本功能

根据官方 Servlet 标准实现的 Http 服务器的部分功能,包括对本地 Servlet 配置文件的读取与解析,并且将实现的 Servlet 类加载并实例化到内存中,对加载的 Servlet 类进行初始化。针对多个请求,开启多线程进行请求的处理,通过 Scoket 得到请求的内容,并且对其进行解析,将解析好的请求信息封装在一个 Request 对象中,同时实例化一个 Response 对象用于后续 Servlet 类的参数传递,通过 Request 中解析到的 ContextPath 去定位到由哪一个 webapps 下的项目进行处理,如果没有找到项目目录,那么就跳转到处理 NotFound 请求的 Servlet 类,如果定位到该项目,就通过 ServletPath 定位到该项目中的哪一个 Servlet 类去处理该请求,如果没有找到对应的 Servlet 说明可能有两种情况,其一是定位的是静态资源,其二是该 Servlet 确实不存在,均交由 DefaultServlet 去处理。找到 Servlet 后,调用该 Servlet 的 service 方法去执行实现类的 doGet 方法,最后对将执行完方法后的 Response 对象的内容按照响应体的格式写入到 Socket 的流中,发送给客户端。最终将实例化的 Servlet 对象均销毁。除此之外还实现了 Session 读取解析加载的功能。

目录结构

在这里插入图片描述

  1. Servlet 标准:官方制定的一些接口,后续只需要实现这些接口即可。
  2. Http 服务器:实现服务器功能的部分,从 HttpServer 为开启入口,开启服务器进行工作。
  3. 测试目录:在 webapps 下以项目名称命名项目包名,里面可以写一些 Servlet 类进行请求处理。

项目详解

主要从三大部分讲解:

  • 服务器的初始化工作
  • 处理服务器逻辑
  • 编写 Servlet 进行测试

初始化服务器

初始化服务器需要对本地 Conf 文件进行解析,首先展示配置文件的目录结构和格式

  • 目录结构:
    在这里插入图片描述
  • web.conf 文件结构:
servlets:
  # ServletName = ServletClassName
  TranslateServlet = com.fjf.webapps.dictionary.TranslateServlet
  LoginActionServlet = com.fjf.webapps.dictionary.LoginActionServlet
  ProfileActionServlet = com.fjf.webapps.dictionary.ProfileActionServlet

servlet-mappings:
  # URLPattern = ServletName
  /translate = TranslateServlet
  /login-action = LoginActionServlet
  /profile-action = ProfileActionServlet

将请求通过 servlet-mappings 去映射到需要加载的 Servlet 类的全限定类名,从而通过反射完成对类的加载。
了解了配置文件的结构后,我们将初始化服务器分为五步:

  1. 扫描 webapps 下的所有文件,每一个文件夹都是一个项目,对于每一个项目实例化一个 Context 对象保存信息
private static void scanContexts() {
    // 获取webapps文件夹
    File webappsRoot = new File(WEBAPPS_BASE);
    // 获取webapps文件夹下所有文件(每个 files[i] 都是一个项目)
    File[] files = webappsRoot.listFiles();
    // 如果为null,则抛出异常
    if (files == null) {
        throw new RuntimeException();
    }

    for (File file : files) {
        // 如果这个file不是目录,那么直接跳过
        if (!file.isDirectory()) {
            continue;
        }
        // 执行到这里说明是一个web应用项目

        // 获取当前应用(Context)名
        String contextName = file.getName();
        System.out.println(contextName);
        Context context = new Context(contextName);
        contextList.add(context);
    }
}
  1. 解析 /WEB-INF/web.conf 文件内容,并且将读取到的内容封装为一个Config 对象并且注入到 Context 中,首先看一下 Context 类的属性和 Config 类的属性,然后展示解析的代码
// Context 类部分属性
public class Context {
    private Config config;
    private String name;
    // web 应用中的所有类都使用这个类加载器加载。(每个 Context 都有属于自己的类加载器,互补干扰)
    private final ClassLoader webappClassLoader = Context.class.getClassLoader();
}
// Config 类的属性
public class Config {
    private Map<String, String> servletNameToServletClassPath;
    private  Map<String, String> urlToServletName;
}
// 解析 web.Conf 文件
private static void parseContextConf() {
   for (Context context : contextList) {
       // 根据 Context 获取项目名称,调用 ConfigReader.read(名称) 获取读取的 Config 类,并且注入给 Context 对象
       context.setConfig(ConfigReader.reader(context.getName()));
   }
}
public class ConfigReader {
    public static Config reader(String contextName){
    // 具体代码见 GitHub...
    }
}
  1. 加载 Servlet Class 类,每个 Context 都有属于自己的 ClassLoader,通过反射技术将
// 加载 Servlet Class 类
private static void loadServletClasses() throws ClassNotFoundException {
    for (Context context : contextList) {
        context.loadServletClasses();
    }
}
// 加载 ServletClass
public void loadServletClasses() throws ClassNotFoundException {
    // 使用 Context 自带的类加载器对 Config 中所有的 Servlet 路径的类进行加载
    Set<String> servletClassNames = new HashSet<>(config.getServletNameToServletClassPath().values());
    for (String servletClass : servletClassNames) {
        Class<?> aClass = webappClassLoader.loadClass(servletClass);
        classList.add(aClass);
    }
}
  1. 实例化 Servlet
// 实例化 Servlet 类
private static void instantiateServletObject() throws InstantiationException, IllegalAccessException {
    for (Context context : contextList) {
        context.instantiateServletObject();
    }
}
// 实例化加载的类,并且将实例化的类 放在 List 中
public List<Servlet> servletList = new ArrayList<>();
public void instantiateServletObject() throws IllegalAccessException, InstantiationException {
    for (Class<?> servletClass : classList) {
        Servlet instance = (Servlet) servletClass.newInstance(); // 默认调用无参构造方法
        servletList.add(instance);
    }
}
  1. 初始化 Servlet
// 初始化加载的 Servlet 类
public void initializeServletObject() throws ServletException {
   for (Servlet servlet : servletList) {
       servlet.init();
   }
}

处理服务器逻辑

以下为服务器处理请求的主体部分

// 2. 处理服务器逻辑
private static void startServer() throws IOException {
    // 多线程处理请求
    ExecutorService threadPool = Executors.newFixedThreadPool(10);

    ServerSocket serverSocket = new ServerSocket(8080);

    // 每次循环处理一次请求
    // 死循环,表示无限监听8080端口有没有请求,并处理
    while (true) {
        // 开启 Socket 端口,表示端口处于等待监听状态
        Socket socket = serverSocket.accept();
        // 调用多线程去处理任务(单次请求响应逻辑核心流程在 RequestResponseTask 中处理)
        Runnable task = new RequestResponseTask(socket);
        // 线程池分配一个线程去处理任务
        threadPool.execute(task);
    }
}

接下来就是 用单个RequestResponseTask 线程处理单次请求,并且对客户端返回响应,处理请求的步骤主要有以下六步:

  1. 读取、解析并得到 Request 请求对象
// 首先是 Request 对象部分属性属性
public class Request implements HttpServletRequest {
    private final String requestMethod;
    private final String contextPath;
    private final String servletPath;
    private final String requestURI;
    private final Map<String, String> parameters;
    private final Map<String, String> headers;
    private final List<Cookie> cookies;
    private HttpSession session = null;
}
// 然后是 Cookie 对象部分属性
public class Cookie {
    private final String name;
    private String value;
}
// 最后是 SessionImpl 对象部分属性
public class HttpSessionImpl implements HttpSession {
    // Session 内部就维护了一个 Map<String, Object> 对象,用于存储信息
    private final Map<String, Object> sessionData;
    private final String sessionID;
}

调用工具类HttpRequestParser.parse(InputStream socketInputStream)根据客户端发送的请求内容解析出来所有需要的字段,赋值给新创建的 Request 对象中,其中 session 对象在构造方法中通过遍历 Cookie 内容获得。

// 解析请求内容
public class HttpRequestParser {
    public Request parse(InputStream socketInputStream) throws UnsupportedEncodingException {
        // 实现代码见 GitHub...
    }
}

  1. 实例化一个 Response 对象
// 创建 Response 对象
Response response = new Response();
  1. 定位到请求的项目
// 返回一个 Context 对象
Context context = null;
String contextPath = request.getContextPath();
for (Context contextTemp : HttpServer.contextList) {
    System.out.println(contextTemp.getName());
    if (contextTemp.getName().equals(contextPath)) {
        context = contextTemp;
        break;
    }
}
  1. 得到该请求加载的 Servlet
// 得到 Servlet 对象
Servlet servlet = null;
if (context == null) {
    // 说明没找到项目,则 跳转到 NotFoundServlet
    servlet = HttpServer.notFoundServlet;
} else {
    // 找到项目了,开始根据 servletPath 寻找 Servlet
    String servletName = context.getConfig().getUrlToServletName().get(request.getServletPath());
    String servletClassName = context.getConfig().getServletNameToServletClassPath().get(servletName);
    for (Servlet servletTemp : context.servletList) {
        if (servletTemp.getClass().getCanonicalName().equals(servletClassName)) {
            servlet = servletTemp;
        }
    }
}
if (servlet == null) {
    // 如果没找到 servlet,说明有两种情况:1. 定位的是静态资源 2. 确实没有这个servlet    --->   需要跳转到 DefaultServlet
    servlet = HttpServer.defaultServlet;
}
  1. 调用 servlet.service() 交给业务处理
// 交由业务处理
servlet.service(request, response);
  1. 根据 Response 对象中的数据,发送 HTTP 响应给浏览器
private void sendResponse(HttpServletRequest request, Response response, OutputStream outputStream) throws IOException, NoSuchFieldException, IllegalAccessException {

    // 保存 Session: 1. 创建一个 Cookie(保存的SessionID) 加入 Response 响应给浏览器。 2. 保存 Session 文件到本地

    // 将响应行、响应头、响应体的内容写入到 Socket 流中,响应给浏览器
    
    // 代码见 GitHub...
}
  • Session 读取解析与持久化操作
    在 SessionImpl 对象实例化的时候有两种构造方法,一种是没有获取到 SessionID的情况,会通过 UUID 随机一个不重复的字符串作为本 Session 的SessionID,并且创建一个新的 SessionData;另一种是获取到请求方的 SessionID 的情况,在这种情况时会去本地加载 SessionID 对应的 Session 文件加载到 SessionData 中。
// 没有 SessionID 创建 SessionImpl 对象
public HttpSessionImpl() {
    this.sessionID = UUID.randomUUID().toString();
    this.sessionData = new HashMap<>();
}
// 有 SessionID 创建 SessionImpl 对象
public HttpSessionImpl(String sessionID) {
    this.sessionID = sessionID;
    this.sessionData = SessionUtil.loadSession(sessionID);
}
// 从本地加载 SessionData 
public static Map<String, Object> loadSession(String sessionID){
    File sessionRoot = new File(BASE_SESSION);
    HashMap<String, Object> map = new HashMap<>();
    String fileName = sessionID + ".sid";

    File[] files = sessionRoot.listFiles();
    if (files == null) {
        throw new RuntimeException();
    }
    for (File file : files) {
        if (file.getName().equals(fileName)) {
            // 找到 sessionId 对应的序列化文件(读取持久化文件)
            String sessionPath = BASE_SESSION + "/" + fileName;
            try (InputStream inputStream = new FileInputStream(sessionPath)){
                ObjectInputStream reader = new ObjectInputStream(inputStream);
                map = (HashMap<String, Object>) reader.readObject();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    return map;
}
// 将 SessionData 保存到本地 session 文件夹下,命名为 sessionID.sid
public static void saveSessionData(HttpSession session) throws IllegalAccessException, NoSuchFieldException {
    Field field = session.getClass().getDeclaredField("sessionData");
    Field field1 = session.getClass().getDeclaredField("sessionID");
    field.setAccessible(true);
    field1.setAccessible(true);
    // 获取 sessionData、sessionID
    Map<String, Object> sessionData = (Map<String, Object>) field.get(session);
    String sessionID = (String) field1.get(session);

    // 如果 sessionData 没有内容就不用持久化到本地了
    if (sessionData.isEmpty()) {
        return;
    }
    
    // 持久化 sessionData
    String savePath = BASE_SESSION + "/" + sessionID + ".sid";
    try (OutputStream outputStream = new FileOutputStream(savePath)){
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(sessionData);
        objectOutputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

编写 Servlet 进行测试

在这里插入图片描述
在这里插入图片描述
配置文件web.conf

servlets:
  # ServletName = ServletClassName
  TranslateServlet = com.fjf.webapps.dictionary.TranslateServlet
  LoginActionServlet = com.fjf.webapps.dictionary.LoginActionServlet
  ProfileActionServlet = com.fjf.webapps.dictionary.ProfileActionServlet

servlet-mappings:
  # URLPattern = ServletName
  /translate = TranslateServlet
  /login-action = LoginActionServlet
  /profile-action = ProfileActionServlet
  • 开启服务器(执行 HttpServer

  • 打开浏览器访问 http://localhost:8080/dictionary/ ,跳转到登录页面
    在这里插入图片描述

  • 访问 http://localhost:8080/dictionary/asdasda 这是随便的路径,本地没有该 Servlet 和静态资源
    在这里插入图片描述

  • 访问已经登陆后的页面(未登录状态)http://localhost:8080/dictionary/profile-action 会重定向到登录界面
    在这里插入图片描述

  • 测试用户名为 fjf 密码为 123456 ,输入错误的密码,重定向返回该登陆页面

  • 输入正确的用户名和密码,跳转到已经登陆后的页面,并且查看本地 SessionID 是否与持久化到服务器本地的 SessionID 相等
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 删除本地存储的 Session 后,重新访问 http://localhost:8080/dictionary/profile-action 又会重新定向到登陆界面,说明 Session 验证没通过


以上测试表明该项目轻量级 Http 服务器的基本功能已经基本实现!

项目介绍到此结束,具体代码请访问此处暂时还未上传至GitHub… 已上传

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值