手写一个简化版Tomcat
一、Tomcat工作原理
我们启动Tomcat时双击的startup.bat文件的主要作用是找到catalina.bat,并且把参数传递给它,而catalina.bat中有这样一段话:

Bootstrap.class是整个Tomcat 的入口,我们在Tomcat源码里找到这个类,其中就有我们经常使用的main方法:

这个类有两个作用 :1.初始化一个守护进程变量、加载类和相应参数。2.解析命令,并执行。
源码不过多赘述,我们在这里只需要把握整体架构,有兴趣的同学可以自己研究下源码。Tomcat的server.xml配置文件中可以对应构架图中位置,多层的表示可以配置多个:

即一个由 Server->Service->Engine->Host->Context 组成的结构,从里层向外层分别是:
Server:服务器Tomcat的顶级元素,它包含了所有东西。
Service:一组 Engine(引擎) 的集合,包括线程池 Executor 和连接器 Connector 的定义。
Engine(引擎):一个 Engine代表一个完整的 Servlet 引擎,它接收来自Connector的请求,并决定传给哪个Host来处理。
Container(容器):Host、Context、Engine和Wraper都继承自Container接口,它们都是容器。
Connector(连接器):将Service和Container连接起来,注册到一个Service,把来自客户端的请求转发到Container。
Host:即虚拟主机,所谓的”一个虚拟主机”可简单理解为”一个网站”。
Context(上下文 ): 即 Web 应用程序,一个 Context 即对于一个 Web 应用程序。Context容器直接管理Servlet的运行,Servlet会被其给包装成一个StandardWrapper类去运行。Wrapper负责管理一个Servlet的装载、初始化、执行以及资源回收,它是最底层容器。
比如现在有以下网址,根据“/”切割的链接就会定位到具体的处理逻辑上,且每个容器都有过滤功能。

二、梳理自己的Tomcat实现思路
一个请求要请求服务器端的一个文件,服务端根据路径查找该文件,如果有则读取给文件并把文件内容响应回客户端。
实现以上效果整体思路如下:
1.ServerSocket占用8080端口,用while(true)循环等待用户发请求。
2.拿到浏览器的请求,解析并返回URL地址,用I/O输入流读取本地磁盘上相应文件。
3.读取文件,如果文件不存在则构建响应报文头、HTML正文内容,如果存在则把文件写到浏览器端。
三、实现自己的Tomcat
工程文件结构:

1.HttpServer核心处理类,用于接受用户请求,传递HTTP请求头信息,关闭容器:
package com.jp.jpHttp; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket; /**
* HttpServer核心处理类,用于接受用户请求,传递HTTP请求头信息,关闭容器
*
*/
public class HttpServer {
// 用于判断是否需要关闭容器
private boolean shutdown = false; public void acceptWait() {
ServerSocket serverSocket = null;
try {
/**
* serverSocket的三个参数
* TCP端口号:0-65535,端口号 0 在所有空闲端口上创建套接字
* 最大连接数:传入连接指示(对连接的请求)的最大队列长度被设置为 backlog 参数。如果队列满时收到连接指示,则拒绝该连接。
* ip地址: 参数可以在 ServerSocket 的多宿主主机 (multi-homed host) 上使用,ServerSocket 仅接受对其地址之一的连接请求。如果 bindAddr 为 null,则默认接受任何/所有本地地址上的连接
*/
serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// 等待用户发请求
while (!shutdown) {
try {
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream(); // 接受请求参数
Request request = new Request(is);
request.parse(); // 创建用于返回浏览器的对象
Response response = new Response(os);
response.setRequest(request);
response.sendStaticResource(); // 关闭一次请求的socket,因为http请求就是采用短连接的方式
socket.close();
System.out.println("服务端的serverSocket关闭");
// 如果请求地址是/shutdown 则关闭容器
if (null != request) {
shutdown = request.getUrL().equals("/shutdown");
}
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
} public static void main(String[] args) {
HttpServer server = new HttpServer();
server.acceptWait();
}
}
2.创建Request类,获取HTTP的请求头所有信息并截取URL地址返回:
package com.jp.jpHttp; import java.io.IOException;
import java.io.InputStream; /**
* 创建Request类,获取HTTP的请求头所有信息并截取URL地址返回
*
*/
public class Request {
private InputStream is;
private String url; public Request(InputStream input) {
this.is = input;
} public void parse() {
// 从socket中读取一个2048长度字符
StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
int i;
byte[] buffer = new byte[Response.BUFFER_SIZE];
try {
i = is.read(buffer);//从输入流is读取一定数量的字节,并存储到buffer字节数组中
} catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j = 0; j < i; j++) {
request.append((char) buffer[j]);//把buffer字符数组中的字节拼成字符串 request
}
// 打印读取的socket中的内容
System.out.println("打印socket的输入流中的内容");
System.out.print(request.toString());//打印字符串request,就是socket里的输入流,即来自客户端的内容
url = parseUrL(request.toString()); //从字符串request中抽取出请求路径
} //从字符串request中抽取出请求路径的函数
private String parseUrL(String requestString) {
int index1, index2;
index1 = requestString.indexOf(' ');// 看socket获取请求头是否有值
if (index1 != -1) {
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
System.out.println();
System.out.println("获取到请求文件的路径(url)" + requestString.substring(index1 + 1, index2));
return requestString.substring(index1 + 1, index2);
}
return null;
} public String getUrL() {
return url;
} }
3.创建Response类,响应请求读取文件并写回到浏览器
package com.jp.jpHttp; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream; /**
* 创建Response类,响应请求读取文件并写回到浏览器
*/
public class Response {
public static final int BUFFER_SIZE = 2048;
// 浏览器访问D盘的文件
private static final String WEB_ROOT = "D:";
private Request request;
private OutputStream output; public Response(OutputStream output) {
this.output = output;
} public void setRequest(Request request) {
this.request = request;
} public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
// 拼接本地目录和浏览器端口号后面的目录
File file = new File(WEB_ROOT, request.getUrL());
System.out.println("请求路径拼接为服务器内的文件路径:"+ file.getAbsolutePath());
//System.out.println("测试应用程序是否可以读取此抽象路径名表示的文件:" + file.canRead());
// 如果文件存在,且不是个目录
if (file.exists() && !file.isDirectory()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch != -1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
System.out.println("请求的文件存在,正在把文件作为响应写进socket的输出流");
} else {
// 文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
String retMessage = "<h1>" + file.getName() + " file or directory not exists</h1>";
String returnMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n"
+ "Content-Length: " + retMessage.length() + "\r\n" + "\r\n" + retMessage;
output.write(returnMessage.getBytes());
System.out.println("请求的文件不存在,返回404");
}
} catch (Exception e) {
System.out.println(e.toString());
} finally {
if (fis != null)
fis.close();
}
}
}
实验文件

