前言:

最近思考怎么提升自己的编码水平,看的是比较多了,看了一些 Java 的源码实现,确实几乎没有重复的代码,封装与抽象做的很漂亮。但是学习不能光看,也得动手。

之前看过一个理论,就是哪怕把常用的框架整个复制一遍,自己写一遍,也会有很多收获,所以我打算把常用的 Java 框架的核心功能自己实现一遍,这样代码量不会太大,实现了核心功能也会有不小的收获。

第一个目标瞄准了 HTTP Server,作为一个 Java Web 开发者,基本上天天与这东西打交道,从最开始的外置 Tomcat 运行 war 包,到后面的 Springboot 使用的内置 Tomcat,都是作为一个服务器来响应客户端的请求。

这里 Tomcat 是一个 Servlet 容器,比普通的 HTTP 服务器要复杂,因为不仅包含了 HTTP 服务器的功能,还作为了 Servlet 的容器,需要实现 Servlet 相关的接口。

这里我的目的就是跟着 B站 尚学堂的课程,实现一个自己的 HTTP 服务器,这个是我前阵子跟着写的,一路跟下来,我发现了不少可以改进的地方,比如使用 Java8 的 API 替换繁杂的 Collection 操作,以及深入学习 Java 对于 TCP/IP 底层的接口的实现,以及 Socket 相关的类的源码的学习。

虽然这是一个挺简单的项目,我用了一天半整个过了一遍,不过说实话收获真的不算少,对于 Tomcat 的文件结构以及 Servlet 有了更深入的理解。

学习编程就是这样,当你学习过程中可能涉及到了很多相关的知识,所以需要构建自己的知识地图,这样才能触类旁通,越学越快。


学习目标:使用 Java 编写一个 HTTP SERVER

参考资料: B站尚学堂百战程序员视频

编写一个基于 请求-响应 的服务器需要用到的知识:

  1. OOP 面向对象编程思想
  2. 容器(集合类)
  3. IO
  4. 多线程
  5. 网络编程
  6. XML 解析
  7. 反射
  8. HTML
  9. HTTP 协议

前置知识1 —— 反射:赋予了 Java 语言动态性

反射相关知识简单介绍与对应样例代码编写

为什么这里需要反射知识?

在编写 Servlet 时,我们需要引入 web.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>study01.server06.basic.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>reg</servlet-name>
        <servlet-class>study01.server06.basic.servlet.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/g</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>reg</servlet-name>
        <url-pattern>/reg</url-pattern>
    </servlet-mapping>
</web-app>

Servlet 容器需要根据 web.xml 配置中配置的类的全路径名来动态生成对应 Servlet 的实例,这就需要使用到反射技术来 在运行期间动态创建对象

前置知识2 —— XML 解析

读取 XML 配置文件的前提就是对其进行解析,Java 主要有两种方式 DOM 和 SAX,这里使用 SAX,下面是简单介绍以及对应的 Demo。

使用 SAX 解析 XML文档简单知识以及样例代码

项目中解析 XML 的应用思路:解析 web.xml,根据 servlet-mappingurl-parttern 找到 servlet-class,然后根据完整的类路径使用反射构造对应的类

3. 封装Web上下文 —— WebContext 类编写

解析完 web.xml 之后,需要根据 url-pattern 找到 servlet-name,然后找到对应的 servlet-class,并初始化对应的类,此时需要使用 Map 保存这些对象。

WebContext 核心代码:

public class WebContext {

    //key-->servlet-name  value -->servlet-class
    private List<Entity> entities = null;

    //key -->url-pattern value -->servlet-name
    private List<Mapping> mappings = null;

    private Map<String, String> entityMap = new HashMap<>();
    private Map<String, String> mappingMap = new HashMap<>();

    public WebContext(List<Entity> entities, List<Mapping> mappings) {
        this.entities = entities;
        this.mappings = mappings;

        //todo 将 entity 对应的 List 转成对应的 Map 这里可以用 Java8 的写法
        for (Entity entity : entities) {
            entityMap.put(entity.getName(), entity.getClz());
        }

        for (Mapping mapping : mappings) {
            for (String pattern : mapping.getPatterns()) {
                mappingMap.put(pattern, mapping.getName());
            }
        }

    }

