# Tomcat学习笔记(三)-手写一个简易版本的Tomcat
[TOC]
名称:Minicat
Minicat要做的事情:作为⼀个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求,
Minicat可以接收到请求进⾏处理,处理之后的结果可以返回浏览器客户端。
1. 提供服务,接收请求(Socket通信)
2. 请求信息封装成Request对象(Response对象)
3. 客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
4. 资源返回给客户端浏览器
我们递进式完成以上需求,提出V1.0、V2.0、V3.0版本的需求
V1.0需求:浏览器请求http://localhost:8080,返回⼀个固定的字符串到⻚⾯"Hello Minicat!"
V2.0需求:封装Request和Response对象,返回html静态资源⽂件
V3.0需求:可以请求动态资源(Servlet)
## Tomcat 1.0 开发
### Tomcat 1.0基础开发
这里我们需要先使用Socket监听8080端口,当有请求访问时,默认就返回一个固定的字符串到前端
第一步,创建一个BootstarpV1类,用于启动Tomcat
```java
public class BootstrapV1 {
/**
* 默认监听8080端口
*/
private int port = 8080;
public void start() throws Exception {
//监听
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("启动Minicat1.0成功,监听端口为:"+port);
while(true){
try(Socket socket = serverSocket.accept()) {
OutputStream outputStream = socket.getOutputStream();
System.out.println("收到访问请求!");
String result = "Hello Minicat";
//增加返回请求头的封装信息
outputStream.write(getHttpHeader200(result.length()).getBytes());
outputStream.write(result.getBytes());
System.out.println("返回结果!");
}
}
}
public static void main(String[] args) throws Exception {
BootstrapV1 bootstrap = new BootstrapV1();
bootstrap.start();
}
}
```
这里进行监听8080端口请求,监听到请求之后,正常需要将Hello Minicat输出到浏览器,但是这里直接访问的话,会出现如下错误

而我们看到后台,其实是有接到请求的,那么就证明是我们返回格式的数据,不符合浏览器的规范导致的,需要对返回值参数进行封装,使得浏览器能够正常解析该数据

### Tomcat 1.0 返回参数封装
我们访问www.baidu.com网址,查看一下浏览器请求的过程,这里看到我们与后端的请求时,返回需要加上一个相应的请求头,这样浏览器才能解析具体的请求体

我们这里针对一些必输的参数进行返回的请求头封装,新建一个HttpProtocolUtil类,用于对基础的返回请求头进行封装
```java
public class HttpProtocolUtil {
/**
* 为响应码200提供请求头信息
* @return
*/
public static String getHttpHeader200(int contentLength) {
return "HTTP/1.1 200 OK \n" +
"Content-Type: text/html \n" +
"Content-Length: " + contentLength + " \n" +
"\r\n";
}
/**
* 为响应码404提供请求头信息(此处也包含了数据内容)
* @return
*/
public static String getHttpHeader404() {
String str404 = "<h1>404 not found</h1>";
return "HTTP/1.1 404 NOT Found \n" +
"Content-Type: text/html \n" +
"Content-Length: " + str404.getBytes().length + " \n" +
"\r\n" + str404;
}
}
```
这时候修改原有代码,增加上返回头的代码
```java
public void start() throws Exception {
//监听
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("启动Minicat1.0成功,监听端口为:"+port);
while(true){
Socket socket = serverSocket.accept();
OutputStream outputStream = socket.getOutputStream();
System.out.println("收到访问请求!");
String result = "Hello Minicat";
//增加返回请求头的封装信息
outputStream.write(getHttpHeader200(result.length()).getBytes());
outputStream.write(result.getBytes());
System.out.println("返回结果!");
socket.close();
}
}
```
此时返回结果正常