实验结果


请求文件不存在


当请求http://localhost:8080/shutdown 时,关闭容器,即不再监听端口

四、读者可以自己做的优化,扩展的点
1.在WEB_INF文件夹下读取web.xml解析,通过请求名找到对应的类名,通过类名创建对象,用反射来初始化配置信息,如welcome页面,Servlet、servlet-mapping,filter,listener,启动加载级别等。
2.抽象Servlet类来转码处理请求和响应的业务。发过来的请求会有很多,也就意味着我们应该会有很多的Servlet,例如:RegisterServlet、LoginServlet等等还有很多其他的访问。可以用到类似于工厂模式的方法处理,随时产生很多的Servlet,来满足不同的功能性的请求。
3.使用多线程技术。本文的代码是死循环,且只能有一个链接,而现实中的情况是往往会有很多很多的客户端发请求,可以把每个浏览器的通信封装到一个线程当中。
https://my.oschina.net/liughDevelop/blog/1790893
手写一个简化版Tomcat的更多相关文章
- 看年薪50W的架构师如何手写一个SpringMVC框架
前言 做 Java Web 开发的你,一定听说过SpringMVC的大名,作为现在运用最广泛的Java框架,它到目前为止依然保持着强大的活力和广泛的用户群. 本文介绍如何用eclipse一步一步搭建S ...
- 手写一个最迷你的Web服务器
今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码. 2. 代码介绍: ( ...
- 『练手』手写一个独立Json算法 JsonHelper
背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...
- 教你如何使用Java手写一个基于链表的队列
在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...
- 【spring】-- 手写一个最简单的IOC框架
1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...
- 放弃antd table,基于React手写一个虚拟滚动的表格
缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...
- 只会用就out了,手写一个符合规范的Promise
Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...
- 搞定redis面试--Redis的过期策略?手写一个LRU?
1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...
- 利用SpringBoot+Logback手写一个简单的链路追踪
目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...
随机推荐
- ListView无障碍识别整个listView,不识别item,设置了setContentDescription也没实用
点击ListView的时候.无障碍识别到的是整个listView,不会读点击的那个item. 解决的方法是在getView里手动设置: <span style="font-size:1 ...
- luogu1220 关路灯
题目大意 路面上有一些开着的灯,每个灯有功率和它的位置,人在第c个灯处,行走速度1m/s.问怎样关灯能使耗能最小,输出这个耗能. 思路 #include <cstdio> #include ...
- 窗口函数 SELECT - OVER Clause (Transact-SQL)
https://docs.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql Determines the pa ...
- POJ 3264 Balanced Lineup (线段树)
Balanced Lineup For the daily milking, Farmer John's N cows (1 ≤ N ≤ 50,000) always line up in the s ...
- 最新昆石VOS2009/VOS3000手机号段导入文件(手机归属地)
使用2017年4月最新版手机号段归属地制作,支持所有版本的VOS 共360569条记录,兼容所有版本的昆石VOS,包括VOS2009.vos3000.vos5000 导入比较简单.下载后解压到桌面在V ...
- 洛谷P1077 摆花(背包dp)
P1077 摆花 题目描述 小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共m盆.通过调查顾客的喜好,小明列出了顾客最喜欢的n种花,从1到n标号.为了在门口展出更多种花,规定第i种花不能 ...
- POJ 2286 The Rotation Game IDA*
(再一次感谢学长幻灯片) ID A* 随便自己yy了一下. 额嗯 思路什么的都没有问题 就是改不对.. 无奈地删代码...边删边交. 删啊删 哎呦 AC了 ... ... ... 找删的那一段 . o ...
- jQuery 对象转成 DOM 对象
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/stri ...
- C#微信公众号的开发——服务配置
最近因为需要用C#开发微信公众号的一些功能,记录一下开发公众号的一些坑..... 首先先介绍一下,微信公众号的官方文档.虽然这个文档我感觉比较糙,但是还是可以借鉴一下让我们摸着石头过河的. 首先我们得 ...
- 《java数据结构与算法》系列之“开篇”
大学的时候学习数据结构,当时吧虽然没挂这门课,但是确实学的不咋地,再但是其实自己一直都觉得数据结构很重要,是基础,只有基础好了,后面的路才能走的更好. 懒惰真的是天下的罪恶之源.所以一直到现在都毕业了 ...