核心项目:DiyTomcat(基本组件,多线程,内置对象,默认页面)
自己做了一个 具备 Servlet 容器功能的 web 服务器,但是并不是和 Tomcat 源码一样。Tomcat 本身的功能十分丰富,而且系统架构也比较复杂,并不适合直接通过完全仿照来学习,在研习了 Tomcat 源码的基础之上去繁化简。...
与Tomcat的区别:参考 Tomcat 的源码,自己做了一个 具备 Servlet 容器功能的 web 服务器,但是并不是和 Tomcat 源码一样。Tomcat 本身的功能十分丰富,而且系统架构也比较复杂,并不适合直接通过完全仿照来学习,在研习了 Tomcat 源码的基础之上去繁化简。
【问题】Tomcat 主要由以下组件构成:
Catalina:Catalina 是 Tomcat 的核心组件,它负责处理 HTTP 请求并将它们转发到适当的 Web 应用程序或 Servlet 容器进行处理。
Coyote:Coyote 是一个 HTTP 连接器,它处理传入的 HTTP 请求,并将它们传递给 Catalina 进行处理。Coyote 还支持 HTTPS 和 SSL。
Jasper:Jasper 是 JSP 引擎,它将 JSP 文件编译成 Java Servlet,并将其传递给 Catalina 进行处理。
Cluster:Cluster 是 Tomcat 的集群组件,它允许多个 Tomcat 服务器之间共享会话和负载均衡请求。
Manager:Manager 是 Tomcat 的管理应用程序,它允许用户管理 Tomcat 服务器,包括管理 Web 应用程序、会话和连接器等。
Realm:Realm 是 Tomcat 的身份验证和授权组件,它允许管理员配置用户和角色,并将它们与 Web 应用程序和 Servlet 容器进行关联。
JDBC 连接池:Tomcat 包含一个 JDBC 连接池,它允许 Web 应用程序和 Servlet 容器访问数据库连接,并提高了应用程序的性能和可伸缩性。
【问题】Tomcat部署多个应用?
使用Context容器:在一个Tomcat中可以使用多个Context容器来部署多个应用程序。每个Context容器可以为应用程序提供单独的配置,并将应用程序部署在不同的上下文路径中,从而实现应用程序之间的隔离。
配置多个Host:同样地,可以在一个Tomcat中配置多个Host,每个Host拥有自己的应用程序。通过在server.xml文件中配置不同的Host,可以实现不同应用程序之间的隔离。
【问题】Tomcat请求过程?
- 用户点击网页内容,请求被发送到本机端口8080/6060,被在那里监听的Coyote HTTP/1.1 Connector获得。
- Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应。
- Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
- Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理)。
- path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
- 构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或 doPost()执行业务逻辑、数据存储等程序。
- Context把执行完之后的HttpServletResponse对象返回给Host。
- Host把HttpServletResponse对象返回给Engine。
- Engine把HttpServletResponse对象返回Connector。
- Connector把HttpServletResponse对象返回给客户Browser。
【问题】Tomcat 有哪几种Connector 运行模式(优化)?
- BIO:一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。Tomcat7版本或更低版本中,在Linux系统中默认使用这种方式。
- NIO:利用Java的异步IO处理,可以通过少量的线程处理大量的请求。tomcat8.0.x中默认使用的是NIO。
- APR:即Apache Portable Runtime,从操作系统层面解决io阻塞问题。Tomcat7或Tomcat8在Win7或以上的系统中启动默认使用这种方式。
【问题】Tomcat有几种部署方式?
- 利用Tomcat的自动部署:把web应用拷贝到webapps目录(生产环境不建议放在该目录中)。Tomcat在启动时会加载目录下的应用,并将编译后的结果放入work目录下。
- 使用Manager App控制台部署:在tomcat主页点击“Manager App” 进入应用管理控制台,可以指定一个web应用的路径或war文件。
- 修改conf/server.xml文件部署:在server.xml文件中,增加Context节点可以部署应用。
- 增加自定义的Web部署文件:在conf/Catalina/localhost/路径下增加 xyz.xml文件,内容是Context节点,可以部署应用。
【问题】Tomcat容器是如何创建servlet类实例?用到了什么原理?
- 当容器启动时,会读取在webapps目录下所有的web应用中的web.xml文件,然后对 xml文件进行解析,并读取servlet注册信息。然后,将每个应用中注册的servlet类都进行加载,并通过 反射的方式实例化。(有时候也是在第一次请求时实例化)
- 在servlet注册时加上1如果为正数,则在一开始就实例化,如果不写或为负数,则第一次请求实例化。
【问题】如何优化Tomcat?tomcat作为Web服务器,它的处理性能直接关系到用户体验,下面是几种常见的优化措施:
- 掉对web.xml的监视,把jsp提前编辑成Servlet。
- 有富余物理内存的情况,加大tomcat使用的jvm的内存服务器所能提供CPU、内存、硬盘的性能对处理能力有决定性影响。
- 利用缓存和压缩:对于静态页面最好是能够缓存起来,这样就不必每次从磁盘上读。这里我们采用了Nginx作为缓存服务器,将图片、css、js文件都进行了缓存,有效的减少了后端tomcat的访问。
采用集群:单个服务器性能总是有限的,最好的办法自然是实现横向扩展,那么组建tomcat集群是有效提升性能的手段。我们还是采用了Nginx来作为请求分流的服务器,后端多个tomcat共享session来协同工作。
优化线程数优化:找到Connector port="8080" protocol="HTTP/1.1",增加maxThreads和acceptCount属性(使acceptCount大于等于maxThreads)。
使用线程池优化:在server.xml中增加executor节点,然后配置connector的executor属性。
内存优化:因为tomcat启动起来后就是一个java进程,所以这块可以参照JVM部分的优化思路。
1,基本组件
1.1,HelloWord
创建Java项目,导入包:
- hutool-all-4.3.1.jar: hutool 工具类包,提供各种各样的便利工具。
- jsoup-1.12.1.jar: 解析xml用。
- jspc_all.jar:把 jsp 编译成为 servlet 用,这是阅读了 tomcat源码,然后对其进行了修改与提取之后,专门提供给本项目使用。
- junit-4.9.jar:单元测试。
- log4j-1.2.17.jar:log4j日志。
- servlet-api.jar:servlet 包。
Hello DiyTomcat
package com.ysy.diytomcat; import cn.hutool.core.util.NetUtil; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Bootstrap { public static void main(String[] args) { try { int port = 18080; //NetUtil.isUsableLocalPort 用来判断端口是否被占用,返回 true 表示没有被占用。 if (!NetUtil.isUsableLocalPort(port)) { System.out.println(port + " 端口已经被占用了!"); return; } //在端口18080上启动 ServerSocket。 服务端和浏览器通信是通过 Socket进行通信的,所以这里需要启动一个 ServerSocket。 ServerSocket ss = new ServerSocket(port); while (true) { //这表示收到一个浏览器客户端的请求 Socket s = ss.accept(); //打开输入流,准备接受浏览器提交的信息 InputStream is = s.getInputStream(); //准备一个长度是 1024 的字节数组,把浏览器的信息读取出来放进去。 int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; is.read(buffer); //把字节数组转换成字符串,并且打印出来 String requestString = new String(buffer, "utf-8"); System.out.println("浏览器的输入信息: \r\n" + requestString); //打开输出流,准备给客户端输出信息 OutputStream os = s.getOutputStream(); //这里准备发送给给客户端的数据。 String response_head = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n"; String responseString = "Hello DIY Tomcat"; responseString = response_head + responseString; //把字符串转换成字节数组发送出去 os.write(responseString.getBytes()); os.flush(); //关闭客户端对应的 socket s.close(); } } catch (IOException e) { e.printStackTrace(); } } }
发给浏览器的信息是Hello DIY Tomcat, 但是在前面还增加了一个 response_head, 因为作为web 服务器,和浏览器之间通信,需要遵循 http 协议,所以需要加上一个头信息:response_head。
1.2,MiniBrowser
为了更好的理解浏览器是如何与服务器进行通信的,做了一个迷你浏览器 MiniBrowser。这个浏览器会模拟发送 Http 协议的请求,并且获取完整的 Http 响应,通过这种方式,可以更好的理解浏览器与服务器是如何通信。
MiniBrowser 主要提供如下方法:
- getHttpBytes 返回二进制的 http 响应。
- getHttpString 返回字符串的 http 响应。
- getContentBytes 返回二进制的 http 响应内容 (可简单理解为去掉头的 html 部分)。
- getContentString 返回字符串的 http 响应内容 (可简单理解为去掉头的 html 部分)。
- 以上4个方法都增加了个 gzip 参数,可以获取压缩后的数据,便于实现 gzip 压缩。
package com.ysy.diytomcat.util; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * @author za-lincanjian * @注意:核心是方法:getHttpBytes,看懂这个方法就看懂全部了 */ public class MiniBrowser { public static void main(String[] args) { //初始化请求地址,这个请求地址就是待会会去链接的地址 String url = "http://static.how2j.cn/diytomcat.html"; //调用该方法,获取 http请求返回值的返回内容(简而言之就是去除掉了返回头的那些字符串)(请进到这个调用方法继续看) String contentString = getContentString(url,false); System.out.println(contentString); //这个方法就是获取全部的Http返回内容的字符串的方法了,各位看了刚才的一些解析这个方法就没啥好说的了 String httpString = getHttpString(url,false); System.out.println(httpString); } //这些重载方法其实就是备用的,以后可以直接调url,默认不gzip public static byte[] getContentBytes(String url) { return getContentBytes(url,false); } //这些重载方法其实就是备用的,以后可以直接调url,默认不gzip public static String getContentString(String url) { return getContentString(url,false); } public static String getContentString(String url, boolean gzip) { //这里获取返回体具体内容的字节数组,请跟进去看 byte[] result = getContentBytes(url,gzip); //getContentString 表示获取内容的字符串,我们获取到具体内容的字节数组后还需要进行编码 if (result == null) { return null; } //这里就是一个编码过程了,我这里跟源代码不同,用StandarCahrset这个类可以避免抛异常,这里引入一个知识,因为这个是个常量,jvm可以知道你会选的是utf-8,所以不要求你抛异常 return new String(result, StandardCharsets.UTF_8).trim(); } public static byte[] getContentBytes(String url, boolean gzip) { //这里是真正的逻辑,就是与请求地址建立连接的逻辑,是整个类的核心,其他方法都只是处理这个方法返回值的一些逻辑而已 byte[] response = getHttpBytes(url,gzip); //这个doubleReturnq其实是这样来的:我们获取的返回值正常其实是这样的 /**(响应头部分) * xxxxx * xxxxx * xxxxx * * (具体内容部分,在这个代码中是hello diytomcat) * xxx */ //也就是说响应头部分和具体内容部分其实隔了一行, \r表示回到行首\n表示换到下一行,那么\r\n就相当于说先到了空格一行的那一行的行首,接着又到了具体内容的那部分的行首 byte[] doubleReturn = "\r\n\r\n".getBytes(); //接着这里初始化一个记录值,做记录用,往下看 int pos = -1; //开始遍历返回内容 for (int i = 0;i < response.length - doubleReturn.length;i++) { //这里的意思就是不断去初始化一个数组(从原数组进行拷贝),目的其实是为了获取到\r\n这一行的起始位置 byte[] temp = Arrays.copyOfRange(response,i,i + doubleReturn.length); //来到这里,就是比较内容,当走到这里,说明temp这个字节数组的内容就是\r\n\r\n的内容了,说明我们找到了他的其实位置 if (Arrays.equals(temp,doubleReturn)) { //将pos等于i,记录位置 pos = i; break; } } //如果没记录到,那就说明压根没具体内容,那其实就是null if (-1 == pos) { return null; } //接着pos就是\r\n\n的第一个\的这个位置,加上\r\n\r\n的长度,相当于来到了具体内容的其实位置 pos += doubleReturn.length; //最后,确定了具体内容是在哪个字节开始,就拷贝这部分内容返回 byte[] result = Arrays.copyOfRange(response,pos,response.length); return result; } public static String getHttpString(String url,boolean gzip) { //这里也没啥了,就是少了截取内容的那部分操作,直接就返回整个返回值的字节数组出来 byte[] bytes = getHttpBytes(url,gzip); return new String(bytes).trim(); } //这些重载方法其实就是备用的,以后可以直接调url,默认不gzip public static String getHttpString(String url) { return getHttpString(url,false); } public static byte[] getHttpBytes(String url, boolean gzip) { //首先初始化一个返回值,这个返回值是一个字节数组,utf-8编码的 byte[] result = null; try { //通过url来new一个URL对象,这样你就不用自己去截取他的端口啊或者请求路径啥的,可以直接调他的方法获取 URL u = new URL(url); //开启一个socket链接,client指的就是你现在的这台计算机 Socket client = new Socket(); //获取到端口号,要是端口号是-1,那就默认取80端口(这个端口也是web常用端口) int port = u.getPort(); if (port == -1) { port = 80; } //这个是socket编程的内容,简单来说就是通过一个host+端口,和这个url建立连接 InetSocketAddress inetSocketAddress = new InetSocketAddress(u.getHost(),port); //开始连接了,1000是超时时间,等于说超过1秒就算你超时了 client.connect(inetSocketAddress,1000); //初始化请求头 Map<String,String> requestHeaders = new HashMap<>(); //这几个参数都是http请求时会带上的请求头 requestHeaders.put("Host", u.getHost() + ":" + port); requestHeaders.put("Accept", "text/html"); requestHeaders.put("Connection", "close"); requestHeaders.put("User-Agent", "how2j mini browser / java1.8"); //gzip是确定客户端或服务器端是否支持压缩 if (gzip) { requestHeaders.put("Accept_Encoding", "gzip"); } //获取到path,其实就是/diytomcat.html,如果没有的话就默认是/ String path = u.getPath(); if (path.length() == 0) { path = "/"; } //接着开始拼接请求的字符串,其实所谓的请求头和请求内容就是这么一串字符串拼接出来 String firstLine = "GET " + path + " HTTP/1.1\r\n"; StringBuffer httpRequestString = new StringBuffer(); //拼接firstLine的内容 httpRequestString.append(firstLine); Set<String> headers = requestHeaders.keySet(); //遍历header的那个map进行拼接 for (String header : headers) { String headerLine = header + ":" + requestHeaders.get(header) + "\r\n"; httpRequestString.append(headerLine); } /**走到这的时候,httpRequestString已经拼接好了,内容是: GET /diytomcat.html HTTP/1.1 Accept:text/html Connection:close User-Agent:how2j mini browser / java1.8 Host:static.how2j.cn:80 */ //通过输出流,将这么一串字符串输出给连接的地址,后面的true是autoFlush,表示开始自动刷新 PrintWriter printWriter = new PrintWriter(client.getOutputStream(),true); printWriter.println(httpRequestString); //这时候你已经将需要的请求所需的字符串发给上面那个url了,其实所谓的http协议就是这样,你发给他这么一串符合规范的字符串,他就给你响应,接着他那边就给你返回数据 //所以这时候我们开启一个输出流 InputStream inputStream = client.getInputStream(); //一次接受1m的数据就行了,1024byte = 1m int bufferSize = 1024; //这里初始化一个输出流,待会存取url返回给我们的数据用 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); //开始new一个1m大小的字节数组 byte[] buffer = new byte[bufferSize]; //循环继续读取 while (true) { //从输入流获取数据,调read方法存到buffer数组中 int length = inputStream.read(buffer); //读到的长度如果是-1,说明没读到数据了,直接退出 if (length == -1) { break; } //接着先将读到的1m数据输出到我们初始化的那个输出流中 byteArrayOutputStream.write(buffer,0,length); //这里是一个结束的操作,length != bufferSize,说明已经是最后一次读取了,为什么这么说? //举个例子,如果你的数据是1025字节,当你第二次循环的时候就是只有一个字节了,这时候就说明处理完这一个字节的数组就可以结束了,因为已经没数据了 if (length != bufferSize) { break; } } //通过方法,将这个输出流返回成字节数组result,为什么要用这个输出流来存返回的字节数组呢?因为如果你用数组的话其实你不能确定 //整个返回数据有多大,用集合理论上是可以实现的,各位有兴趣可以试试 result = byteArrayOutputStream.toByteArray(); //这是个好习惯,不过最好是放在finally进行关闭比较好,这里就是关闭连接了 client.close(); } catch (Exception e) { //接着这里是异常打印 e.printStackTrace(); //这里是将返回的异常信息进行字节数组编码,其实就是兼容这个方法 try { result = e.toString().getBytes("utf-8"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } } //返回结果 return result; } }
1.3,自动测试
不断对这个项目进行持续的改造和重构, 那么如何保证重构之后,以前的功能依旧可以使用呢? 当然可以通过手动测试来进行,但是随着功能越来越多,频繁的手动测试开始变得无聊和难以持续。 所以这个时候,我们就会引入单元自动测试来检查在进行了代码修改之后,以前的代码依然可以其作用。 如果测试失败,那么就可以及时得到提醒有地方改得不对,这样就可以在重构的同时保证项目本身的质量了。
package com.ysy.diytomcat.test; import cn.hutool.core.util.*; import com.ysy.diytomcat.util.MiniBrowser; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; public class TestTomcat { private static int port = 18080; private static String ip = "127.0.0.1"; @BeforeClass public static void beforeClass() { //所有测试开始前看diy tomcat 是否已经启动了 if (NetUtil.isUsableLocalPort(port)) { System.err.println("请先启动 位于端口: " + port + " 的diy tomcat,否则无法进行单元测试"); System.exit(1); } else { System.out.println("检测到 diy tomcat已经启动,开始进行单元测试"); } } @Test public void testHelloTomcat() { String html = getContentString("/"); Assert.assertEquals(html, "Hello DIY Tomcat"); } //准备一个工具方法,用来获取网页返回。 private String getContentString(String uri) { String url = StrUtil.format("http://{}:{}{}", ip, port, uri); String content = MiniBrowser.getContentString(url); return content; } }
1.4,Request对象
目前我们从浏览器获取信息是所有的都放在一个 String 里面,伴随着服务器功能的开发,需要从这个字符串里解构出更加丰富的信息,为了方便后续的解析工作,我们引入 Request 对象,用来代表浏览器发过来的请求信息。 伴随着服务器的功能完善,这个Request对象会不断地改进和重构。
改进MiniBrowser:添加readBytes通用方法(方便后续调用)
public static byte[] readBytes(InputStream is) throws IOException { int buffer_size = 1024; byte buffer[] = new byte[buffer_size]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { int length = is.read(buffer); if (-1 == length) break; baos.write(buffer, 0, length); if (length != buffer_size) break; } byte[] result = baos.toByteArray(); return result; }
Request对象:创建 Request 对象用来解析 requestString 和 uri。
package com.ysy.diytomcat.http; import cn.hutool.core.util.StrUtil; import com.ysy.diytomcat.util.MiniBrowser; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class Request { private String requestString; private String uri; private Socket socket; public Request(Socket socket) throws IOException { this.socket = socket; parseHttpRequest(); if (StrUtil.isEmpty(requestString)) return; parseUri(); } //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。 private void parseHttpRequest() throws IOException { InputStream is = this.socket.getInputStream(); byte[] bytes = MiniBrowser.readBytes(is); requestString = new String(bytes, "utf-8"); } //解析真实链接 private void parseUri() { String temp = StrUtil.subBetween(requestString, " ", " "); //不包括?,链接即是真实链接 if (!StrUtil.contains(temp, '?')) { uri = temp; return; } //包括?,截取?之前的字符串 temp = StrUtil.subBefore(temp, '?', false); uri = temp; } public String getUri() { return uri; } public String getRequestString() { return requestString; } }
Bootstrap:修改 Bootstrap 使得数据从 Request 对象里获取来。
package com.ysy.diytomcat; import cn.hutool.core.util.NetUtil; import com.ysy.diytomcat.http.Request; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Bootstrap { public static void main(String[] args) { try { int port = 18080; if (!NetUtil.isUsableLocalPort(port)) { System.out.println(port + " 端口已经被占用了!"); return; } ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); OutputStream os = s.getOutputStream(); String response_head = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n"; String responseString = "Hello DIY Tomcat"; responseString = response_head + responseString; os.write(responseString.getBytes()); os.flush(); s.close(); } } catch (IOException e) { e.printStackTrace(); } } }
再运行 TestTomcat,此时单元测试的作用就可以显示出来了。经过上述的重构之后,单元测试依然可以通过,那么说明并没有影响到原来的功能性。
浏览器的输入信息: uri: null 浏览器的输入信息: GET / HTTP/1.1 Accept:text/html Connection:close User-Agent:how2j mini brower / java1.8 Host:127.0.0.1:18080 uri: /
运行单元测试之后,会看到上面所示的情况,会发送两条消息。第二条消息是正常的测试信息,那么第一条是什么呢?它是TestTomcat中beforeClass 里的检查端口是否启动的代码导致的空消息。
1.5,Response对象
首先准备一个常量工具类,用于存放响应的头信息模板。
public class Constant { public final static String response_head_202 = "HTTP/1.1 200 OK\r\n Content-Type: {}\r\n\r\n"; }
创建 Response 类
package com.ysy.diytomcat.http; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; public class Response { //用于存放返回的 html 文本 private StringWriter stringWriter; private PrintWriter writer; //contentType就是对应响应头信息里的 Content-type ,默认是 "text/html"。 private String contentType; public Response() { //用于提供一个 getWriter() 方法,这样就可以像 HttpServletResponse 那样写成 response.getWriter().println(); 这种风格了。 this.stringWriter = new StringWriter(); this.writer = new PrintWriter(stringWriter); this.contentType = "text/html"; } public String getContentType() { return contentType; } public PrintWriter getWriter() { return writer; } public byte[] getBody() throws UnsupportedEncodingException { String content = stringWriter.toString(); byte[] body = content.getBytes("utf-8"); return body; } }
重构 Bootstrap
package com.ysy.diytomcat; import cn.hutool.core.util.*; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Bootstrap { public static void main(String[] args) { try { int port = 18080; if (!NetUtil.isUsableLocalPort(port)) { System.out.println(port + " 端口已经被占用了!"); return; } ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String html = "Hello DIY Tomcat"; response.getWriter().println(html); //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } } catch (IOException e) { e.printStackTrace(); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
为什么要把头和主体分开,而不直接使用合并的 html 呢? 因为在接下来的工作里,会对头部做更复杂的处理, 主体部分也会面对二进制文件和gzip压缩,现在分开来,后续处理起来更加游刃有余。
1.6,文本文件
根据 tomcat 的目录结构,我们在 diytomcat 目录下新建 webapps/ROOT 目录,然后在里面新建文件 a.html,内容如下:
I am fun, Thanks.
新增 webappsFolder 和 rootFolder, 方便后续使用。
public final static File webappsFolder = new File(SystemUtil.get("user.dir"),"webapps"); public final static File rootFolder = new File(webappsFolder,"ROOT");
重构 Bootstrap
package com.ysy.diytomcat; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.*; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Bootstrap { public static void main(String[] args) { try { int port = 18080; if (!NetUtil.isUsableLocalPort(port)) { System.out.println(port + " 端口已经被占用了!"); return; } ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) continue; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file File file = FileUtil.file(Constant.rootFolder, fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } } catch (IOException e) { e.printStackTrace(); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
1.7,日志
tomcat 的日志会放在 logs/ 目录下,日子文件默认是 "catalina“, 然后随着日期滚动,接下来我们就会通过 log4j 来模仿这种做法。
log4j 配置文件
#只输出 info 以上的,输出到连个自定义appender: stdout和R. log4j.rootLogger=info, stdout, R #输出到控制台,格式是 优先级,后面跟上 日期 类名称 方法名称 换行 优先级 消息 换行 log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{MM dd, yyyy HH:mm:ss a} %c %M%n%-5p: %m%n #输出到每日滚动文件, 文件名称是 logs/catalina. 如果发生滚动,日期格式是 yyyy-MM-dd, 格式同上。 log4j.appender.R=org.apache.log4j.DailyRollingFileAppender log4j.appender.R.File=logs/catalina log4j.appender.R.DatePattern='.'yyyy-MM-dd'.log' log4j.appender.R.layout=org.apache.log4j.PatternLayout log4j.appender.R.layout.ConversionPattern=%d{MM dd, yyyy HH:mm:ss a} %c %M%n%-5p: %m%n
Bootstrap改造:(1)像 tomcat 那样 一开始打印 jvm 信息。(2)把端口占用提示信息注释掉,当真正异常发生的时候,就会打印出来。
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { public static void main(String[] args) { try { logJVM(); int port = 18080; //把端口占用提示信息注释掉,当真正异常发生的时候,就会打印出来 // if (!NetUtil.isUsableLocalPort(port)) { // System.out.println(port + " 端口已经被占用了!"); // return; // } ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) continue; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file File file = FileUtil.file(Constant.rootFolder, fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } } catch (IOException e) { e.printStackTrace(); } } private static void logJVM() { Map<String,String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key+":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
重复运行Bootstrap
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : Server version: How2J DiyTomcat/1.0.1 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : Server built: 2020-04-08 10:20:22 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : Server number: 1.0.1 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : OS Name : Windows 10 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : OS Version: 10.0 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : Architecture: amd64 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : Java Home: E:\Java\jdk-1.8\jre 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : JVM Version: 1.8.0_151-b12 08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM INFO : JVM Vendor: Oracle Corporation java.net.BindException: Address already in use: JVM_Bind at java.net.DualStackPlainSocketImpl.bind0(Native Method) at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106) at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387) at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190) at java.net.ServerSocket.bind(ServerSocket.java:375) at java.net.ServerSocket.<init>(ServerSocket.java:237) at java.net.ServerSocket.<init>(ServerSocket.java:128) at com.ysy.diytomcat.Bootstrap.main(Bootstrap.java:27)
2,多线程,多项目
2.1,耗时任务
耗时任务指的是访问某个页面,比较消耗时间,比如连接数据库什么的。 这里为了简化,故意设计成访问 timeConsume.html会花掉1秒钟。
Bootstrap改造:增加对 timeConsume.html 的判断。
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { public static void main(String[] args) { try { logJVM(); int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) continue; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file File file = FileUtil.file(Constant.rootFolder, fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if(fileName.equals("timeConsume.html")){ ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String,String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key+":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
2.2,线程池
当前的服务器是单线程的,处理一个请求之后,才能处理下一个,这样效率肯定是很低的,所以我们要把它改造成多线程的,可以同时处理多个请求。而每当有请求来的时候,就创建一个新的线程,这种方式开销非常大,所以我们要使用 线程池的方式对线程进行重复利用。
首先准备一个工具类 ThreadUtil, 里面创建了一个 ThreadPoolExecutor 对象。
- 第一个参数 20 表示线程池初始有20个核心线程数。
- 第二个参数 100 表示最多会有 100个线程。
- 第三个和第四个参数 表示如果 新增加出来的线程如果空闲时间超过 60秒,那么就会被回收,最后保留 20个线程。
- 第五个参数 new LinkedBlockingQueue<Runnable>(10), 表示当有很多请求短时间过来,使得20根核心线程都满了之后,并不会马上分配新的线程处理更多的请求, 而是把这些请求放过在 这个LinkedBlockingQueue里, 当核心线程忙过来了,就会来处理 这个队列里的请求。 只有当处理不过来的请求数目超过 了 10个之后,才会增加更多的线程来处理。
package com.ysy.diytomcat.util; import java.util.concurrent.*; public class ThreadPoolUtil { private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(20, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10)); public static void run(Runnable r) { threadPool.execute(r); } }
改造Bootstrap:当有请求来的时候,就创建一个 Runnable 任务,并且把他丢进线程池运行即可,然后就再去准备接受下一个请求。
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { public static void main(String[] args) { try { logJVM(); int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) return; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file File file = FileUtil.file(Constant.rootFolder, fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
2.3,多应用
目前的做法只支持 ROOT 这么一个应用,那么我们将继续进行改造使得服务器只是多个应用。
- 首先在 webapps 下新建目录 a, 并在其中放入一个 index.html 。
Hello DIY Tomcat from index.html@a
- 新建 Context 类,来代表一个应用,它有两个属性, path 和 docBase,以及对应的 getter, setter。path 表示访问的路径;docBase 表示对应在文件系统中的位置;
package com.ysy.diytomcat.catalina; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.log.LogFactory; public class Context { private String path; private String docBase; public Context(String path, String docBase) { TimeInterval timeInterval = DateUtil.timer(); this.path = path; this.docBase = docBase; LogFactory.get().info("Deploying web application directory {}", this.docBase); LogFactory.get().info("Deployment of web application directory {} has finished in {} ms", this.docBase, timeInterval.intervalMs()); } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getDocBase() { return docBase; } public void setDocBase(String docBase) { this.docBase = docBase; } }
对 Bootstrap 进行改造
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { //声明一个 contextMap 用于存放路径和Context 的映射。 public static Map<String, Context> contextMap = new HashMap<>(); public static void main(String[] args) { try { logJVM(); scanContextsOnWebAppsFolder(); int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) return; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file File file = FileUtil.file(Constant.rootFolder, fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } //创建 scanContextsOnWebAppsFolder 方法,用于扫描 webapps 文件夹下的目录,对这些目录调用 loadContext 进行加载。 private static void scanContextsOnWebAppsFolder() { File[] folders = Constant.webappsFolder.listFiles(); for (File folder : folders) { if (!folder.isDirectory()) continue; loadContext(folder); } } //加载这个目录成为 Context 对象。 private static void loadContext(File folder) { String path = folder.getName(); if ("ROOT".equals(path)) path = "/"; else path = "/" + path; String docBase = folder.getAbsolutePath(); Context context = new Context(path, docBase); contextMap.put(context.getPath(), context); } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
改造 Request
package com.ysy.diytomcat.http; import cn.hutool.core.util.StrUtil; import com.ysy.diytomcat.Bootstrap; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.util.MiniBrowser; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class Request { private String requestString; private String uri; private Socket socket; private Context context; public Request(Socket socket) throws IOException { this.socket = socket; parseHttpRequest(); if (StrUtil.isEmpty(requestString) || requestString.equals("")) return; parseUri(); //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。 parseContext(); if (!"/".equals(context.getPath())) uri = StrUtil.removePrefix(uri, context.getPath()); } //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。 private void parseContext() { String path = StrUtil.subBetween(uri, "/", "/"); if (null == path) path = "/"; else path = "/" + path; context = Bootstrap.contextMap.get(path); if (null == context) context = Bootstrap.contextMap.get("/"); } //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。 private void parseHttpRequest() throws IOException { InputStream is = this.socket.getInputStream(); byte[] bytes = MiniBrowser.readBytes(is); requestString = new String(bytes, "utf-8"); } //解析真实链接 private void parseUri() { String temp = StrUtil.subBetween(requestString, " ", " "); //不包括?,链接即是真实链接 if (!StrUtil.contains(temp, '?')) { uri = temp; return; } //包括?,截取?之前的字符串 temp = StrUtil.subBefore(temp, '?', false); uri = temp; } public Context getContext() { return context; } public String getUri() { return uri; } public String getRequestString() { return requestString; } }
2.4,配置型多应用
多应用开发里,是扫描 webapps 文件夹,这节实现像 tomcat 那样通过 配置 server.xml 来实现这个效果。
- 在项目下新建个 conf 目录,然后新建 server.xml。
<?xml version="1.0" encoding="UTF-8"?> <Server> <Context path="/b" docBase="d:/project/diytomcat/b" /> </Server>
- 在 E:/IDEA/workspace/DiyTomcat/webapps/ 创建 b目录,并且在其中存放 index.html, 内容如下:Hello DIY Tomcat from index.html@b
- 修改Constant,增加两个常量,用于定位 server.xml
public static final File confFolder = new File(SystemUtil.get("user.dir"),"conf"); public static final File serverXmlFile = new File(confFolder, "server.xml");
- 准备工具类 ServerXMLUtil,这个工具类使用了第三方工具 jsoup。
package com.ysy.diytomcat.util; import cn.hutool.core.io.FileUtil; import com.ysy.diytomcat.catalina.Context; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.util.ArrayList; import java.util.List; public class ServerXMLUtil { public static List<Context> getContexts() { List<Context> result = new ArrayList<>(); //获取 server.xml 的内容 String xml = FileUtil.readUtf8String(Constant.serverXmlFile); //转换成 jsoup document Document d = Jsoup.parse(xml); //查询所有的 Context 节点 Elements es = d.select("Context"); //遍历这些节点,并获取对应的 path和docBase ,以生成 Context 对象, 然后放进 result 返回。 for (Element e : es) { String path = e.attr("path"); String docBase = e.attr("docBase"); Context context = new Context(path, docBase); result.add(context); } return result; } }
- 修改Bootstrap
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { //声明一个 contextMap 用于存放路径和Context 的映射。 public static Map<String, Context> contextMap = new HashMap<>(); public static void main(String[] args) { try { logJVM(); scanContextsOnWebAppsFolder(); scanContextsInServerXML(); int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) return; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 Context context = request.getContext(); if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象 File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } //创建scanContextsInServerXML, 通过 ServerXMLUtil 获取 context, 放进 contextMap里。 private static void scanContextsInServerXML() { List<Context> contexts = ServerXMLUtil.getContexts(); for (Context context : contexts) { contextMap.put(context.getPath(), context); } } //创建 scanContextsOnWebAppsFolder 方法,用于扫描 webapps 文件夹下的目录,对这些目录调用 loadContext 进行加载。 private static void scanContextsOnWebAppsFolder() { File[] folders = Constant.webappsFolder.listFiles(); for (File folder : folders) { if (!folder.isDirectory()) continue; loadContext(folder); } } //加载这个目录成为 Context 对象。 private static void loadContext(File folder) { String path = folder.getName(); if ("ROOT".equals(path)) path = "/"; else path = "/" + path; String docBase = folder.getAbsolutePath(); Context context = new Context(path, docBase); contextMap.put(context.getPath(), context); } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
3,Tomcat内置对象
3.1,Host
修改Server.xml:Host 的意思是虚拟主机,通常都是 localhost,即表示本机。
<?xml version="1.0" encoding="UTF-8"?> <Server> <Host name="localhost"> <Context path="/b" docBase="E:/IDEA/workspace/DiyTomcat/webapps/b"/> </Host> </Server>
修改ServerXMLUtil:增加一个 getHostName 来解析 host 元素下的 name 属性。
package com.ysy.diytomcat.util; import cn.hutool.core.io.FileUtil; import com.ysy.diytomcat.catalina.Context; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.util.ArrayList; import java.util.List; public class ServerXMLUtil { public static List<Context> getContexts() { List<Context> result = new ArrayList<>(); //获取 server.xml 的内容 String xml = FileUtil.readUtf8String(Constant.serverXmlFile); //转换成 jsoup document Document d = Jsoup.parse(xml); //查询所有的 Context 节点 Elements es = d.select("Context"); //遍历这些节点,并获取对应的 path和docBase ,以生成 Context 对象, 然后放进 result 返回。 for (Element e : es) { String path = e.attr("path"); String docBase = e.attr("docBase"); Context context = new Context(path, docBase); result.add(context); } return result; } public static String getHostName() { String xml = FileUtil.readUtf8String(Constant.serverXmlFile); Document d = Jsoup.parse(xml); Element host = d.select("Host").first(); return host.attr("name"); } }
创建 host类:有 name 和 contextMap 属性。
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.Constant; import com.ysy.diytomcat.util.ServerXMLUtil; import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; public class Host { private String name; //contextMap 其实就是本来在 bootstrap 里的 contextMap , 只不过挪到这里来了。 private Map<String, Context> contextMap; public Host() { this.contextMap = new HashMap<>(); this.name = ServerXMLUtil.getHostName(); scanContextsOnWebAppsFolder(); scanContextsInServerXML(); } public String getName() { return name; } public void setName(String name) { this.name = name; } private void scanContextsInServerXML() { List<Context> contexts = ServerXMLUtil.getContexts(); for (Context context : contexts) { contextMap.put(context.getPath(), context); } } private void scanContextsOnWebAppsFolder() { File[] folders = Constant.webappsFolder.listFiles(); for (File folder : folders) { if (!folder.isDirectory()) continue; loadContext(folder); } } private void loadContext(File folder) { String path = folder.getName(); if ("ROOT".equals(path)) path = "/"; else path = "/" + path; String docBase = folder.getAbsolutePath(); Context context = new Context(path, docBase); contextMap.put(context.getPath(), context); } public Context getContext(String path) { return contextMap.get(path); } }
修改Request:因为 Request 以前是从 Bootstrap.contextMap 里获取 context 对象的,伴随 contextMap 移动到 Host ,为了能够实现原来的功能,需要在 Request 里增加个 host对象。
package com.ysy.diytomcat.http; import cn.hutool.core.util.StrUtil; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.catalina.Host; import com.ysy.diytomcat.util.MiniBrowser; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class Request { private String requestString; private String uri; private Socket socket; private Context context; private Host host; public Request(Socket socket, Host host) throws IOException { this.socket = socket; this.host = host; parseHttpRequest(); if (StrUtil.isEmpty(requestString) || requestString.equals("")) return; parseUri(); //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。 parseContext(); if (!"/".equals(context.getPath())) uri = StrUtil.removePrefix(uri, context.getPath()); } //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。 private void parseContext() { String path = StrUtil.subBetween(uri, "/", "/"); if (null == path) path = "/"; else path = "/" + path; context = host.getContext(path); if (null == context) context = host.getContext("/"); } //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。 private void parseHttpRequest() throws IOException { InputStream is = this.socket.getInputStream(); byte[] bytes = MiniBrowser.readBytes(is); requestString = new String(bytes, "utf-8"); } //解析真实链接 private void parseUri() { String temp = StrUtil.subBetween(requestString, " ", " "); //不包括?,链接即是真实链接 if (!StrUtil.contains(temp, '?')) { uri = temp; return; } //包括?,截取?之前的字符串 temp = StrUtil.subBefore(temp, '?', false); uri = temp; } public Context getContext() { return context; } public String getUri() { return uri; } public String getRequestString() { return requestString; } }
修改Bootstrap:伴随着 contextMap 移动到host对象,Bootstrap 就把 contextMap 相关的代码都移动过去了。
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.catalina.Host; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { //声明一个 contextMap 用于存放路径和Context 的映射。 public static Map<String, Context> contextMap = new HashMap<>(); public static void main(String[] args) { try { logJVM(); int port = 18080; Host host = new Host(); ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, host); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) return; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 Context context = request.getContext(); if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象 File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
3.2,Engine
在 tomcat 里 Engine表示 servlet 引擎,用来处理 servlet 的请求。在 Host 上增加 Engine 节点, 它有个defaultHost="localhost" 表示默认的 Host 是名称是 "localhost" 的 Host。
修改 server.xml
<?xml version="1.0" encoding="UTF-8"?> <Server> <Engine defaultHost="localhost"> <Host name = "localhost"> <Context path="/b" docBase="E:/IDEA/workspace/DiyTomcat/webapps/b" /> </Host> </Engine> </Server>
创建Engine
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.ServerXMLUtil; import java.util.List; public class Engine { private String defaultHost; private List<Host> hosts; public Engine() { this.defaultHost = ServerXMLUtil.getEngineDefaultHost(); this.hosts = ServerXMLUtil.getHosts(this); checkDefault(); } private void checkDefault() { if (null == getDefaultHost()) throw new RuntimeException("the defaultHost" + defaultHost + " does not exist!"); } public Host getDefaultHost() { for (Host host : hosts) { if (host.getName().equals(defaultHost)) return host; } return null; } }
修改Host
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.Constant; import com.ysy.diytomcat.util.ServerXMLUtil; import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; public class Host { private String name; //contextMap 其实就是本来在 bootstrap 里的 contextMap , 只不过挪到这里来了。 private Map<String, Context> contextMap; //增加 Engine 属性 private Engine engine; public Host() { this.contextMap = new HashMap<>(); this.name = ServerXMLUtil.getHostName(); //在构造方法中接受它 this.engine = engine; scanContextsOnWebAppsFolder(); scanContextsInServerXML(); } public String getName() { return name; } public void setName(String name) { this.name = name; } //创建scanContextsInServerXML, 通过 ServerXMLUtil 获取 context, 放进 contextMap里。 private void scanContextsInServerXML() { List<Context> contexts = ServerXMLUtil.getContexts(); for (Context context : contexts) { contextMap.put(context.getPath(), context); } } //创建 scanContextsOnWebAppsFolder 方法,用于扫描 webapps 文件夹下的目录,对这些目录调用 loadContext 进行加载。 private void scanContextsOnWebAppsFolder() { File[] folders = Constant.webappsFolder.listFiles(); for (File folder : folders) { if (!folder.isDirectory()) continue; loadContext(folder); } } //加载这个目录成为 Context 对象。 private void loadContext(File folder) { String path = folder.getName(); if ("ROOT".equals(path)) path = "/"; else path = "/" + path; String docBase = folder.getAbsolutePath(); Context context = new Context(path, docBase); contextMap.put(context.getPath(), context); } public Context getContext(String path) { return contextMap.get(path); } }
修改 ServerXMLUtil
package com.ysy.diytomcat.util; import cn.hutool.core.io.FileUtil; import com.ysy.diytomcat.catalina.*; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.util.ArrayList; import java.util.List; public class ServerXMLUtil { public static List<Context> getContexts() { List<Context> result = new ArrayList<>(); //获取 server.xml 的内容 String xml = FileUtil.readUtf8String(Constant.serverXmlFile); //转换成 jsoup document Document d = Jsoup.parse(xml); //查询所有的 Context 节点 Elements es = d.select("Context"); //遍历这些节点,并获取对应的 path和docBase ,以生成 Context 对象, 然后放进 result 返回。 for (Element e : es) { String path = e.attr("path"); String docBase = e.attr("docBase"); Context context = new Context(path, docBase); result.add(context); } return result; } public static String getEngineDefaultHost() { String xml = FileUtil.readUtf8String(Constant.serverXmlFile); Document d = Jsoup.parse(xml); Element host = d.select("Engine").first(); return host.attr("defaultHost"); } public static List<Host> getHosts(Engine engine) { List<Host> result = new ArrayList<>(); String xml = FileUtil.readUtf8String(Constant.serverXmlFile); Document d = Jsoup.parse(xml); Elements es = d.select("Host"); for (Element e : es) { String name = e.attr("name"); Host host = new Host(name, engine); result.add(host); } return result; } public static String getHostName() { String xml = FileUtil.readUtf8String(Constant.serverXmlFile); Document d = Jsoup.parse(xml); Element host = d.select("Host").first(); return host.attr("name"); } }
修改Request
package com.ysy.diytomcat.http; import cn.hutool.core.util.StrUtil; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.catalina.Engine; import com.ysy.diytomcat.catalina.Host; import com.ysy.diytomcat.util.MiniBrowser; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class Request { private String requestString; private String uri; private Socket socket; private Context context; private Engine engine; public Request(Socket socket, Engine engine) throws IOException { this.socket = socket; this.engine = engine; parseHttpRequest(); if (StrUtil.isEmpty(requestString) || requestString.equals("")) return; parseUri(); //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。 parseContext(); if (!"/".equals(context.getPath())) uri = StrUtil.removePrefix(uri, context.getPath()); } //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。 private void parseContext() { String path = StrUtil.subBetween(uri, "/", "/"); if (null == path) path = "/"; else path = "/" + path; context = engine.getDefaultHost().getContext(path); if (null == context) context = engine.getDefaultHost().getContext("/"); } //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。 private void parseHttpRequest() throws IOException { InputStream is = this.socket.getInputStream(); byte[] bytes = MiniBrowser.readBytes(is); requestString = new String(bytes, "utf-8"); } //解析真实链接 private void parseUri() { String temp = StrUtil.subBetween(requestString, " ", " "); //不包括?,链接即是真实链接 if (!StrUtil.contains(temp, '?')) { uri = temp; return; } //包括?,截取?之前的字符串 temp = StrUtil.subBefore(temp, '?', false); uri = temp; } public Context getContext() { return context; } public String getUri() { return uri; } public String getRequestString() { return requestString; } }
修改Bootstrap
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.catalina.Engine; import com.ysy.diytomcat.catalina.Host; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { //声明一个 contextMap 用于存放路径和Context 的映射。 public static Map<String, Context> contextMap = new HashMap<>(); public static void main(String[] args) { try { logJVM(); int port = 18080; //由开始创建 Host 对象变成现在的 创建 Engine 对象 Engine engine = new Engine(); ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, engine); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) return; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 Context context = request.getContext(); if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象 File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
3.3,Service
Service 是 Engine 的父节点,用于代表 tomcat 提供的服务。
修改server.xml:在Engine 上面增加个 Service 节点。
<?xml version="1.0" encoding="UTF-8"?> <Server> <Service name="Catalina"> <Engine defaultHost="localhost"> <Host name="localhost"> <Context path="/b" docBase="E:/IDEA/workspace/DiyTomcat/webapps/b"/> </Host> </Engine> </Service> </Server>
ServerXMLUtil中添加getServiceName():正好之前的 getHostName 不用了,改造成 getServiceName。
public static String getServiceName() { String xml = FileUtil.readUtf8String(Constant.serverXmlFile); Document d = Jsoup.parse(xml); Element host = d.select("Service").first(); return host.attr("name"); }
新增Service类:有name 和Engine 属性。一个 Service 下通常只有一个 Engine, 就不做成 List<Engine> 集合了。
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.ServerXMLUtil; public class Service { private String name; private Engine engine; public Service() { this.name = ServerXMLUtil.getServiceName(); this.engine = new Engine(this); } public Engine getEngine() { return engine; } }
修改Engine:改造一下,增加 Service 属性。
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.ServerXMLUtil; import java.util.List; public class Engine { private String defaultHost; private List<Host> hosts; private Service service; public Engine(Service service) { this.defaultHost = ServerXMLUtil.getEngineDefaultHost(); this.hosts = ServerXMLUtil.getHosts(this); this.service = service; checkDefault(); } private void checkDefault() { if (null == getDefaultHost()) throw new RuntimeException("the defaultHost" + defaultHost + " does not exist!"); } public Service getService() { return service; } public Host getDefaultHost() { for (Host host : hosts) { if (host.getName().equals(defaultHost)) return host; } return null; } }
修改Request:就像当初抛弃从 Host 切换到 Engine 一样,现在从 Engine 切换到 Service。
package com.ysy.diytomcat.http; import cn.hutool.core.util.StrUtil; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.catalina.Engine; import com.ysy.diytomcat.catalina.Host; import com.ysy.diytomcat.catalina.Service; import com.ysy.diytomcat.util.MiniBrowser; import java.io.IOException; import java.io.InputStream; import java.net.Socket; public class Request { private String requestString; private String uri; private Socket socket; private Context context; private Service service; public Request(Socket socket, Service service) throws IOException { this.socket = socket; this.service = service; parseHttpRequest(); if (StrUtil.isEmpty(requestString) || requestString.equals("")) return; parseUri(); //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。 parseContext(); if (!"/".equals(context.getPath())) uri = StrUtil.removePrefix(uri, context.getPath()); } //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。 private void parseContext() { String path = StrUtil.subBetween(uri, "/", "/"); if (null == path) path = "/"; else path = "/" + path; context = service.getEngine().getDefaultHost().getContext(path); if (null == context) context = service.getEngine().getDefaultHost().getContext("/"); } //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。 private void parseHttpRequest() throws IOException { InputStream is = this.socket.getInputStream(); byte[] bytes = MiniBrowser.readBytes(is); requestString = new String(bytes, "utf-8"); } //解析真实链接 private void parseUri() { String temp = StrUtil.subBetween(requestString, " ", " "); //不包括?,链接即是真实链接 if (!StrUtil.contains(temp, '?')) { uri = temp; return; } //包括?,截取?之前的字符串 temp = StrUtil.subBefore(temp, '?', false); uri = temp; } public Context getContext() { return context; } public String getUri() { return uri; } public String getRequestString() { return requestString; } }
修改 Bootstrap
package com.ysy.diytomcat; import cn.hutool.core.io.*; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.*; import cn.hutool.log.LogFactory; import cn.hutool.system.*; import com.ysy.diytomcat.catalina.Context; import com.ysy.diytomcat.catalina.Engine; import com.ysy.diytomcat.catalina.Host; import com.ysy.diytomcat.catalina.Service; import com.ysy.diytomcat.http.*; import com.ysy.diytomcat.util.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; public class Bootstrap { //声明一个 contextMap 用于存放路径和Context 的映射。 public static Map<String, Context> contextMap = new HashMap<>(); public static void main(String[] args) { try { logJVM(); int port = 18080; Service service = new Service(); ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, service); System.out.println("浏览器的输入信息: \r\n" + request.getRequestString()); System.out.println("uri: " + request.getUri()); Response response = new Response(); String uri = request.getUri(); //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。 if (null == uri) return; System.out.println(uri); //如果是 "/", 那么依然返回原字符串。 Context context = request.getContext(); if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html String fileName = StrUtil.removePrefix(uri, "/"); //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象 File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { //如果文件存在,那么获取内容并通过 response.getWriter 打印。 String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { //如果文件不存在,那么打印 File Not Found。 response.getWriter().println("File Not Found"); } } //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。 handle200(s, response); } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。 byte[] body = response.getBody(); //获取主题信息部分,即 html 对应的 字节数组。 byte[] responseBytes = new byte[head.length + body.length]; //拼接头信息和主题信息,成为一个响应字节数组。 ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); //close 自动 flush s.close(); } }
3.4,Server
Server 就代表最外层的 Server 元素,即服务器本身。
创建Server类:添加service属性,然后就把 Bootstrap 的内容搬过来了~
package com.ysy.diytomcat.catalina; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import com.ysy.diytomcat.util.* import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.log.LogFactory; import cn.hutool.system.SystemUtil; import com.ysy.diytomcat.http.*; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class Server { private Service service; public Server(){ this.service = new Service(this); } public void start(){ logJVM(); init(); } private void init() { try { int port = 18080; ServerSocket ss = new ServerSocket(port); while(true) { Socket s = ss.accept(); Runnable r = new Runnable(){ @Override public void run() { try { Request request = new Request(s,service); Response response = new Response(); String uri = request.getUri(); if(null==uri) return; System.out.println("uri:"+uri); Context context = request.getContext(); if("/".equals(uri)){ String html = "Hello DIY Tomcat from how2j.cn"; response.getWriter().println(html); } else{ String fileName = StrUtil.removePrefix(uri, "/"); File file = FileUtil.file(context.getDocBase(),fileName); if(file.exists()){ String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if(fileName.equals("timeConsume.html")){ ThreadUtil.sleep(1000); } } else{ response.getWriter().println("File Not Found"); } } handle200(s, response); } catch (IOException e) { e.printStackTrace(); } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String,String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key+":\t\t" + infos.get(key)); } } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); byte[] body = response.getBody(); byte[] responseBytes = new byte[head.length + body.length]; ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); s.close(); } }
修改Service:提供 server 属性和构造方法支持。
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.ServerXMLUtil; public class Service { private String name; private Engine engine; private Server server; public Service() { this.server = server; this.name = ServerXMLUtil.getServiceName(); this.engine = new Engine(this); } public Engine getEngine() { return engine; } public Server getServer() { return server; } }
修改Bootstrap:现在 Bootstrap 就变成了一个壳子了,直接调用 Server 实例的 start 方法。
package com.ysy.diytomcat; import com.ysy.diytomcat.catalina.*; public class Bootstrap { public static void main(String[] args) { Server server = new Server(); server.start(); } }
4,其他页面
4.1,404页面
当访问的文件不存在的时候,目前仅仅是返回字符串 File Not Found, 并且返回代码还是 200的,这样不符合 http 协议的规范,应当返回 404, 所以我们接下来就要进行改造, 使得它呈现 tomcat 的 404 这种页面风格。
Constant新增
public static final String response_head_404 = "HTTP/1.1 404 Not Found\r\n Content-Type: text/html\r\n\r\n"; public static final String textFormat_404 = "<html><head><title>DIY Tomcat/1.0.1 - Error report</title><style>" + "<!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" + "A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> " + "</head><body><h1>HTTP Status 404 - {}</h1>" + "<HR size='1' noshade='noshade'><p><b>type</b> Status report</p><p><b>message</b> <u>{}</u></p><p><b>description</b> " + "<u>The requested resource is not available.</u></p><HR size='1' noshade='noshade'><h3>DiyTocmat 1.0.1</h3>" + "</body></html>";
修改Server
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.*; import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.log.LogFactory; import cn.hutool.system.SystemUtil; import com.ysy.diytomcat.http.*; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class Server { private Service service; public Server() { this.service = new Service(this); } public void start() { logJVM(); init(); } private void init() { try { int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, service); Response response = new Response(); String uri = request.getUri(); if (null == uri) return; System.out.println("uri:" + uri); Context context = request.getContext(); if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { String fileName = StrUtil.removePrefix(uri, "/"); File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { handle404(s, uri); return; } } handle200(s, response); } catch (IOException e) { e.printStackTrace(); } finally { try { if (!s.isClosed()) s.close(); } catch (IOException e) { e.printStackTrace(); } } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } protected void handle404(Socket s, String uri) throws IOException { OutputStream os = s.getOutputStream(); String responseText = StrUtil.format(Constant.textFormat_404, uri, uri); responseText = Constant.response_head_404 + responseText; byte[] responseByte = responseText.getBytes("utf-8"); os.write(responseByte); } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); byte[] body = response.getBody(); byte[] responseBytes = new byte[head.length + body.length]; ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); s.close(); } }
4.2,500页面
Constant:
public static final String response_head_500 = "HTTP/1.1 500 Internal Server Error\r\n Content-Type: text/html\r\n\r\n"; public static final String textFormat_500 = "<html><head><title>DIY Tomcat/1.0.1 - Error report</title><style>" + "<!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" + "A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> " + "</head><body><h1>HTTP Status 500 - An exception occurred processing {}</h1>" + "<HR size='1' noshade='noshade'><p><b>type</b> Exception report</p><p><b>message</b> <u>An exception occurred processing {}</u></p><p><b>description</b> " + "<u>The server encountered an internal error that prevented it from fulfilling this request.</u></p>" + "<p>Stacktrace:</p>" + "<pre>{}</pre>" + "<HR size='1' noshade='noshade'><h3>DiyTocmat 1.0.1</h3>" + "</body></html>";
Server
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.*; import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.log.LogFactory; import cn.hutool.system.SystemUtil; import com.ysy.diytomcat.http.*; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class Server { private Service service; public Server() { this.service = new Service(this); } public void start() { logJVM(); init(); } private void init() { try { int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, service); Response response = new Response(); String uri = request.getUri(); if (null == uri) return; System.out.println("uri:" + uri); Context context = request.getContext(); if ("/500.html".equals(uri)) { throw new Exception("this is a deliberately created exception"); } if ("/".equals(uri)) { String html = "Hello DIY Tomcat"; response.getWriter().println(html); } else { String fileName = StrUtil.removePrefix(uri, "/"); File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { handle404(s, uri); return; } } handle200(s, response); } catch (Exception e) { LogFactory.get().error(e); handle500(s, e); } finally { try { if (!s.isClosed()) s.close(); } catch (IOException e) { e.printStackTrace(); } } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } protected void handle500(Socket s, Exception e) { try { OutputStream os = s.getOutputStream(); StackTraceElement stes[] = e.getStackTrace(); StringBuffer sb = new StringBuffer(); sb.append(e.toString()); sb.append("\r\n"); for (StackTraceElement ste : stes) { sb.append("\t"); sb.append(ste.toString()); sb.append("\r\n"); } String msg = e.getMessage(); if (null != msg && msg.length() > 20) msg = msg.substring(0, 19); String text = StrUtil.format(Constant.textFormat_500, msg, e.toString(), sb.toString()); text = Constant.response_head_500 + text; byte[] responseBytes = text.getBytes("utf-8"); os.write(responseBytes); } catch (IOException e1) { e1.printStackTrace(); } } protected void handle404(Socket s, String uri) throws IOException { OutputStream os = s.getOutputStream(); String responseText = StrUtil.format(Constant.textFormat_404, uri, uri); responseText = Constant.response_head_404 + responseText; byte[] responseByte = responseText.getBytes("utf-8"); os.write(responseByte); } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); byte[] body = response.getBody(); byte[] responseBytes = new byte[head.length + body.length]; ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); s.close(); } }
4.3,欢迎文件
所谓的欢迎文件就是访问某个 context 的时候,如果没有指明文件,那么就默认访问 index.html 或者 index.jsp 这种功能。
Constant:增加 webXmlFile 文件,指向 conf/web.xml 这里。
public static final File webXmlFile = new File(confFolder, "web.xml");
conf/web.xml:在 conf 目录下新建 web.xml 文件,里面保存如下信息。这个 web.xml 是在 tomcat/conf/web.xml 这个位置的, 和大家开发 j2ee 的时候 webContent/WEB-INF/web.xml 那个 web.xml 文件是不同的文件。tomcat 关于文件的默认配置其实都是放在这里的。
<?xml version="1.0" encoding="UTF-8"?> <web-app> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
WebXMLUtil:准备个工具类,用来获取 某个 Context 下的欢迎文件名称。根据 Context的 docBase 去匹配 web.xml 中的3个文件,找到哪个,就是哪个,如果都没有找到,默认就返回 index.html 文件。
package com.ysy.diytomcat.util; import java.io.File; import static com.ysy.diytomcat.util.Constant.webXmlFile; import cn.hutool.core.io.FileUtil; import com.ysy.diytomcat.catalina.Context; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public class WebXMLUtil { public static String getWelcomeFile(Context context) { String xml = FileUtil.readUtf8String(webXmlFile); Document d = Jsoup.parse(xml); Elements es = d.select("welcome-file"); for (Element e : es) { String welcomeFileName = e.text(); File f = new File(context.getDocBase(), welcomeFileName); if (f.exists()) return f.getName(); } return "index.html"; } }
Request:以前访问 /a/index 的时候会匹配 /a 这个context,但是访问呢 /a 的时候,匹配的是 / 这个 context,因为现在有了 欢迎文件了,所以 /a 也要匹配 context了,所以Request有几个地方要做一下调整。
- parseContext 里,先通过 uri 进行匹配,这样 /a 就可以匹配到 context了。
private void parseContext() { Engine engine = service.getEngine(); context = engine.getDefaultHost().getContext(uri); if (null != context) return; String path = StrUtil.subBetween(uri, "/", "/"); if (null == path) path = "/"; else path = "/" + path; context = service.getEngine().getDefaultHost().getContext(path); if (null == context) context = service.getEngine().getDefaultHost().getContext("/"); }
- 在构造方法里,比如 访问的地址是 /a, 那么 uri就变成 ""了,所以考虑这种情况, 让 uri 等于 "/"。
public Request(Socket socket, Service service) throws IOException { this.socket = socket; this.service = service; parseHttpRequest(); if (StrUtil.isEmpty(requestString) || requestString.equals("")) return; parseUri(); //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。 parseContext(); if (!"/".equals(context.getPath())) { uri = StrUtil.removePrefix(uri, context.getPath()); if (StrUtil.isEmpty(uri)) uri = "/"; } }
Server:当 uri 等于 "/" 的时候,uri 就修改成欢迎文件,后面就当作普通文件来处理了。
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.*; import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.log.LogFactory; import cn.hutool.system.SystemUtil; import com.ysy.diytomcat.http.*; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class Server { private Service service; public Server() { this.service = new Service(this); } public void start() { logJVM(); init(); } private void init() { try { int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, service); Response response = new Response(); String uri = request.getUri(); if (null == uri) return; System.out.println("uri:" + uri); Context context = request.getContext(); if ("/500.html".equals(uri)) { throw new Exception("this is a deliberately created exception"); } if ("/".equals(uri)) uri = WebXMLUtil.getWelcomeFile(request.getContext()); String fileName = StrUtil.removePrefix(uri, "/"); File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { handle404(s, uri); return; } handle200(s, response); } catch (Exception e) { LogFactory.get().error(e); handle500(s, e); } finally { try { if (!s.isClosed()) s.close(); } catch (IOException e) { e.printStackTrace(); } } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } protected void handle500(Socket s, Exception e) { try { OutputStream os = s.getOutputStream(); StackTraceElement stes[] = e.getStackTrace(); StringBuffer sb = new StringBuffer(); sb.append(e.toString()); sb.append("\r\n"); for (StackTraceElement ste : stes) { sb.append("\t"); sb.append(ste.toString()); sb.append("\r\n"); } String msg = e.getMessage(); if (null != msg && msg.length() > 20) msg = msg.substring(0, 19); String text = StrUtil.format(Constant.textFormat_500, msg, e.toString(), sb.toString()); text = Constant.response_head_500 + text; byte[] responseBytes = text.getBytes("utf-8"); os.write(responseBytes); } catch (IOException e1) { e1.printStackTrace(); } } protected void handle404(Socket s, String uri) throws IOException { OutputStream os = s.getOutputStream(); String responseText = StrUtil.format(Constant.textFormat_404, uri, uri); responseText = Constant.response_head_404 + responseText; byte[] responseByte = responseText.getBytes("utf-8"); os.write(responseByte); } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); byte[] body = response.getBody(); byte[] responseBytes = new byte[head.length + body.length]; ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); s.close(); } }
以前访问 "/" 的时候,是直接在内存里写字符串,现在在 ROOT 下新建一个 index.html ,内容是一样的。
4.4,mime-type
文件有各种格式,比如 png, jpg, txt, html , exe 等等,但是浏览器却不能理解这些后缀名,它能理解的是 mime-type,比如 png 对应的 mime-type 是 image/png。当它拿到的响应告诉它 mime-type 是 image/png 的时候,它就会按照对应的格式去理解和解析这个图片。
目前我们默认返回的是 text/html 就是告诉浏览器把拿到的数据要当作 html 来解析,但是我们还会访问其他格式的文件,比如 .txt, .png, .pdf,就不能都统一返回成 text/html了。
所以我们要把不同后缀名的文件,翻译成对应的 mime-type 然后在 http 响应中,用 Content-type 这个头信息告诉浏览器,这样浏览器就可以更好地进行解析工作了。web.xml:tomcat 把 mime-type 相关的信息都存放在这个 web.xml里。
<?xml version="1.0" encoding="UTF-8"?> <web-app> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <mime-mapping> <extension>pdf</extension> <mime-type>application/pdf</mime-type> </mime-mapping> </web-app>
WebXMLUtil
package com.ysy.diytomcat.util; import java.io.File; import java.util.*; import static com.ysy.diytomcat.util.Constant.webXmlFile; import cn.hutool.core.io.FileUtil; import com.ysy.diytomcat.catalina.Context; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public class WebXMLUtil { private static Map<String, String> mimeTypeMapping = new HashMap<>(); public static synchronized String getMimeType(String extName) { if (mimeTypeMapping.isEmpty()) initMimeType(); String mimeType = mimeTypeMapping.get(extName); if (null == mimeType) return "text/html"; return mimeType; } private static void initMimeType() { String xml = FileUtil.readUtf8String(webXmlFile); Document d = Jsoup.parse(xml); Elements es = d.select("mime-mapping"); for (Element e : es) { String extName = e.select("extension").first().text(); String mimeType = e.select("mime-type").first().text(); mimeTypeMapping.put(extName, mimeType); } } public static String getWelcomeFile(Context context) { String xml = FileUtil.readUtf8String(webXmlFile); Document d = Jsoup.parse(xml); Elements es = d.select("welcome-file"); for (Element e : es) { String welcomeFileName = e.text(); File f = new File(context.getDocBase(), welcomeFileName); if (f.exists()) return f.getName(); } return "index.html"; } }
Response:给contentType增加一个 setter。
public void setContentType(String contentType) { this.contentType = contentType; }
Server:在文件存在的时候,获取mimeType,并且设置在 response 的 contentType 属性上。
String extName = FileUtil.extName(file); String mimeType = WebXMLUtil.getMimeType(extName); response.setContentType(mimeType); String fileContent = FileUtil.readUtf8String(file); response.getWriter().println(fileContent);
a.txt:在ROOT下准备个 a.txt。
4.5,二进制文件
前我们能处理的文件都是文本文件,如 html 和 txt. 但是业务上总是需要处理二进制文件的,比如图片和 pdf这样的。
Response:为 Response 准备一个 body[] 来存放二进制文件,提供setter,修改 getter, 当body 不为空的时候,直接返回 body。
private byte[] body; public void setBody(byte[] body) { this.body = body; }
public byte[] getBody() throws UnsupportedEncodingException { if (null == body) { String content = stringWriter.toString(); body = content.getBytes("utf-8"); } return body; }
Server:以前处理文本文件的方式,会先读取成字符串,然后交给 response 。现在改成直接读取成二进制文件,交给 response 的 body。
package com.ysy.diytomcat.catalina; import com.ysy.diytomcat.util.*; import cn.hutool.core.io.FileUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.log.LogFactory; import cn.hutool.system.SystemUtil; import com.ysy.diytomcat.http.*; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class Server { private Service service; public Server() { this.service = new Service(this); } public void start() { logJVM(); init(); } private void init() { try { int port = 18080; ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); Runnable r = new Runnable() { @Override public void run() { try { Request request = new Request(s, service); Response response = new Response(); String uri = request.getUri(); if (null == uri) return; System.out.println("uri:" + uri); Context context = request.getContext(); if ("/500.html".equals(uri)) { throw new Exception("this is a deliberately created exception"); } if ("/".equals(uri)) uri = WebXMLUtil.getWelcomeFile(request.getContext()); String fileName = StrUtil.removePrefix(uri, "/"); File file = FileUtil.file(context.getDocBase(), fileName); if (file.exists()) { String extName = FileUtil.extName(file); String mimeType = WebXMLUtil.getMimeType(extName); response.setContentType(mimeType); // String fileContent = FileUtil.readUtf8String(file); // response.getWriter().println(fileContent); byte body[] = FileUtil.readBytes(file); response.setBody(body); if (fileName.equals("timeConsume.html")) { ThreadUtil.sleep(1000); } } else { handle404(s, uri); return; } handle200(s, response); } catch (Exception e) { LogFactory.get().error(e); handle500(s, e); } finally { try { if (!s.isClosed()) s.close(); } catch (IOException e) { e.printStackTrace(); } } } }; ThreadPoolUtil.run(r); } } catch (IOException e) { LogFactory.get().error(e); e.printStackTrace(); } } private static void logJVM() { Map<String, String> infos = new LinkedHashMap<>(); infos.put("Server version", "How2J DiyTomcat/1.0.1"); infos.put("Server built", "2020-04-08 10:20:22"); infos.put("Server number", "1.0.1"); infos.put("OS Name\t", SystemUtil.get("os.name")); infos.put("OS Version", SystemUtil.get("os.version")); infos.put("Architecture", SystemUtil.get("os.arch")); infos.put("Java Home", SystemUtil.get("java.home")); infos.put("JVM Version", SystemUtil.get("java.runtime.version")); infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor")); Set<String> keys = infos.keySet(); for (String key : keys) { LogFactory.get().info(key + ":\t\t" + infos.get(key)); } } protected void handle500(Socket s, Exception e) { try { OutputStream os = s.getOutputStream(); StackTraceElement stes[] = e.getStackTrace(); StringBuffer sb = new StringBuffer(); sb.append(e.toString()); sb.append("\r\n"); for (StackTraceElement ste : stes) { sb.append("\t"); sb.append(ste.toString()); sb.append("\r\n"); } String msg = e.getMessage(); if (null != msg && msg.length() > 20) msg = msg.substring(0, 19); String text = StrUtil.format(Constant.textFormat_500, msg, e.toString(), sb.toString()); text = Constant.response_head_500 + text; byte[] responseBytes = text.getBytes("utf-8"); os.write(responseBytes); } catch (IOException e1) { e1.printStackTrace(); } } protected void handle404(Socket s, String uri) throws IOException { OutputStream os = s.getOutputStream(); String responseText = StrUtil.format(Constant.textFormat_404, uri, uri); responseText = Constant.response_head_404 + responseText; byte[] responseByte = responseText.getBytes("utf-8"); os.write(responseByte); } private static void handle200(Socket s, Response response) throws IOException { String contentType = response.getContentType(); String headText = Constant.response_head_202; headText = StrUtil.format(headText, contentType); byte[] head = headText.getBytes(); byte[] body = response.getBody(); byte[] responseBytes = new byte[head.length + body.length]; ArrayUtil.copy(head, 0, responseBytes, 0, head.length); ArrayUtil.copy(body, 0, responseBytes, head.length, body.length); OutputStream os = s.getOutputStream(); os.write(responseBytes); s.close(); } }
MiniBrowser:改造一下 MiniBrowser,给 readBytes 方法增加一个 fully 参数,表示是否完全读取。当 fully 等于 true的时候,即便读取到的数据没有buffer_size 那么长,也会继续读取。为什么要这么改动呢? 主要是为了测试 etf.pdf 这个文件, 这个文件比较大 (其实也没多大), 那么在传输过程中,可能就不会一次传输 1024个字节,有时候会小于这个字节数,如果读取到的数据小于这个字节就结束的话,那么读取到的文件就是不完整的。
public static byte[] readBytes(InputStream is, boolean fully) throws IOException { int buffer_size = 1024; byte buffer[] = new byte[buffer_size]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { int length = is.read(buffer); if (-1 == length) break; baos.write(buffer, 0, length); if (!fully && length != buffer_size) break; } byte[] result = baos.toByteArray(); return result; }
Request:修改 Request 的 parseHttpRequest 方法。
byte[] bytes = MiniBrowser.readBytes(is,false);
这个要带个 false 参数,表示还像原来那样,如果读取到的数据不够 bufferSize ,那么就不继续读取了。为什么这里不能用过 true 呢? 因为浏览器默认使用长连接,发出的连接不会主动关闭,那么 Request 读取数据的时候 就会卡在那里了~
更多推荐
所有评论(0)