前言
本篇博客与大家分享的是个人实现的一个HTTP服务器+Servlet容器,其可以理解为极小级的 tomcat 内部功能涉及存储多个web应用,并可以能与标准的浏览器进行简单交互的 HTTP 服务器,本项目十分适合对网络学习的初学者,可以更加清晰的认识网路传输,HTTP协议等知识,在此声明本篇博客只作为博主的个人学习及笔记记录,如能对各位博友有所帮助不胜荣幸,同时此篇博客也可能会存在些博主编写时的笔误或者对某处有认识不恰当的地方,针对这些问题,敬请各位博友斧正
项目简介
该项目是在 TCP 的基础上,按照规定的HTTP协议获取解析数据,并且通过URL上的路径找到服务器中对应 web 应用下的对应 Servlet 处理,并最终发送响应回客户端, 实现一个能与标准的浏览器进行简单交互的 HTTP 服务器 本项目要对网络原理与 HTTP 协议具有一定了解,不太清除的博友可以参考HTTP协议与网络原理
涉及知识预研
HTTP协议
HTTP协议是建立在 TCP 的基础上的一个应用层协议,其作用在于处理资源的请求与响应过程 HTTP协议格式 
URL——本质上是通过某种方式的唯一定位资源,对应到浏览器访问网页就是输入的网址 其标准格式为:protocol : // hostname[:port] / path / [;parameters][?query]#fragment 对HTTP协议来说,如下图一个URL各部分拆解,因为明确是HTTP协议最前面的 protocol 省略 
TCP 协议与 Socket 套接字编程
HTTP 协议是基于 TCP 协议的基础上工作的,因为 TCP 传输的可靠性 TCP进行数据传输会从本地端口传输,最终将数据交付到目的端口的进程中,而我们所实现的HTTP服务器最终也是以进程的方式体现 而TCP是传输层协议,对于 TCP 传输数据,一条TCP连接一共要经历三个阶段  Socket编程又叫套接字编程,TCP、UDP等本身时实现在操作系统内部的一种能力,Socket则是暴露在外部给应用层的一个“接口”(插口),由此让应用层可以通过Socket使用操作系统提供各种服务 本项目中通过 JDK 封装好的Socket类来实现对TCP的使用,本次一共使用到两个相关的类 java.net.ServerSocket——服务器作为被动连接方,做三次握手之前的端口监听使用,用于确定服务器提供连接到具体哪个端口 java.net.Socket——客户端作为主动连接方,服务器作为被动连接方,创建一条建立好的TCP连接
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
socket.close();
实现基本原理
对于HTTP服务器
其主要功能就是获取解析 HTTP 请求,然后处理数据,最终发会响应,具体步骤如下:
- 先利用 Socket 建立好 TCP 连接
- 在 TCP 的基础上,获取传输来的请求数据,再根据 HTTP 协议解析数据
- 再通过解析 URL 中的 contextPath 和 servletPath 定位到具体由服务器哪个 servlet 处理
- 最终将处理好的数据再组装成 Response 通过 Socket 传回客户端(浏览器)
对于Servlet容器
主要用于实现官方的Servlet标准,这里只实现了一下常用功能,所有本质上实现了一个官方Servlet的子集
其内部会根据标准的 webapp 的格式保存各个 Web应用,并且会在特定目录下保存编写的每个项目编写的 Servlet,再通过各个 Web应用的配置文件(web.conf),在对应的文件中找到Servlet对象并利用反射机制对其实例化,保存到一个Map中,最终等到HTTP服务器处理请求时调用
整体工作流程
整体流程框架
- 首先初始化各个 Context下 的 Servlet 对象(扫描/加载类/实例化/初始化)
容器中存储的所有Web应用下的所有 Servlet.init() - TCP Server流程
while(true){ 每次循环处理一个HTTP请求 servlet.service } - 执行销毁过程,servlet.destroy()
在大的流程框架基础下,对于初始化操作和处理HTTP请求操作还有进一步的流程框架
初始化操作具体流程
- 扫描出所有Web应用下的 Context
- 读取并解析每个Web应用下的 web 配置文件(web.conf),获取到每个应用Servelt的具体位置
- 根据第二步解析出Servelt的具体位置,加载需要的 ServeltClass 表现为class<?> 对象
- 实例化第三步加载出的所有的 Servlet
- 调用每个Servlet的init()方法,进行初始化
处理HTTP请求具体流程
- 从 socket 中解析出 请求对象 request
- 构造好 service()方法中要使用的响应对象 response
- 根据请求中解析URL得到的ContextPath,判断,交由哪个 context 进行处理
- 根据请求中解析URL得到的ServletPath,判断,交由哪个 servlet 进行处理
- 调用对应 servlet.service(request,response)
- 发送 HTTP响应
项目模块
 本项目分为协议制定模块、HTTP服务器模块、动态资源模块
