- 最近笔者读了《深入剖析tomcat》这本书(原作:《how tomcat works》),发现该书简单易读,每个章节
- 循序渐进的讲解了tomcat的原理,在接下来的章节中,tomcat都是基于上一章新增功能并完善,到最后形成
- 一个简易版tomcat的完成品。所以有兴趣的同学请按顺序阅读,本文为记录第三章的知识点以及源码实现
- (造轮子)。
点我阅读上一章内容
上一章我们实现了一个简单的 Servlet 容器,能够调用并执行用户自定义实现 Servlet 接口的类。
- //HttpServlet源代码片段
- public abstract
- class
- HttpServlet
- extends
- GenericServlet
- {
- ...
- public void service(ServletRequest req, ServletResponse res)
- throws ServletException, IOException{
- HttpServletRequest request;
- HttpServletResponse response;
- if (!(req instanceof HttpServletRequest &&
- res instanceof HttpServletResponse)) {
- throw new ServletException("non-HTTP request or response");
- }
- request = (HttpServletRequest) req;
- response = (HttpServletResponse) res;
- service(request, response);
- }
- ...
- }
如上所示源码,再来看看我们上一章的 ServletProcess 调用 Servlet 源码:
- servlet.service(new RequestFacade(request), new ResponseFacade(response));
很明显上述的 request/response 在 HttpServlet 时会抛出异常,所以本章我们会将 Request/Response/RequestFacade/ResponseFacade 都实现 HttpServletRequest/HttpServletResponse 接口。
在代码实现前我们看看整体模块以及流程执行图(看不清可以点击放大):
1. Bootstrap 模块
启动模块目前我们没有多大工作,只是启动 HttpConnector:
- public final class Bootstrap {
- public
- static
- void
- main
- (String[] args)
- {
- new HttpConnector().start();
- }
- }
2. HttpConnector 模块 (连接器)
- 连接器模块和下面的核心模块的前身其实就是上一章的HttpServer类,我们把它按功能拆分成了
- 等待和建立连接(HttpConnector)/处理连接(HttpProcess)2个模块。
连接器功能是等待请求并将请求丢给相应执行器去执行:
- public
- class
- HttpConnector
- implements
- Runnable
- {
- public
- void
- start
- ()
- {
- new Thread(this).start();
- }
- @Override
- public
- void
- run
- ()
- {
- ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
- while (true) {
- Socket accept = serverSocket.accept();
- HttpProcess process = new HttpProcess(this);
- process.process(accept);
- }
- serverSocket.close();
- }
- }
3. 核心模块 (执行器)
- 上面也有说到,执行器也是上一章HttpServer类的前身,只不过这章我们修改了解析请求信息的方式。
- public class HttpProcess {
- private HttpRequest request;
- private HttpResponse response;
- private HttpRequestLine httpRequestLine = new HttpRequestLine();
- public
- void
- process
- (Socket socket)
- {
- SocketInputStream input = new SocketInputStream(socket.getInputStream(), 2048);
- OutputStream output = socket.getOutputStream();
- //初始化request以及response
- request = new HttpRequest(input);
- response = new HttpResponse(output, request);
- //解析request请求和请求头
- this.parseRequest(input);
- this.parseHeaders(input);
- //调用对应的处理器处理
- if (request.getRequestURI().startsWith(SERVLET_URI_START_WITH)) {
- new ServletProcess().process(request, response);
- } else {
- new StaticResourceProcess().process(request, response);
- }
- }
- }
看了上面的实现可能很多人对有些对象有点陌生,下面一一介绍:
- 1. HttpRequest/HttpResponse变量就是上一章的Request/Response对象,因为实现了
- HttpServletReuqest/HttpServletResponse也就顺便改了个名,将会在下面介绍;
- 2. 每一个请求都对应了一个HttpProcess对象,所以这里request/response是成员变量;
- 3. SocketInputStream就是我们从Tomcat org.apache.catalina.connector.http.
- SocketInputStream拷贝过来的一个副本(现在已弃用),我们在下面会简单介绍;
- 4. HttpRequestLine对象和parseRequest()/parseHeader()方法也在下面介绍。
readRequestLine() 可以读取请求行,并给传入的 HttpRequestLine 对象赋值; 那么我们再来看看 HttpRequestLine 对象:
- public
- class
- SocketInputStream
- extends
- InputStream
- {
- public
- void
- readRequestLine
- (HttpRequestLine requestLine)
- {}
- public
- void
- readHeader
- (HttpHeader header)
- {}
- }
其实这个对象就是用三个数组分别装了请求行的方法 / URI / 协议的信息,还有三个 int 分别储存数组结束位置的索引 (因为数组初始大小一般比实际要大,为避免读取到无效字符)
- public final class HttpRequestLine {
- public char[] method = new char[8];
- public char[] uri = new char[64];
- public char[] protocol = new char[1024];
- public int methodEnd;
- public int uriEnd;
- public int protocolEnd;
- ...
- }
作用几乎都一致,分别存储 key 和 value 内容、结尾处索引。 让我们再回到 HttpProcess 这个类,看看具体怎么读取请求行和请求头呢?来看看代码:
- public final class HttpHeader {
- public char[] name;
- public char[] value;
- public int nameEnd;
- public int valueEnd;
- ...
- }
- private
- void
- parseRequest
- (SocketInputStream input)
- throws
- IOException, ServletException
- ;
- 给请求行对象赋值,然后获取其中的方法 / 协议字符串
- input.readRequestLine(httpRequestLine);
- String method = new String(httpRequestLine.method, 0, httpRequestLine.methodEnd);
- String protocol = new String(httpRequestLine.protocol, 0, httpRequestLine.protocolEnd);
- 判断URI有没有使用"?"传递参数,如果有就截取并丢到HttpRequest的QueryString变量中,
- 最后截取URI即可。
- String uri;
- int question = httpRequestLine.indexOf("?");
- if (question >= 0) {
- request.setQueryString(new String(httpRequestLine.uri, question + 1,
- httpRequestLine.uriEnd - question - 1));
- uri = new String(httpRequestLine.uri, 0, question);
- } else {
- uri = new String(httpRequestLine.uri, 0, httpRequestLine.uriEnd);
- }
- 判断是不是从 ? 传递jsessionid过来,如果是就赋值到request对象中
- String match = ";jsessionid=";
- int semicolon = uri.indexOf(match);
- if (semicolon >= 0) {
- String rest = uri.substring(semicolon + match.length());
- int semicolon2 = rest.indexOf(';');
- if (semicolon2 >= 0) {
- request.setRequestedSessionId(rest.substring(0, semicolon2));
- rest = rest.substring(semicolon2);
- } else {
- request.setRequestedSessionId(rest);
- rest = "";
- }
- request.setRequestedSessionURL(true);
- uri = uri.substring(0, semicolon) + rest;
- } else {
- request.setRequestedSessionId(null);
- request.setRequestedSessionURL(false);
- }
- 这里调用了一个校验URI合法性的方法,
- 如果URI不合法 (例如包含'.//'之类跳转目录的危险字符)则抛异常,否则就将上面解析到的内容丢到request中去。
就这样,请求行的信息就被我们读取完毕,那我们再来看看读取请求头的代码:
- String normalizedUri = this.normalize(uri);
- if (normalizedUri == null) {
- throw new ServletException("Invalid URI: " + uri + "'");
- }
- request.setRequestURI(normalizedUri);
- request.setMethod(method);
- request.setProtocol(protocol);
- private
- void
- parseHeaders
- (SocketInputStream input)
- throws
- IOException, ServletException
- ;
- 上来就是一个while(true),当所有请求行读取完毕才停止;
- while (true) {
- HttpHeader header = new HttpHeader();
- input.readHeader(header);
- if (header.nameEnd == 0 || header.valueEnd == 0) {
- return;
- }
- ........
- 根据读取完的请求头对象获得请求头的key / value并放到request中
- String name = new String(header.name, 0, header.nameEnd);
- String value = new String(header.value, 0, header.valueEnd);
- request.addHeader(name, value);
- 最后我们对一些特殊的请求头做处理 (cookie、content - type、content - length),
- 其中RequestUtil这个类也是从Tomcat拷贝过来的(滑稽脸)用于做一些通用的字符串操作。
- switch (httpHeaderEnum.get()) {
- case CONTENT_LENGTH:
- int n = Integer.parseInt(value);
- request.setContentLength(n);
- break;
- case CONTENT_TYPE:
- request.setContentType(value);
- break;
- case COOKIE:
- Cookie[] cookies = RequestUtil.parseCookieHeader(value);
- Stream.of(cookies).forEach(cookie -> request.addCookie(cookie));
- //如果sessionid不是从cookie中获取的,则优先使用cookie中的sessionid
- if (!request.isRequestedSessionIdFromCookie()) {
- Stream.of(cookies)
- .filter(cookie -> "jsessionid".equals(cookie.getName()))
- .findFirst().
- ifPresent(cookie -> {
- //设置cookie的值
- request.setRequestedSessionId(cookie.getValue());
- request.setRequestedSessionCookie(true);
- request.setRequestedSessionURL(false);
- });
- }
- break;
- default:
- }
到这里,HttpProcess 处理请求的逻辑就搞定啦,(是不是觉得代码有点多),细心的客官们一定发现了,request 怎么可以设置那么多属性呢?上一章的 request 好像没有那么多功能吧?是的,我们这一章也对 request/response 做了手脚,请看下文分析:
哈哈没有看错,多了一堆参数,但是细心的客官们应该可以看到,这些参数都是非常眼熟,而且上面已经对大部分参数设值过了,眼生的可能就是下面的那个 ParameterMap,那么等下我们慢慢分析:(那些 get、set 方法就不分析了) 请求头 (header) 操作:
- public
- class
- HttpRequest
- implements
- HttpServletRequest
- {
- private String contentType;
- private int contentLength;
- private InputStream input;
- private String method;
- private String protocol;
- private String queryString;
- private String requestURI;
- private boolean requestedSessionCookie;
- private String requestedSessionId;
- private boolean requestedSessionURL;
- protected ArrayList<Cookie> cookies = new ArrayList<>();
- protected HashMap<String, ArrayList<String>> headers = new HashMap<>();
- protected ParameterMap parameters;
- ...
- }
大家可以看到请求头是是个 Map,key 是请求头的名字,value 则是请求头的内容数组 (一个请求头可以有多个内容),所以也就是对这个 Map 做操作而已~ Cookie 操作:
- public
- void
- addHeader
- (String name, String value)
- {
- name = name.toLowerCase();
- //如果key对应的value不存在则new一个ArrayList
- ArrayList<String> values = headers.computeIfAbsent(name, k -> new ArrayList<>());
- values.add(value);
- }
- public ArrayList getHeaders(String name) {
- name = name.toLowerCase();
- return headers.get(name);
- }
- public String getHeader(String name) {
- name = name.toLowerCase();
- ArrayList<String> values = headers.get(name);
- if (values != null) {
- return values.get(0);
- } else {
- return null;
- }
- }
- public ArrayList getHeaderNames() {
- return new ArrayList(headers.keySet());
- }
好像也没什么好说的,对 List
- public Cookie[] getCookies() {
- return cookies.toArray(new Cookie[cookies.size()]);
- }
- public
- void
- addCookie
- (Cookie cookie)
- {
- cookies.add(cookie);
- }
- public final class ParameterMap extends HashMap < String,
- String[] > {
- private boolean locked = false;
- public boolean isLocked() {
- return locked;
- }
- public void setLocked(boolean locked) {
- this.locked = locked;
- }
- public String[] put(String key, String[] value) {
- if (locked) {
- throw new IllegalStateException("error");
- }
- return (super.put(key, value));
- }...
- }
那么我们来看看对 parameter 这个 map 的操作有:
- 好吧其实它就是在HashMap基础上加了一个locked对象(如果已经解析参数完毕了则将这个对
- 象设置为true禁止更改),key是参数名,value是参数值数组(可有多个)例:
- 127.0.0.1:8080/servlet/QueryServlet?name=geoffrey&name=yip
代码都很简单,但是这个 parseParameters() 是什么呢,对,它是去解析请求的参数了 (懒加载),因为我们不知道用户使用 Servlet 会不会用到请求参数这个功能,而且解析它的开销比解析其他数据大,所以我们会在用户真正使用参数的时候才会去解析,提高整体的响应速度,大概的代码如下:
- public String getParameter(String name) {
- parseParameters();
- String[] values = parameters.get(name);
- return Optional.ofNullable(values).map(arr - >arr[0]).orElse(null);
- }
- public Map getParameterMap() {
- parseParameters();
- return this.parameters;
- }
- public ArrayList < String > getParameterNames() {
- parseParameters();
- return new ArrayList < >(parameters.keySet());
- }
- public String[] getParameterValues(String name) {
- parseParameters();
- return parameters.get(name);
- }
大概内容就是根据之前 HttpProcess 解析请求行的 queryString 参数以及如果是 POST 请求的表单数据放入 ParameterMap 中,并且锁定 Map。
- protected
- void
- parseParameters
- ()
- {
- if (parsed) {
- return;//如果已经解析完成则不解析
- }
- ParameterMap results = parameters;
- if (results == null) {
- results = new ParameterMap();
- }
- results.setLocked(false);
- String encoding = getCharacterEncoding();
- //获取?传参的内容
- String queryString = getQueryString();
- RequestUtil.parseParameters(results, queryString, encoding);
- //如果是POST表单则解析POST表单内容
- if (HTTPMethodEnum.POST.name().equals(getMethod())
- && (getContentLength() > 0)
- && "application/x-www-form-urlencoded".equals(getContentType())) {
- int max = getContentLength();
- int len = 0;
- byte[] buf = new byte[getContentLength()];
- ServletInputStream is = getInputStream();
- while (len < max) {
- int next = is.read(buf, len, max - len);
- if (next < 0) {
- break;
- }
- len += next;
- }
- is.close();
- if (len < max) {
- throw new RuntimeException("Content length mismatch");
- }
- RequestUtil.parseParameters(results, buf, encoding);
- }
- //解析完毕则锁定map
- results.setLocked(true);
- parsed = true;
- parameters = results;
- }
- public
- class
- HttpResponse
- implements
- HttpServletResponse
- {
- ...
- }
ServletProcess 具体只需要将 request 和 response 的外观类跟着升级实现对应的接口即可:
- public
- void
- process
- (HttpRequest request, HttpResponse response)
- throws
- IOException
- {
- ...
- servlet.service(new HttpRequestFacade(request), new HttpResponseFacade(response));
- ...
- }
- public
- class
- HttpRequestFacade
- implements
- HttpServletRequest
- {
- private HttpRequest request;
- ...
- }
- public
- class
- HttpResponseFacade
- implements
- HttpServletResponse
- {
- private HttpResponse response;
- ...
- }
我们先编写一个 Servlet:
- public
- class
- RegisterServlet
- extends
- HttpServlet
- {
- @Override
- public
- void
- doGet
- (HttpServletRequest req, HttpServletResponse resp)
- {
- String name = req.getParameter("name");
- String password = req.getParameter("password");
- if (StringUtil.isBlank(name) || StringUtil.isBlank(password)) {
- try {
- resp.getWriter().println("账号/密码不能为空!");
- } finally {
- return;
- }
- }
- System.out.println("Parse user register method:" + req.getMethod());
- System.out.println("Parse user register cookies:");
- Optional.ofNullable(req.getCookies())
- .ifPresent(cookies ->
- Stream.of(cookies)
- .forEach(cookie ->System.out.println(cookie.getName() + ":" + cookie.getValue()
- )));
- System.out.println("Parse http headers:");
- Enumeration<String> headerNames = req.getHeaderNames();
- while (headerNames.hasMoreElements()) {
- String headerName = headerNames.nextElement();
- System.out.println(headerName + ":" + req.getHeader(headerName));
- }
- System.out.println("Parse User register name :" + name);
- System.out.println("Parse User register password :" + password);
- try {
- resp.getWriter().println("注册成功!");
- } finally {
- return;
- }
- }
- @Override
- public
- void
- doPost
- (HttpServletRequest req, HttpServletResponse resp)
- {
- this.doGet(req, resp);
- }
- }
编写一个 HTML:
- <html>
- <head>
- <title>注册</title>
- </head>
- <body>
- <
- form
- method
- =
- "post"
- action
- =
- "/servlet/RegisterServlet"
- >
- 账号:
- <
- input
- type
- =
- "text"
- name
- =
- "name"
- >
- <br>
- 密码:
- <
- input
- type
- =
- "password"
- name
- =
- "password"
- >
- <br>
- <
- input
- type
- =
- "submit"
- value
- =
- "提交"
- >
- </form>
- </body>
- </html>
打开浏览器测试:
控制台输出:
到这里,咱们的 Tomcat 3.0 web 服务器就已经开发完成啦(滑稽脸),已经可以实现简单的自定义 Servlet 调用,以及请求行 / 请求头 / 请求参数 / cookie 等信息的解析:
- - 每一次请求就new一次Servlet,Servlet应该在初始化项目时就应该初始化,是单例的。
- - 并未遵循Servlet规范实现相应的生命周期,例如init()/destory()方法我们均未调用。
- - HttpServletRequest/HttpServletResponse接口的大部分方法我们仍未实现
- - 其他未实现的功能
在下一个章节我们会讲讲 Tomcat 的默认连接器,并且进一步优化我们的代码,敬请期待!
PS:本章源码已上传 github(稍等) SimpleTomcat
来源: https://juejin.im/post/5a49ca76f265da4328413499