学习Tomcat源码是因为我之前写的《Spring之我见》系列文章,当学习到spring是如何启动的时候涉及到了tomcat从web.xml读取到ContextLoaderListener,从而初始化spring容器。换句话说,spring启动靠的是tomcat的帮助,这让我想先研究tomcat开始。
tomcat我参考的是《深入剖析Tomcat》,虽然介绍的tomcat比较老,还是tomcat4,但是学习思想是最重要的。而且这本书是教你怎么从零实现一个servlet容器,这种学习方法能够让我们知其然,并知其所以然。
最简单的servlet容器需要几个比较重要的组件
- HttpServlet 等待并接受请求
- Request 封装Request请求
- Response 封装Response请求
- ServletProcessor 具体处理Servlet请求
- StaticResourceProcessor 具体处理静态页面
HttpServlet 容器的入口
我们先看 HttpServlet ,这段代码本质是ServerSocket 阻塞等待请求,如果拿到了就封装Request和Response,然后根据url分发给不同的处理器,处理完后继续轮询等待请求,直到遇到SHUTDOWN_COMMAND命令关闭程序。
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.logging.Logger;
public class HttpServlet {
private static final Logger log = Logger.getLogger(HttpServlet.class.getName());
// 请求接口
private static final int port = 8088;
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
private boolean shutdown = false;
private void await() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(HttpServlet.port, 1, InetAddress.getByName("127.0.0.1"));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while (true) {
Socket socket;
InputStream input;
OutputStream output;
try {
System.out.println("等待指令。。。。" + LocalDateTime.now().toString());
// 从 socket拿到InputStream 和 OutputStream 并封装成 Request 和 Response
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
Request request = new Request(input);
request.parse();
Response response = new Response(output);
if (SHUTDOWN_COMMAND.equals(request.getUri())) {
break;
}
response.setRequest(request);
// 根据url 分发任务给 servlet处理器(ServletProcessor) 或者 静态页面处理器 (StaticResourceProcessor)
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor servletProcessor = new ServletProcessor();
servletProcessor.process(request, response);
} else {
StaticResourceProcessor staticResourceProcessor = new StaticResourceProcessor();
staticResourceProcessor.process(request, response);
}
socket.close();
} catch (Exception e) {
}
}
}
public static void main(String[] args) {
HttpServlet httpServlet = new HttpServlet();
System.out.println("servlet容器启动成功");
httpServlet.await();
}
}
Socket的介绍我直接摘百度百科吧
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
对于我肤浅的理解。socket就是操作TCP/IP请求的api,给编程人员带来便利,从这点我们也知道,Tomcat的基础是Socket。
Request 与 Response
HttpServlet代码首先提到了Request 和 Response,封装的理由是为了做一些额外的操作,我们看一下Request代码
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Logger;
public class Request implements ServletRequest {
private static final int BUFFER_SIZE = 2048;
private InputStream inputStream;
private String uri;
private static final Logger log = Logger.getLogger(Request.class.getName());
/**
* 解析请求url
*/
public String parseUri(String requestUri) {
int index1, index2;
index1 = requestUri.indexOf(" ");
if (index1 != -1) {
index2 = requestUri.indexOf(" ", index1 + 1);
if (index2 > index1) {
return requestUri.substring(index1 + 1, index2);
}
}
return null;
}
/**
* 读取inputStream
*/
public void parse() {
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[BUFFER_SIZE];
try {
i = inputStream.read(buffer);
} catch (Exception e) {
log.severe(e.toString());
i = -1;
}
for (int j = 0; j < i; j++) {
request.append((char) buffer[j]);
}
uri = parseUri(request.toString());
}
public Request(InputStream inputStream) {
this.inputStream = inputStream;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public Object getAttribute(String s) {
return null;
}
public Enumeration<String> getAttributeNames() {
return null;
}
public String getCharacterEncoding() {
return null;
}
public void setCharacterEncoding(String s) throws UnsupportedEncodingException {
}
public int getContentLength() {
return 0;
}
public long getContentLengthLong() {
return 0;
}
public String getContentType() {
return null;
}
public ServletInputStream getInputStream() throws IOException {
return null;
}
public String getParameter(String s) {
return null;
}
public Enumeration<String> getParameterNames() {
return null;
}
public String[] getParameterValues(String s) {
return new String[0];
}
public Map<String, String[]> getParameterMap() {
return null;
}
public String getProtocol() {
return null;
}
public String getScheme() {
return null;
}
public String getServerName() {
return null;
}
public int getServerPort() {
return 0;
}
public BufferedReader getReader() throws IOException {
return null;
}
public String getRemoteAddr() {
return null;
}
public String getRemoteHost() {
return null;
}
public void setAttribute(String s, Object o) {
}
public void removeAttribute(String s) {
}
public Locale getLocale() {
return null;
}
public Enumeration<Locale> getLocales() {
return null;
}
public boolean isSecure() {
return false;
}
public RequestDispatcher getRequestDispatcher(String s) {
return null;
}
public String getRealPath(String s) {
return null;
}
public int getRemotePort() {
return 0;
}
public String getLocalName() {
return null;
}
public String getLocalAddr() {
return null;
}
public int getLocalPort() {
return 0;
}
public ServletContext getServletContext() {
return null;
}
public AsyncContext startAsync() throws IllegalStateException {
return null;
}
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
return null;
}
public boolean isAsyncStarted() {
return false;
}
public boolean isAsyncSupported() {
return false;
}
public AsyncContext getAsyncContext() {
return null;
}
public DispatcherType getDispatcherType() {
return null;
}
}
Request实现了ServletRequest,大部分方法因为现在用不上所以没有实现,其中parse()方法从inputStream读取请求信息,读取内容范例如下
GET /servlet/TestServlet HTTP/1.1
Host: localhost:8088
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: cookie_mobile=15921307683; cookie_user_info="{\"firstName\":\"%E8%B4%BE%E7%A7%8B%E7%94%9F\",\"mobile\":\"15921307683\",\"email\":\"\"}"; cookie_user_org="{\"orgName\":\"%E5%BC%80%E5%8F%91%E7%BB%84\",\"orgCode\":\"006\"}"; sessionid=cc8798be-08f6-4051-936f-988a5c846ad7
而作为最简单的servlet容器,我们先不管Cookie啥信息,只看第一行。嗯,我们读到了是一个GET请求,请求的url是 /servlet/TestServlet ,http协议版本是HTTP/1.1 ,前面说到我们要根据url要分发请求,这就用上了parseUri()方法,它通过字符串处理直接提取出了 “/servlet/TestServlet” 并保存到uri变量。
Response也是同理,多了一个sendStaticResource方法,这个方法是由静态页面处理器(StaticResourceProcessor)直接调用的,用来返回html页面。这个方法现在还没用上,后面会再提。
public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
File file = new File(WEB_ROOT + "webapp", request.getUri());
fis = new FileInputStream(file);
outputStream.write(("HTTP/1.1 200 \r\n"
+ "Content-Type: text/html\r\n" + "\r\n"
).getBytes());
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch != -1) {
outputStream.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
} catch (Exception e) {
String errorMsg = "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>";
outputStream.write(errorMsg.getBytes());
} finally {
outputStream.flush();
outputStream.close();
if (fis != null) {
fis.close();
}
}
}
ServletProcessor StaticResourceProcessor 处理的大脑
介绍完了Request和Response,开始进入核心处理类ServletProcessor
ServletProcessor只有一个process方法,接受Request和Response参数,这里面涉及类加载器的,大概流程是通过类加载器加载url中同名的类,然后调用service方法,比如请求url是“/servlet/TestServlet”,“/servlet”说明需要调用servlet处理器(ServletProcessor),然后 “TestServlet”说明调用同名的TestServlet类。
ServletProcessor:
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.util.logging.Logger;
public class ServletProcessor {
private static final Logger log = Logger.getLogger(HttpServlet.class.getName());
public void process(Request request, Response response) {
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
URLClassLoader loader = null;
try {
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Response.WEB_ROOT + "servlet" + File.separator);
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
urls[0] = classPath.toURI().toURL();
loader = new URLClassLoader(urls);
} catch (Exception e) {
log.severe(e.toString());
}
Class myClass = null;
try {
myClass = loader.loadClass("servlet." + servletName);
} catch (Exception e) {
log.severe(e.toString());
}
Servlet servlet;
try {
servlet = (Servlet) myClass.newInstance();
// 使用包装类,可以让Servlet开发者接触不到parseUri等方法
RequestWrapper requestWrapper = new RequestWrapper(request);
ResponseWrapper responseWrapper = new ResponseWrapper(response);
servlet.service(requestWrapper, responseWrapper);
} catch (Exception e) {
log.severe(e.toString());
}
}
}
TestServlet:继承标准的Servlet接口,复写service方法,service方法会由ServletProcessor调用,到这一块对于学习了Servlet的人就很熟悉了。
package servlet;
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class TestServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("TestServlet init");
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws IOException {
System.out.println("TestServlet service ");
PrintWriter writer = res.getWriter();
writer.println("HTTP/1.1 200 \r\n"
+ "Content-Type: text/html\r\n" + "\r\n" + "hello , it is TestServlet");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
System.out.println("TestServlet destroy");
}
}
这里面有一点改进的地方在于:对于TestServlet 来说,是客户端程序员会编写的类,而ServletProcessor做了什么是不需要关心的,但是service会拿到ServletProcessor给的ServletRequest 和ServletResponse 变量,也就是Request和Response对象,而我们知道Request和Response有一些拓展方法比如 parseUri,这些是不允许 客户端程序员通过强转来调用的。 那如何保证安全性呢?
我们可以通过外观类来实现,我们设计一个外观类RequestWrapper ,它拥有一个Request对象,一般方法都直接调用Request的方法,而对于私密的方法不暴露在RequestWrapper 类中,达到调用安全的目的。
ResponseWrapper一样,不再贴出代码
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
public class RequestWrapper implements ServletRequest {
private Request request;
public RequestWrapper(Request request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
this.request = request;
}
@Override
public Object getAttribute(String name) {
return request.getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return request.getAttributeNames();
}
@Override
public String getCharacterEncoding() {
return null;
}
@Override
public void setCharacterEncoding(String env) throws UnsupportedEncodingException {
}
@Override
public int getContentLength() {
return 0;
}
@Override
public long getContentLengthLong() {
return 0;
}
@Override
public String getContentType() {
return null;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return null;
}
@Override
public String getParameter(String name) {
return null;
}
@Override
public Enumeration<String> getParameterNames() {
return null;
}
@Override
public String[] getParameterValues(String name) {
return new String[0];
}
@Override
public Map<String, String[]> getParameterMap() {
return null;
}
@Override
public String getProtocol() {
return null;
}
@Override
public String getScheme() {
return null;
}
@Override
public String getServerName() {
return null;
}
@Override
public int getServerPort() {
return 0;
}
@Override
public BufferedReader getReader() throws IOException {
return null;
}
@Override
public String getRemoteAddr() {
return null;
}
@Override
public String getRemoteHost() {
return null;
}
@Override
public void setAttribute(String name, Object o) {
}
@Override
public void removeAttribute(String name) {
}
@Override
public Locale getLocale() {
return null;
}
@Override
public Enumeration<Locale> getLocales() {
return null;
}
@Override
public boolean isSecure() {
return false;
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
return null;
}
@Override
public String getRealPath(String path) {
return null;
}
@Override
public int getRemotePort() {
return 0;
}
@Override
public String getLocalName() {
return null;
}
@Override
public String getLocalAddr() {
return null;
}
@Override
public int getLocalPort() {
return 0;
}
@Override
public ServletContext getServletContext() {
return null;
}
@Override
public AsyncContext startAsync() throws IllegalStateException {
return null;
}
@Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
return null;
}
@Override
public boolean isAsyncStarted() {
return false;
}
@Override
public boolean isAsyncSupported() {
return false;
}
@Override
public AsyncContext getAsyncContext() {
return null;
}
@Override
public DispatcherType getDispatcherType() {
return null;
}
}
而对于静态页面处理器就更简单了,只有一个方法,方法也只有一句话
sendStaticResource方法就是之前写在Response 的拓展方法。通过File读取servlet容器中预存的html页面,然后通过outputStream发出去。
/**
* http://localhost:8080/index.html
*/
public class StaticResourceProcessor {
public void process(Request request, Response response) {
try {
response.sendStaticResource();
} catch (Exception e) {
e.printStackTrace();
}
}
}
结语
大致的流程就是这些,这是最简单的servlet容器,本书会一层层深入,完整代码可以看我的项目SimpleServlet ,我会跟着看书的进度同时更新代码和文章
对应的分支是v1.1
https://github.com/lovejj1994/SimpleServlet