- 协议制定模块(standard):主要功能制定一套协议标准,然后由服务器按照标准实现,Web应用按照标准使用服务器提供的服务
 该模块一共包括: 三个Servlet级别的Servlet、ServletRequest、ServletResponse接口 四个Http级别的HttpServlet、HttpServletRequest、HttpServletResponse、HttpSession抽象类和接口 以及一个ServletException自定义异常类、一个Cookie类
理论上指定标准模块应该实现标准的Servlet,但因官方标准太过庞大,这里指实现了些基本标准,可以理解为一个标准Servlet的子集
 该模块包括: HTTP级别Request、Response实现类,及它们的请求解析与响应发送类和一个session操作类 四个本地的处理静态资源类DefaultServlet,三个错误信息类MethodNotAllowed、NotFoundServlet、NotLogin 九个对服务器初始化及实现的提供各种服务类,Config、ConfigReader、Context、DefaultContext、ErrorServlet、HTTPServlet、PoolStart、RequestResponseTask、ServletInitialize
- 动态资源模块:
 该模块下用于设计动态资源,配合对应的Web应用使用  这是一个Web应用的多级目录,web.conf下为该Web应用的配置文件,类似于web.xml,本项目中会创建结果简单的Web应用,主要用于测试服务器提供的各种服务是否正常执行
模块具体实现
标准制定模块
标准制定模块内中各个接口抽象类之间的关系

自定义Servlet接口,实现一个官方Servlet的子集Servlet
自定义异常类,用于处理Servlet中的异常
自定义ServletRequest接口
自定义ServletResponse接口
HttpServlet 抽象类,实现自 Servlet,并实现了service()方法和doGet()方法
HTTPServletRequest接口,继承自ServletRequest,在其基础上加入了一下HTTP请求的抽象方法
HTTPServletResponse接口,继承自ServletResponse接口,并且添加了HTTP特有的响应方法
自定义的Cookie类,封装出一个cookie对象,用于处理存储Http请求头中的 cookie 信息
HTTP服务器模块
该模块主要包括五个实现制定标准的Request、Response的实现类,三个响应错误的实现类,一个调用静态资源的实现类,九个实现服务器具体服务的实现类
Request实现类,实现自HTTPServletRequest接口,并创建了请求中各种属性{ 请求方法(method)、路径(RequestURI)、Context(ContextPath)、Servlet(ServletPath)、query(parameter)、请求头(header)、Cookie(CookieList)、session数据(session) },以及对应的get()方法
Response实现类,实现自HTTPServletResponse接口,并创建了响应中的各种属性{ 状态码(status)、 Cookie(CookieList)、header(headers)、OutPutSteam(bodyOutputStream)、PrintWriter(bodyPrintWriter) },以及对应的get()、set()方法
HTTPSessionlmpl实现类,实现自HTTPSession接口,并创建了Session的属性{ sessionID、sessionData },并且实现了根据传入的sessionID在本地获取session的方法,和在本地保存session的方法
HttpServer类,实现main方法,作为服务器的入口,建立了给定端口的监听,并按照整体框架流程创建初始化和销毁方法,并其创建线程池,每次分配线程来处理HTTP请求
ServletInitialize类,做为对服务器初始化的类,其内部封装了五个方法,分别对应初始化的五个流程
具体初始化流程:
- 根据存放 Context 的 webapps 的路径,利用 IO 对文件进行扫描,并将其下所有 context 对象保存在 Map 中
- 读取并解析各自 web 配置文件,此处为了简便并没有实现Tomcat中的web.xml 而是使用自定义格式,利用有限状态机进行解析,得到ServletPath和ServletName的Map、ServeletName和具体存储位置的Map
- 根据 Context 加载需要的ServletClass 表现为 Class<?> 对象,引入ClassLoader 类进行类加载,主要目的为了保证每个 Context 之间的类加载器各不相同,起到隔离目的,防止加载时出现版本冲突,导致加载错误
- 实例化所需要的Servlet对象,利用反射技术进行对象实例化操作,调用每个 Class<?> 对象newInstance()方法
- 执行Servlet初始化操作,调用Servelt对象的init()方法,让子类可以通过重新init()方法实现不同的初始化工作
PoolStart类,主要功能创建线程池,并为每个发送来的请求分配线程进行HTTP处理,此处使用 ThreadPoolExecutor 自定义线程池
RequestResponseTask类,用于进行HTTP请求-响应的处理,具体共进行五部操作
- 解析 HTTP 请求得到 Request 对象
- 构建 Response 对象
- 根据从 URL 中解析到的 contextPath,找到交由哪个 context 进行处理
- 根据从 URL 中解析到的 servletPath,找到交由哪个 servelt 进行处理
- 调用对应 servlet.service()方法,
- 发送 Response 对象,进行HTTP响应

