您的位置:首页 > 运维架构 > Tomcat

Tomcat源码之旅--最简单的Servlet容器实现

2018-03-19 19:18 441 查看
学习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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  tomcat java servlet