探秘Tomcat——从一个简陋的Web服务器开始
前言:
无论是之前所在实习单位小到一个三五个人做的项目,还是如今一个在做的百人以上的产品,一直都能看到tomcat的身影。工作中经常遇到的操作就是启动和关闭tomcat服务,或者修改了摸个java文件,编译该文件,将生成的class文件塞到tomcat目录下相应的jar包中去,以使其生效,但是也可以热部署,不需要这么繁琐的操作。
总之,一直以来都是习惯了tomcat的存在,没有深究tomcat的运行机制和原理,上一次对于tomcat源码的跃跃欲试还是去年的事儿了——《探秘Tomcat(一)——Myeclipse中导入Tomcat源码》。在这篇文章中,我下载了tomcat6版本的代码,并将其导入到eclipse中,时隔一年多,原来的项目还在,但是为显诚意,我还是重头做了一遍导入tomcat源码到eclipse的操作。
对于tomcat的崇敬之情让我决定深入其中,一探究竟。于是我找到了网上大家首推的教材——《深入剖析tomcat》,书比较老,但是很权威,不影响原理性东西的理解。目前已经看到第二章。
读过或者了解该书的应该都知道,这不是一本上来就直接告诉你tomcat的设计思想,用到的什么设计模式或者源码中某一行有什么匠心独运的地方。该书采用一个循序渐进的方式从一个简单的不能再简单的servlet容器开始,之后慢慢丰富,添加功能模块,最终形成我们想知道的tomcat的模样。
背景知识:
- HTTP请求:
请求方法——统一资源标识符URI——协议/版本
请求头
实体
比如这里我们可以看到请求的方法Request Method是GET, Request URL为http://tech.qq.com/a/20160604/007535.htm,并且分别有Request和下面要讲的Response的请求头信息,如Content-Type等。
- HTTP共支持7中请求方法
GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE
- HTTP响应
协议——状态码——描述
响应头
响应实体段
- Socket
socket在应用程序中用于从网络中读取数据,实现不同计算机之间的通讯,实现一个socket需要知道对应应用程序的ip地址和端口号。
首先创建该socket类的实例,有了该实例后,就可以使用它实现发送或接收字节流。如果想要发送字节流,需要调用socket类的getOutputStream来获取一个java.io.OutputStream对象;要发送文本到远程应用程序,需要使用返回的OutputStream对象创建一个java.io.PrintWriter对象;要从连接的另一端接收字节流,需要调用Socket类的getInputStream方法,其会返回一个java.io.InputStream对象。
- ServerSocket类
有了客户端的socket可以发送请求,如果没有服务端来响应,发送的请求也是肉包子打狗,ServerSocket就是充当服务端的角色,serversocket出于随时待命的状态,一旦有客户端发出请求,serversocket就要给出响应,从而实现与客户端的通信。
请求响应模型
有了以上的背景知识,我们就可以实现一个简单到爆的通讯模型,新建一个socket客户端通讯类,用于发送和接收数据,还需要创建一个服务端的ServerSocket用于监听和响应客户端的请求。
主要包含以下三个类:
HttpServer:模拟一个Web服务器
package myTest; import java.net.Socket; import java.net.ServerSocket; import java.net.InetAddress; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.io.File; public class HttpServer { /** WEB_ROOT is the directory where our HTML and other files reside. * For this package, WEB_ROOT is the "webroot" directory under the working * directory. * The working directory is the location in the file system * from where the java command was invoked. */ public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot"; // shutdown command private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received private boolean shutdown = false; public static void main(String[] args) { HttpServer server = new HttpServer(); server.await(); } public void await() { ServerSocket serverSocket = null; int port = 8080; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } // Loop waiting for a request while (!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // create Request object and parse Request request = new Request(input); request.parse(); // create Response object Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); continue; } } } }
从代码可以看出:
- 类中分别创建了socket和serversocket;
- 定义了一个WEB_ROOT目录,其中存放了对应请求的相应结果文件;
- 定义了一个关闭命令,通过在浏览器中输入类似.../SHUTDOWN来关闭Web服务器
- 创建了一个await方法,一直监听127.0.0.1的8080端口,如果有请求产生(比如在浏览器中输入一个请求地址),则会进入await方法并执行serverSocket的accept方法。
Request类:
模拟一个HTTP请求。
package myTest; import java.io.InputStream; import java.io.IOException; public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } public void parse() { // Read a set of characters from the socket StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; try { i = input.read(buffer); } catch (IOException e) { e.printStackTrace(); i = -1; } for (int j=0; j<i; j++) { request.append((char) buffer[j]); } System.out.print(request.toString()); uri = parseUri(request.toString()); } private String parseUri(String requestString) { int index1, index2; index1 = requestString.indexOf(' '); if (index1 != -1) { index2 = requestString.indexOf(' ', index1 + 1); if (index2 > index1) return requestString.substring(index1 + 1, index2); } return null; } public String getUri() { return uri; } }
从代码中可以发现:
- 可以实现传递InputStream对象,在处理与客户端通讯的Socket对象中获取;
- 调用InputStream对象的read来获取HTTP请求的原始数据;
- parse方法用于解析HTTP请求中的原始数据(原始数据由上面的getInputStream中获得);
- parseUri作为一个私有方法被parse调用,用于解析HTTP请求的URI
Response类:
模拟HTTP的响应。
package myTest; import java.io.OutputStream; import java.io.IOException; import java.io.FileInputStream; import java.io.File; /* HTTP Response = Status-Line *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF */ public class Response { private static final int BUFFER_SIZE = 1024; Request request; OutputStream output; public Response(OutputStream output) { this.output = output; } public void setRequest(Request request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; FileInputStream fis = null; try { File file = new File(HttpServer.WEB_ROOT, request.getUri()); if (file.exists()) { fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); while (ch!=-1) { output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } } else { // file not found String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>"; output.write(errorMessage.getBytes()); } } catch (Exception e) { // thrown if cannot instantiate a File object System.out.println(e.toString() ); } finally { if (fis!=null) fis.close(); } } }
从代码可以发现:
- 和Request类似,这里通过接受OutputStream构造了Response对象;
- setRequest方法用于接收Request对象,因为在Response中需要用到Request的getUri方法;
- sendStaticResource方法主要用于处理请求的响应,如这里发送一个静态资源html作为请求的结果
至此, 本篇主要提到:
- 一些基本概念如http请求、http响应、socket等;
- 对于一个超级简陋的web服务器有了基本的认识;
- 明确了客户端和服务端各自的角色和职责。
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
友情赞助
如果你觉得博主的文章对你那么一点小帮助,恰巧你又有想打赏博主的小冲动,那么事不宜迟,赶紧扫一扫,小额地赞助下,攒个奶粉钱,也是让博主有动力继续努力,写出更好的文章^^。
1. 支付宝 2. 微信