    /**
     * 从 pattern 中找到对应的 name 并且获取到对应 name 的具体类全路径
     *
     * @param pattern url
     * @return class full name
     */
    public String getClz(String pattern) {
        String name = mappingMap.get(pattern);
        return entityMap.get(name);
    }
}

4.HTTP服务器的编写思路:

1、了解 HTTP 协议 ---> 手写HTTP服务器 —— HTTP 协议简介

2、具体使用到的是 java.net 包下的类,也就是 Java 中的网络编程相关知识 ---> Java 网络编程概述

  • 因为要编写一个 HTTP 服务器,就需要能够接受 HTTP 请求,那么就一定要了解 HTTP 的格式,才能对应的进行正确的解析。

Socket 编程的特点:服务器与浏览器 建立连接 ,然后通过不同的协议发送数据。

目标:

  1. 使用 ServerSocket 与浏览器建立连接,并获取请求协议。
  2. 返回响应协议

返回给浏览器响应内容的步骤:

  • 准备内容
  • 获取字节数长度
  • 拼接响应协议
  • 使用输出流输出

这里使用到的 JDK 的类是 java.net 包下的 Socket 相关的类,这个包是 Java 中网络相关的包,之前也比较陌生,借着这个机会顺便梳理一下 java.net 包的结构:

  • ServerSocket : 充当服务器角色的套接字实现类
  • Socket : 充当客户端角色的套接字实现类。

这里通过使用 ServerSocket 绑定一个端口,客户端可以通过这个端口发送请求给服务器。

已经成功建立连接

image-20201011152457353

封装 Response 对象,只关注返回的内容

上一步使用 ServerSocket 已经可以接收 HTTP 请求了,但是并没有返回给浏览器任何数据,所以这一步开始返回数据给浏览器。

返回数据需要返回 Response 对象,这里面封装了 HTTP 响应数据规定的 :

  • HTTP 协议版本号
  • 状态码
  • 状态信息,服务器自己定义
  • HTTP 响应头
  • 可选项 ,数据主体

下面是相关代码: 可以看到这里返回的数据主体和Response 相关的格式都是手动拼接的,并且下面的拼接 HTTP 响应固定信息不应该与业务逻辑耦合在一起。

image-20201027223657756

有 Response 的 ServerSocket:

image-20201027223530562

所以需要将 Response 逻辑单独封装起来,解除与业务逻辑之间的耦合:

Response 类中的主要逻辑:构建响应固定格式信息

image-20201027223800732

封装 Response 后的 service 方法: 所有构建 Response 固定格式信息的步骤都被封装,只需要传入本次的返回码即可。

image-20201027223913454

封装 Request

将请求字符串转为对象

快速获取请求参数

HTTP 协议的意义

固定的格式,通过大量的对字符串的操作,就可以对请求进行处理,然后返回。

GET 方法需要对中文进行 decode 转为 utf8 字符格式

引入 Servlet,解耦业务代码

整合 web.xml 使用反射动态创建 Servlet

引入多线程

  • 每次请求起一个线程,然后创建对于的Servlet进行处理
  • 编写 DisPatcherServlet 进行分发

服务器中的链接特性:

  • 长连接
  • 短链接

问题

1、Maven/Web 项目中 classpath 读取 Resouce 资源

2、IDEA 断点调试技巧 ---- 静态代码块中的代码必须要一个个打断点才能一步步 debug ,否则会被直接跳过

Learning Poin

  • ServerSocket 源码 ----> 背后是 Java 中 net 包的作用以及设计理念

总结:

封装了请求、响应,Servlet 的分发,使用反射动态从配置文件中生成 Servlet,底层是 java.net 包中的 ServerSocket 类,实现了 WebServer 的基础功能,也有很多细节值得继续挖掘。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。