HttpRequestParser类,主要功能为解析 HTTP 请求最终封装成 Request对象,根据HTTP格式,分别解析请求行,请求头,请求体,以及分割URL
HttpResponseSend类,主要功能拼接 HTTP 响应,最终将Response写回客户端
Config类,其内部将 解析 web 配置文件产生的两个Map封装成一个对象
ConfigReader类,其功能为利用状态机技术解析 web 配置文件
Context类,其内部封装了 context 的各个属性,name,config,configRead,以及类加载器ClassLoader
DefaultServlet类,继承自HttpServlet,其功能为服务器处理静态资源
NotFoundServlet类,继承自HttpServlet,其功能为服务器响应404错误
MethodNotAllowed类,继承自Httpservlet,其功能为服务器响应405错误
NotLogin类,继承自HttpServelt,其功能为服务器响应401错误
ErrorServlet类,其内部封装存储着各个错误响应Servlet的Map,用于响应错误时调用
动态资源模块
其下为Web应用的动态资源,此处实现了登录的LoginActionServlet,验证登录信息的ProfileActionServlet,实现简单翻译功能的TranslateServlet
重点部分详解
类加载器加载Servlet
类加载器
实现通过类的权限名获取该类的二进制字节流代码块叫做类加载器(其本身也是一个类),其为所有被载入内存中的类生成一个java.lang.Class实例对象
本项目在服务器初始化阶段,实例化Servlet对象就是利用其类加载器ClassLoader,引入了 ClassLoader 类进行类加载,主要目的是不同的 context 之间的类加载器(ClassLoader)需要各不相同,防止有版本冲突时,类加载错误,起到隔离的目的
关于Java类加载机制的参考 java虚拟机类加载机制
public class Context {
private final ClassLoader webappClassLoader = Context.class.getClassLoader();
Map<String,Class<?>> servletClassMap = new HashMap<>();
public void loadServletClasses() throws ClassNotFoundException {
Set<String> servletClassNames = new HashSet<>(config.getServletToServletClassNameMap().values());
for(String servletClassName : servletClassNames){
Class<?> servletClass = webappClassLoader.loadClass(servletClassName);
servletClassMap.put(servletClassName,servletClass);
}
}
Map<String,Servlet> servletMap = new HashMap<>();
public void instantiateServletObjects() throws IllegalAccessException, InstantiationException {
for(String servletClassName : servletClassMap.keySet()){
Servlet servlet = (Servlet) servletClassMap.get(servletClassName).newInstance();
servletMap.put(servletClassName,servlet);
}
}
}
cookie+session的设置
Session是指Web系统的会话,指用户登录以后,在退出之前都是一个会话, 它的作用就在于客户端在访问敏感资源时,服务器可以进行身份认证
Cookie实际上就是一小段文本信息,其原理是:客户端本地保存用户的身份信息,然后每次发送HTTP请求时携带该Cookie,用于登录验证 使用场景:登录页面 多少天内免登录 和 记住密码等
本项目采用session+cookie的方式用来实现用户登录时服务器的验证操作
具体流程: 场景:客户端访问页面进行用户登录
- 客户端输入用户名+密码点击提交,向服务器发起验证请求
- 服务器第一次收到请求,将请求来的用户名+密码封装成一个User对象,再将User对象存放在Map中,key = sessionName,value = User对象
- 在服务器本地开辟一块内存空间,将存储session信息的Map写入空间中,同时利用UUID生成一个sessionID作为文件名
- 服务器将sessionID存放到cookie中,向客户端发送含sessionID的cookie
- 客户端接收到后,将其保存到本地,接下来每次发送请求时,都会发送含有sessinID的cookie,供服务器验证身份

