本篇将会讲解tomcat的内部机制,我们会从0开始从底层实现一个tomcat,来实现简单的服务器机制。
阅读本篇需要的前置知识有:Java基础、HTML、http协议、Servlet
如果你缺乏这些前置知识阅读本篇可能会遭遇困难,建议你先阅读我整理的以下笔记后再来阅读本篇:
JavaWeb开发笔记图解整理(二)——XML、Tomcat、Servlet-优快云博客
JavaWeb笔记整理——HTML、CSS、JavaScript_html css java-优快云博客
一、Tomcat在JavaWeb中的地位
Tomcat是一系列Java类,是基于Java编写的服务器。
Tomcat在JavaWeb中的后端技术中,它作为服务器中间件,用于解析浏览器发送过来的http请求,从而获取服务器内部中的各种资源(可能是html、servlet等)。同时Tomcat内部有两个HashMap,用于存储servlet资源,并便于管理和访问servlet资源。
如果没有Tomcat(等类似的组件),服务器就无法对浏览器中的http请求进行解析,也不知道浏览器到底想要请求什么web资源,所以就没有办法给浏览器返回资源来显示页面。
当然,Tomcat配置了服务器的端口和ip,从而让浏览器能够通过这个ip和端口访问到我们的Tomcat服务器。
二、从零开始自己做一个Tomcat
Tomcat对于很多人来说很神秘,它是怎样解析html的?它又是如何返回资源的?这些对于大家来说可能都蒙着一层迷雾。
但是Tomcat其实并没有这么神秘,它本质上就是一些Java程序,接下来我们就根据上面的图,自己来做一个Tomcat从而让大家更了解Tomcat到底是怎样运作的。
1、实现Tomcat和浏览器的网络通信——socket网络编程
我们先来打通Tomcat和浏览器的网络通信,这显然需要用到socket技术。
请阅读下列代码,注释中有我的讲解。 (这里的代码和我的讲解是核心内容,请不要跳过)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class MyTomcatV1 {
public static void main(String[] args) throws IOException {
//创建ServerSocket在9000端口监听
ServerSocket serverSocket = new ServerSocket(9000);
System.out.println("MyTomcatV1在9000监听");
//如果服务器没有被关闭,就继续执行下面的代码
while (!serverSocket.isClosed()) {
//获取一个socket
Socket socket = serverSocket.accept();
//通过socket获取一个InputStream对象
InputStream inputStream = socket.getInputStream();
//通过BufferedReader将inputstream对象转换为一个reader 用字节流读取,
// 并按行读取,效率更高
BufferedReader reader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String mes = null;
//这里读取到的数据,就是浏览器发送的http请求
System.out.println("=========接收到数据========");
while ((mes = reader.readLine()) != null) {
if (mes.length() == 0) {
break;
}
System.out.print(mes);
}
//读取数据并处理数据后,Tomcat回送一个http响应
//先创建一个OutputStream
OutputStream outputStream = socket.getOutputStream();
/**创建一个自定义的响应头
* 这里包含着
* 1.http的版本和响应码 200 表示发送响应成功
* 2.响应内容的类型是text/html类型 并且使用utf-8字符集
*/
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type:text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "发送的http响应";
System.out.println("======给浏览器返回的数据======");
System.out.println(resp);
//发送http响应,注意要转换为字节数据
outputStream.write(resp.getBytes());
outputStream.flush();
//关闭流
outputStream.close();
reader.close();
socket.close();
}
}
}
我们运行程序,并使用浏览器测试一下。
发现浏览器成功的接收了我们的http响应,并且将响应的内容打印了出来。
这里如果我们把响应体改为html页面内容,浏览器就会显示相应的网页:
把我们写的html页面替换掉我们之前写的响应体的内容:
变成这样:
然后我们需要重新启动程序,再在浏览器中测试一下。
页面就会变为下面的样子:
以上,我们就完成了浏览器和Tomcat的基本通信。但是如果有多个浏览器访问Tomcat,我们依然会把它作为一个请求处理,所以我们需要使用多线程模型来完善我们的Tomcat。
我们还需要让Tomcat接收浏览器提交的数据,并进行处理。这就需要我们进一步完善。
2、实现Tomcat的多线程——BIO线程模型
接下来我们来实现多线程。
这里我们专门创建一个HttpRequestHandler处理http请求,并把之前我们写的Tomcat处理http请求的内容移植到HttpRequestHandler里。
如果Tomcat接收了一个Http请求,就创建一个socket对象并把这个socket对象传递给HttpRequestHandler对象,然后HttpRequestHandler对象就会启动一个线程来处理http请求。
为了实现多线程我们肯定需要继承Thread类,或者实现Runnable接口。
这里我们选择实现Runnable接口,这样就可以保留继承这一方式给其他的功能。
阅读下列代码,这里其实就是把之前的代码做成了一个线程。
import java.io.*;
import java.net.Socket;
public class HttpRequestHandler implements Runnable{
//创建一个socket属性,将来从MyTomcat中接收
private Socket socket = null;
//构造器,接收socket对象
public HttpRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//通过socket获取一个InputStream对象
try {
InputStream inputStream = socket.getInputStream();
//通过BufferedReader将inputstream对象转换为一个reader 用字节流读取,
// 并按行读取,效率更高
BufferedReader reader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String mes = null;
//这里读取到的数据,就是浏览器发送的http请求
System.out.println("=========接收到数据========");
while ((mes = reader.readLine()) != null) {
if (mes.length() == 0) {
break;
}
System.out.print(mes);
}
//返回数据给浏览器
OutputStream outputStream = socket.getOutputStream();
/**创建一个自定义的响应头
* 这里包含着
* 1.http的版本和响应码 200 表示发送响应成功
* 2.响应内容的类型是text/html类型 并且使用utf-8字符集
*/
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type:text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>login</title>\n" +
"</head>\n" +
"<body>\n" +
"<h1>登录页面</h1>\n" +
"<form>\n" +
" 用户名:<input>\n" +
" 密码:<input type=\"password\">\n" +
" <input type=\"submit\" value=\"登录\">\n" +
"</form>\n" +
"</body>\n" +
"</html>";
outputStream.write(resp.getBytes());
outputStream.flush();
//关闭流
outputStream.close();
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后一定要确保socket为null,不然会阻塞
if (socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
然后我们需要修改我们的MyTomcat,当Tomcat接收到一个连接时,接收请求,并启动一个线程:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class MyTomcatV2 extends Thread{
public static void main(String[] args) throws IOException {
//创建ServerSocket在9000端口监听
ServerSocket serverSocket = new ServerSocket(9000);
System.out.println("MyTomcatV2在9000监听");
//如果服务器没有被关闭,就继续执行下面的代码
while (!serverSocket.isClosed()) {
//得到socket对象
Socket socket = serverSocket.accept();
//创建httpRequestHandler对象
HttpRequestHandler httpRequestHandler = new HttpRequestHandler(socket);
//启动线程
new Thread(httpRequestHandler).start();
}
}
}
然后我们启动程序,并打开浏览器测试一下:
可以看到,有多个浏览器访问时,我们也可以向他们发送页面资源,从而让浏览器成功打开界面。
这样就避免了有多个浏览器同时访问服务器时,产生堵塞的问题。
(但是这里我发现,即便没有使用多线程来编写Tomcat我们也能开启多页面,可能是因为页面不够多,没有产生阻塞之类。)
3、使用servlet处理浏览器发送的信息(难)
(1)编写servlet规范
为了我们自己编写一个servlet从而处理浏览器发来的各种数据,我们需要先指定一套servlet规范,如下图可以看到,servlet jar包中的servlet结构是这样的:

我们来学习一下这套规范,并自己仿制一套。
先把结构搭建起来:
创建MyServlet接口、MyHttpServlet抽象类实现MyServlet接口
真正实现功能的Servlet——MyLoginServer。继承MyHttpServlet
此外,我们知道HttpServlet中还有两个重要的属性:Request和Response。这两个我们还需要自己写一下。
在Request类中,我们用来封装网页发来的http请求,其中内容包括method(get/post)、uri(web.xml中map配置的路径)、和参数列表(num1=xx,num2=xxx)。
而在Response类中,我们与Tomcat的socket对象相关联,从而获取到OutputStream用于给浏览器返回http响应,从而让浏览器显示一个新页面。
以上我们就编号了Servlet的规范,接下来我们需要进行Servlet的具体实现。
(2)实现Request和Response
上面说到,在Request类中,我们用来封装网页发来的http请求,可以见得,Request中肯定关联着socket对象中的输入流InputStream来获取http请求。我们来编写一下:
阅读以下代码和注释讲解:
package http;
import java.io.*;
import java.util.HashMap;
public class MyRequest {
//封装在Request中的属性:
private String method;
private String uri;
private HashMap<String, String> parametersMap = new HashMap<>();
private InputStream inputStream = null;
/**
*
* @param inputStream 输入流,由Tomcat传入
* @throws IOException
*/
public MyRequest(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader
(inputStream, "utf-8"));
// 读取第一行 根据下面的请求头来解析出来method和uri
// http请求头
// GET /myTomcat?username=111111&pwd=1111111 HTTP/1.1
//这里也就是Tomcat解析http请求的过程:
//读取第一行的数据 也就是请求头
String s = bufferedReader.readLine();
//分割字符串,从而获取到GET (method)
String[] strs = s.split(" ");
method = strs[0];
//获取到以?分割的字符串
int i = strs[1].indexOf("?");
//这里判断是否有参数传入,分两种情况讨论
//如果?的索引是-1,说明没有参数传入
if (i == -1) {
//得到第一个分割的字符串也就是uri
uri = strs[1];
} else {
//得到?之前的
uri = strs[1].substring(0, i);
String parameters = strs[1].substring(i + 1);
String[] parameterpairs = parameters.split("&");
//parameterpairs[0] = username=111111
for (String s1 : parameterpairs) {
if (s1.split("=").length == 2) {
parametersMap.put(s1.split("=")[0], s1.split("=")[1]);
}
}
}
}
/**
*
* @param name 传入一个参数名从而得到参数
* @return
*/
//通过这个getParameters方法得到参数
public String getParameters(String name){
String s = parametersMap.get(name);
return s;
}
}
package http;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
public class MyResponse {
private OutputStream outputStream = null;
private Writer writer = null;
//写一个http的响应头 要根据http响应的格式编写
public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
/**
*
* @param outputStream 由Tomcat传入与socket关联的OutputStream
*/
public MyResponse(OutputStream outputStream) {
this.outputStream = outputStream;
writer = new OutputStreamWriter(outputStream);
}
}
可以看到所谓的Tomcat对http请求进行解析,实际上就是将一个字符串进行分割处理,并分别获取其中的有用的信息,并分别放入对应的属性中。
(3)实现Servlet接口和HttpServlet抽象类的功能
Servlet接口中重要的三个方法是service方法、init方法和destroy方法,我们这里只实现service方法:
package servlet;
import http.MyRequest;
import http.MyResponse;
public interface MyServlet {
public void service(MyRequest request, MyResponse response);
}
学过java基础,我们知道接口中如果没有标识default,那么就是一个抽象方法,没有方法体。
由于我们的MyHttpServlet抽象类实现了MyServlet接口,所以我们需要在MyHttpServlet实现service方法,同时我们还需要在这里编写doGet和doPost抽象方法。
package servlet;
import http.MyRequest;
import http.MyResponse;
public abstract class MyHttpServlet implements MyServlet {
@Override
public void service(MyRequest request, MyResponse response) {
}
public abstract void doGet(MyRequest request, MyResponse response);
public abstract void doPost(MyRequest request, MyResponse response);
}
由于我们的MyLoginServlet在之前继承了 MyHttpServlet所以这里还需要实现doGet和doPost方法。
(4)更新HttpRequestHandler
我们需要更新这个类,来处理Request请求。
之前我们是通过返回浏览器页面和在服务端打印接收到的数据来进行处理的。
现在我们通过socket关联的OutputStream来返回浏览器一个实际的页面。
import http.MyRequest;
import http.MyResponse;
import utils.WebUtils;
import java.io.*;
import java.net.Socket;
public class HttpRequestHandler implements Runnable{
//创建一个socket属性,将来从MyTomcat中接收
private Socket socket = null;
//构造器,接收socket对象
public HttpRequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//通过socket获取一个InputStream对象
try {
InputStream inputStream = socket.getInputStream();
MyRequest request = new MyRequest(inputStream);
OutputStream outputStream = socket.getOutputStream();
MyResponse response = new MyResponse(outputStream);
//得到uri
String uri = request.getUri();
//解析uri是否是静态页面,这里我们编写一个工具类
if (WebUtils.isHtml(uri)){
String content = WebUtils.readHtml(uri.substring(1));
content = MyResponse.respHeader + content;
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
socket.close();
return;
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后一定要确保socket为null,不然会阻塞
if (socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
这里我们还编写了一个WebUtils来进行内容的读取,以及uri的判断:
package utils;
import java.io.*;
public class WebUtils {
/**
* 如果是html文件,就返回true
* @param uri
* @return
*/
public static boolean isHtml(String uri){
return uri.endsWith(".html");
}
/**
* 读取文件内容
* @param filename
* @return
* @throws IOException
*/
public static String readHtml(String filename) throws IOException {
String path = utils.WebUtils.class.getResource("/").getPath();
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader =
new BufferedReader(new FileReader(path + filename));
String str = "";
while((str = bufferedReader.readLine()) != null){
stringBuilder.append(str);
}
return stringBuilder.toString();
}
}
运行结果:
(5) 关联Servlet
现在我们需要编写Servlet来处理登录的请求,如果输入的密码和用户名符合,那么就进入另一个页面,表示登录成功。
怎么让服务器使用Servlet呢?我们需要先在我们的工程项目中添加web框架:
选第一个web选项点ok即可。然后会出现如下的文件夹:
在这里我们可以配置Servlet的信息和映射,从而让服务器和页面可以找到这个Servlet。
我们先编写一个判断登录成功的Servlet:
package servlet;
import http.MyRequest;
import http.MyResponse;
import java.io.IOException;
import java.io.OutputStream;
public class Check extends MyHttpServlet{
@Override
public void doGet(MyRequest request, MyResponse response) throws IOException {
String username = request.getParameters("username");
String pwd = request.getParameters("pwd");
if ("123".equals(username) && "123".equals(pwd)){
OutputStream outputStream = response.getOutputStream();
String content = "<h1>登录成功</h1>";
content = MyResponse.respHeader + content;
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
}
}
@Override
public void doPost(MyRequest request, MyResponse response) throws IOException {
doGet(request,response);
}
}
然后在web.xml文件中配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>Check</servlet-name>
<servlet-class>servlet.Check</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Check</servlet-name>
<url-pattern>/check</url-pattern>
</servlet-mapping>
</web-app>
然后我们需要在Tomcat中添加两个容器,分别用于存放web.xml中读取的文件
然后我们编写一个init()方法来读取web.xml文件并初始化Tomcat的容器 。(用到了反射来创建对象)
public void init() throws DocumentException, ClassNotFoundException, IllegalAccessException, InstantiationException {
//通过don4j来读取web.xml文件中的配置信息
//首先获取路径
String path = MyTomcatV3.class.getResource("/").getPath();
// System.out.println(path);
SAXReader saxReader = new SAXReader();
// <?xml version="1.0" encoding="UTF-8"?>
// <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
// xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
// version="4.0">
// <servlet>
// <servlet-name>Check</servlet-name>
// <servlet-class>servlet.Check</servlet-class>
// </servlet>
// <servlet-mapping>
// <servlet-name>Check</servlet-name>
// <url-pattern>/check</url-pattern>
// </servlet-mapping>
// </web-app>
Document read = saxReader.read(new File(path + "web.xml"));
Element rootElement = read.getRootElement();
// Element servlet = rootElement.element("servlet");
// Element servletMapping = rootElement.element("servlet-mapping");
List<Element> elements = rootElement.elements();
for (Element element : elements) {
if ("servlet".equalsIgnoreCase(element.getName())) {
Element name = element.element("servlet-name");
Element servletClass = element.element("servlet-class");
servletMapping.put(name.getText(),
(MyHttpServlet) Class.forName(servletClass.getText().trim())
.newInstance());
} else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
Element name1 = element.element("servlet-name");
Element url = element.element("url-pattern");
servletUrlMapping.put(url.getText().trim(),
name1.getText().trim());
}
}
}
更新RequestHandler,当读取的uri是一个servlet时,就通过两个map找到对应的servlet对象,并执行service方法:
以上就是Tomcat的一个简单的底层原理,其实我写的还是太简略了一些,很多功能都没有说明白,等以后有机会我再来详细补充一下……