这样,1.0的需求我们就完成了,下面我们开始做2.0的需求
## Tomcat 2.0开发
### 封装Request和Response类
需要完成需求封装Request和Response对象,返回html静态资源⽂件
首先分析相对于静态的1.0Tomcat,2.0的Tomcat需要对浏览器的请求进行解析,需要知道请求的是哪个文件,然后我们后端才能找到对应的文件,将其返回,这里我们先访问http://localhost:8080/index.html,可以看到请求头信息上一些关键的数据

GET /index.html HTTP/1.1
请求类型 请求文件地址 协议
我们这里需要对请求类型与静态资源地址进行封装,其他信息就暂时不做处理
首先建立Request类与Response类
```java
public enum HttpMethod {
/**
* get请求
*/
GET("get"),
/**
* post请求
*/
POST("post");
private String value;
HttpMethod(String value) {
this.value = value;
}
}
```
```java
public class Request {
/**
* 方法类型
*/
private HttpMethod method;
/**
* url
*/
private String url;
/**
* 请求的输入流
*/
private InputStream inputStream;
/**
* 根据输入流进行初始化设置
* @param inputStream
*/
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
// 从输入流中获取请求信息
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte[] bytes = new byte[count];
inputStream.read(bytes);
//将请求信息转为字符串
String requestValue = new String(bytes);
//按照换行符来切割,只获取第一行信息即可
String[] requestValues = requestValue.split("\n");
//将第一行信息按照空格来进行切割,获取前两个参数
String[] httpValue = requestValues[0].split(" ");
String method = httpValue[0];
String url = httpValue[1];
this.method = HttpMethod.valueOf(method);
this.url = url;
}
}
```
```java
public class Response {
private OutputStream outputStream;
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
public void outPutStaticFile(String path) throws Exception {
// 获取静态资源文件的绝对路径
String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
// 输入静态资源文件
File file = new File(absoluteResourcePath);
if(file.exists() && file.isFile()) {
// 读取静态资源文件,输出静态资源
StaticResourceUtil.outputStaticResource(new FileInputStream(file),outputStream);
}else{
// 输出404
output(HttpProtocolUtil.getHttpHeader404());
}
}
private void output(String httpHeader404) throws IOException {
outputStream.write(httpHeader404.getBytes());
}
}
```
StaticResourceUtil工具类
```java
public class StaticResourceUtil {
/**
* 获取静态资源文件的绝对路径
* @param path
* @return
*/
public static String getAbsolutePath(String path) {
//获取静态文件地址
String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\", File.separator) + path;
}
/**
* 将静态文件从输入流中读取到输出流
* @param fileInputStream
* @param outputStream
* @throws IOException
*/
public static void outputStaticResource(FileInputStream fileInputStream, OutputStream outputStream) throws IOException {
int resourceSize= fileInputStream.available();
// 输出http请求头,然后再输出具体内容
outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
//将文件中读取到的字节返回到前台
byte[] bytes = fileInputStream.readAllBytes();
outputStream.write(bytes);
}
}
```
### Tomcat 2.0启动类编写
```java
public class BootstrapV2 {
/**
* 默认监听8080端口
*/
private int port = 8080;
public void start() throws Exception {
//监听
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("启动Minicat2.0成功,监听端口为:"+port);
while(true){
try(Socket socket = serverSocket.accept()) {
System.out.println("收到访问请求!");
InputStream inputStream = socket.getInputStream();
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
String url = request.getUrl();
response.outPutStaticFile(url);
System.out.println("返回结果!");
socket.close();
}
}
}
public static void main(String[] args) throws Exception {
BootstrapV2 bootstrap = new BootstrapV2();
bootstrap.start();
}
}
```
### Tomcat 2.0测试
这里我们在resources文件夹下面增加一个index.html页面
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>static resouce</title>
</head>
<body>
Hello Minicat-静态资源文件访问!
</body>
</html>
```
启动Tomcat 2.0,看看能否达到我们预期的效果
访问:http://localhost:8080/index.html

然后访问一下其他不存在的页面,看能否正常输出404:

现在2.0的开发完成了,下面我们开发3.0的Tomcat
## Tomcat 3.0开发
3.0需求:可以请求动态资源(Servlet)
那么我们的解决思路是,首先需要声明一个Servlet接口,然后新增一个web.xml配置文件,启动Tomcat3.0的时候就将web.xml中的配置的servlet加入到缓存当中,每次发起请求的时候,使用缓存中的servlet进行处理,匹配不到servlet就去匹配相应的静态文件,如果都匹配不上就报错
### Tomcat3.0 基础开发
**新增Servlet基类**
```java
public interface Servlet {
void init() throws Exception;
void destory() throws Exception;
void service(Request request,Response response) throws Exception;
}
```
**新增HttpServlet实现类**
```java
public abstract class HttpServlet implements Servlet{
public abstract void doGet(Request request,Response response);
public abstract void doPost(Request request,Response response);
@Override
public void service(Request request, Response response) throws Exception {
if(HttpMethod.GET.equals(request.getMethod())) {
doGet(request,response);
}else{
doPost(request,response);
}
}
}
```
**新增需要测试的类LagouServlet**
```java
public class LagouServlet extends HttpServlet {
@Override
public void doGet(Request request, Response response) {
String content = "<h1>LagouServlet get</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
String content = "<h1>LagouServlet post</h1>";
try {
response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void init() throws Exception {
}
@Override
public void destory() throws Exception {
}
}
```
**新增web.xml配置文件并进行解析**
在resources下面增加web.xml配置文件
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>lagou</servlet-name>
<servlet-class>server.LagouServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>lagou</servlet-name>
<url-pattern>/lagou</url-pattern>
</servlet-mapping>
</web-app>
```
pom.xml文件增加dom4j依赖
```xml
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
```
**新建一个BootstrapV3**
启动的时候读取配置文件进行初始化Servlet缓存
```java
public class BootstrapV3 {
/**
* 默认监听8080端口
*/
private int port = 8080;
private Map<String,HttpServlet> servletMap = new HashMap<String,HttpServlet>();
public void start() throws Exception {
loadServlet();
//监听
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("启动Minicat2.0成功,监听端口为:"+port);
while(true){
try(Socket socket = serverSocket.accept()) {
System.out.println("收到访问请求!");
InputStream inputStream = socket.getInputStream();
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
String url = request.getUrl();
// 静态资源处理
if(servletMap.get(url) == null) {
response.outPutStaticFile(request.getUrl());
}else{
// 动态资源servlet请求
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request,response);
}
System.out.println("返回结果!");
socket.close();
}
}
}
/**
* 加载解析web.xml,初始化Servlet
*/
private void loadServlet() {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// <servlet-name>lagou</servlet-name>
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
// <servlet-class>server.LagouServlet</servlet-class>
Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
String servletClass = servletclassElement.getStringValue();
// 根据servlet-name的值找到url-pattern
Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
// /lagou
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
BootstrapV3 bootstrap = new BootstrapV3();
bootstrap.start();
}
}
```
### Tomcat3.0测试
访问http://localhost:8080/lagou,得到正确的结果

### Tomcat 3.0 使用线程池进行优化访问请求
这里如果我们一个请求速度很慢的话,就会直接导致整个线程阻塞,下面对线程阻塞进行测试
**在servlet代码中增加线程睡眠时间**

这里发现前端请求这个servlet时,无法再请求静态页面或者其他内容了
http://localhost:8080/lagou的请求阻塞,导致我们无法请求到http://localhost:8080/index.html

我们这里使用线程池对其进行异步处理,接收到请求使用使用子线程去处理具体的请求
这里继续请求http://localhost:8080/lagou,发现一直在等待

同时访问静态文件,http://localhost:8080/index.html,发现可以成功进入,证明使用线程池优化是可以达到同时处理多个请求的目的

Tomcat学习笔记(三)-手写一个简易版本的Tomcat