public class HttpSessionImpl implements HttpSession {
private final String sessionID;
private final Map<String,Object> sessionData;
private static final String SESSION_BASE = "D:\\JavaDemo\\HttpServlet2.0\\sessions";
public HttpSessionImpl(){
sessionID = UUID.randomUUID().toString();
sessionData = new HashMap<>();
}
public HttpSessionImpl(String sessionID) throws IOException, ClassNotFoundException {
this.sessionID = sessionID;
sessionData = loadSessionData(sessionID);
}
private Map<String, Object> loadSessionData(String sessionID) throws IOException, ClassNotFoundException {
String path = SESSION_BASE+"\\"+sessionID+".session";
File file = new File(path);
if(!file.exists()){
return new HashMap<>();
}else{
try(InputStream inputStream = new FileInputStream(file)){
try(ObjectInputStream objectInputStream = new ObjectInputStream(inputStream)){
return (Map<String, Object>) objectInputStream.readObject();
}
}
}
}
@Override
public Object getAttribute(String name) {
return sessionData.get(name);
}
public String getSessionID() {
return sessionID;
}
public Map<String, Object> getSessionData() {
return sessionData;
}
public static String getSessionBase() {
return SESSION_BASE;
}
@Override
public void removeAttribute(String name) {
sessionData.remove(name);
}
@Override
public void setAttribute(String name, Object value) {
sessionData.put(name,value);
}
public void saveSessionData() throws IOException {
String path = SESSION_BASE+"\\"+sessionID+".session";
try(OutputStream os = new FileOutputStream(path)){
try(ObjectOutputStream objectOutputStream = new ObjectOutputStream(os)){
objectOutputStream.writeObject(sessionData);
objectOutputStream.flush();
}
}
}
@Override
public String toString() {
return "HttpSessionImpl{" +
"sessionData=" + sessionData +
'}';
}
}
编写身份验证的 servlet
public class ProfileActionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, ClassNotFoundException {
HttpSession session = req.getSession();
User user = (User)session.getAttribute("user");
System.out.println("User:"+user);
if(user == null){
resp.sendRedirect("login.html");
}else{
resp.setContentType("text/plain");
resp.getWriter().println(user.toString());
}
}
}
整体流程分析
-
初始化工作 1.1 扫描出所有的 Context 1.2 读取并解析每个web应用的配置文件 web.conf 1.3 加载需要的 ServletClass 表现为 Class<?> 1.4 实例化需要的 Servlet 对象 1.5 执行每个 Servlet 对象的init()方法 -
处理HTTP请求-响应 2.1 读取HTTP请求解析为Request对象 2.1.1 解析请求行(方法,路径,版本号) 2.1.2 解析请求头(核心为解析cookie) 2.1.3 解析请求体 2.2 构建Response对象 2.3 根据URL解析到的 contextPath,找到交由哪个 context 进行处理 2.4 根据URL中解析到的 servletPath,找到交由哪个 servelt 进行处理 2.5 调用对应 servlet.service()方法, 2.6 发送 Response 对象,进行HTTP响应
源码
github项目源码
项目的边界与总结
- HTTP协议层面,目前只支持HTTP1.0版本,只支持短连接,只支持到GET方法如果请求方法为POST直接回返回405错误
- 对于TCP server处理,本项目采用BIO的模式,并未采用真实Tomcat的NIO模式
- Servlet层面只实现了一个Servelt的子集
本项目作为实现一个HTTP服务器,旨在对网络原理、HTTP协议进行更加具象化的认识,作为一个完整流程,可以较为全面的学习巩固到网络的项相关知识,对于边界与项目的不足,随着接下来学习深入还会进一步优化,最后如本篇博客对广大博友有所帮助,不胜荣幸
|