# Web服务器 **Repository Path**: giteeygq/handwritten-server ## Basic Information - **Project Name**: Web服务器 - **Description**: 本项目基于Java从0开发实现了一个遵循JavaEE规范的Web服务器。熟悉HTTP协议并基于此通过IO流实现数据传送,响应二进制文件等;通过类加载类加载机制实现动态服务器;使用责任链、线程池优化服务器性能。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-07-27 - **Last Updated**: 2023-09-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: JavaSE, JavaEE, Maven, Tomcat ## README # Handwriting Server(Powered by Yi) 本项目将实现一个功能健全的Web服务器,一个遵循JavaEE规范的服务器,可以处理静态web资源,同时也可以处理动态web资源,比如servlet等。项目一共可以分为三个阶段,**阶段一**,使用SE阶段学习的基础知识实现一个静态web资源服务器,可以处理静态web资源,包含文本文件以及二进制文件等;**阶段二**,在阶段一的基础上加上处理动态web资源的功能,实现Servlet规范,本阶段也是本项目最为核心 的一块内容;**阶段三**,阶段二已经是一个功能比较完善的服务器,但是还有一些高级特性没有实现,在最后这一阶段,我们要实现类加载器、Filter、Listener等高级特性。 其次,我们从JavaWeb阶段学习的过程中是功能的使用者,而开发服务器变成功能的提供者;还有服务器的本质是什么?无非就是把网络路径,解析成服务器本地硬盘路径,然后通过stream的形式将文件给游览器。 注:使用Maven来实现本项目,工具类代码(Utils)实现在总章最后。 ![架构图](README.assets/Snipaste_2023-09-01_15-35-53.png) # Phrase1-Complete Static Server 问题:什么是静态服务器?什么是动态服务器? ## 1. Simple Server HTTP协议是整个网络通讯的基石,客户端和服务器之间的沟通全部依赖于HTTP报文,具体可以细分为客户端发送给服务器的HTTP请求报文,服务器发送给客户端的HTTP响应报文。报文的格式一定要切记,客户端和服务器之间通讯其实就是解析报文,取出其中的数据。 ```java package com.yun.server; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; public class BootStrap1 { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(8090); //accpet()是阻塞方法,同时服务器需要持续的监听某一端口号,所以需要使用while循环... while (true){ Socket server = serverSocket.accept(); InputStream requestInputStream = server.getInputStream(); OutputStream responseOutputStream = server.getOutputStream(); //available()方法是阻塞方法,如果还没有读取请求报文的数据,但是有新的请求到来,会阻塞在当前位置,这在实际应 //用场景中并不合理...故需要启动多线程,使用匿名内部类即可,因为每个请求只需要处理一次.. new Thread(new Runnable() { @Override public void run() { int available = 0; try { available = requestInputStream.available(); byte[] bytes = new byte[available]; requestInputStream.read(bytes); String requestMessage = new String(bytes, 0, available); System.out.println(requestMessage); responseOutputStream.write(bytes); responseOutputStream.flush(); responseOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` ## 2. Message encapsulate Object 通过上述操作,发现不管是客户端发送过来的请求报文,还是接下来服务器发送给客户端的响应报文,全部都是字符串的形式。如果想获取请求报文中的部分数据,比如获取请求路径、某一个请求头等,非常的不方便,所以可以考虑将请求报文、响应报文封装到request、response对象中。 ```java package com.yun.server; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; public static void main(String[] args) { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(8080); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); //将请求报文,响应报文进行封装 Request request = new Request(requestInputStream); response = new Response(responseOutputStream); } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } ``` Request ```java package com.yun.server.http; import com.yun.server.catalina.Context; import com.yun.server.utils.Constant; import com.yun.server.utils.StringUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; /** * 请求行 * GET /http/demo1/2.html HTTP/1.1 * * 请求头 * Host: localhost:63342 * Connection: keep-alive * Upgrade-Insecure-Requests: 1 * User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 * Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,;q=0.8,application/signed-exchange;v=b3;q=0.9 * Sec-Fetch-Site:none * Sec-Fetch-Mode:navigate * Sec-Fetch-Dest:document * Accept-Encoding:gzip,deflate,br * Accept-Language:zh-CN,zh;q=0.9 * Cookie:Idea-31d5f4eb=af750744-2158-4f3c-bba9-e7eae63a38fd * * 请求体 */ public class Request { private String method; private String requestURI; private String requestURL; private String contextPath; private String servletPath; private String protocol; private Map header; private InputStream inputStream; private String requestStr; private Map contextMap; public Request(InputStream inputStream) throws IOException { this.inputStream = inputStream; this.header = new HashMap<>(); parseRequestStr(); parseRequestLine(); parseRequestHeader(); parsePath(); System.out.println("contextPath:" + this.contextPath + "\n" + "servletPath:" + this.servletPath); } private void parsePath() { parseContextPath(); parseServletPath(); parseRequestURL(); } private void parseServletPath() { //obtain servletPath String removedURI = StringUtils.removePrefix(this.requestURI, this.contextPath); if(removedURI.length() == 0){ this.servletPath = "/"; }else { this.servletPath = removedURI; } } private void parseContextPath() { //obtain contextPath int index = this.requestURI.indexOf("/", 1); if(index != -1){ this.contextPath = this.requestURI.substring(0, index); } else { this.contextPath = this.requestURI; } } private void parseRequestURL() { String host = this.getHeader("Host"); this.requestURL = host + this.requestURI; } private void parseRequestHeader() { int startHeaderIndex = this.requestStr.indexOf("\r\n"); int endHeaderIndex = this.requestStr.indexOf("\r\n\r\n"); String headerStr = this.requestStr.substring(startHeaderIndex + 1, endHeaderIndex); String[] partsHeader = headerStr.split("\r\n"); for (String header : partsHeader) { int middle = header.indexOf(":"); String headerKey = header.substring(0, middle).trim(); String headerValue = header.substring(middle + 1).trim(); this.header.put(headerKey, headerValue); } } private void parseRequestLine() { int index = this.requestStr.indexOf("\r\n"); String requestLine = this.requestStr.substring(0, index); String[] parts = requestLine.split(" "); for (int i = 0; i < parts.length; i++) { if(i == 0){ this.method = parts[i]; }else if(i == 1){ this.requestURI = parts[i]; }else { this.protocol = parts[i]; } } } private void parseRequestStr() throws IOException { int count = 0; count = this.inputStream.available(); while (count == 0){ count = this.inputStream.available(); } byte[] bytes = new byte[count]; this.inputStream.read(bytes); this.requestStr = new String(bytes, 0, count); } public Map getContextMap() { return contextMap; } public String getMethod() { return method; } public String getRequestURI() { return requestURI; } public String getContextPath() { return contextPath; } public String getServletPath() { return servletPath; } public String getProtocol() { return protocol; } public String getHeader(String key) { return this.header.get(key); } public String getRequestURL() { return requestURL; } public InputStream getInputStream() { return inputStream; } public String getRequestStr() { return requestStr; } } ``` Response ```java package com.yun.server.http; import com.sun.deploy.util.ArrayUtil; import org.apache.commons.lang3.ArrayUtils; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * 响应行: * 协议版本 状态码 原因短语 * HTTP/1.1 200 OK * * 响应头: * Location: http://www.cskaoyan.com/指示新的资源的位置,一般和302、307状态码搭配使用 * Server: apache tomcat 指示服务器的类型 * Content-Encoding: gzip 服务器发送的数据采用的编码类型 * Content-Length: 80 告诉浏览器正文的长度 * Content-Language: zh-cn服务发送的文本的语言 * Content-Type: text/html; 服务器发送的内容的MIME类型 * Last-Modified: Tue, 11 Jul 2000 18:23:51 GMT文件的最后修改时间 * Refresh: 1;url=http://www.cskaoyan.com指示客户端刷新频率。单位是秒 * Content-Disposition: attachment; filename=aaa.zip指示客户端保存文件 * Set-Cookie: SS=Q0=5Lb_nQ; path=/search服务器端发送的Cookie * Expires: 0 * Cache-Control: no-cache (1.1) * Connection: close/Keep-Alive * Date: Tue, 11 Jul 2000 18:23:51 GMT * * 空行 * * 响应体 * */ public class Response { private int status; private Map responseHeader; private OutputStream outputStream; private String contentType; private StringWriter stringWriter; //CharStream private byte[] responseBody; public Response(OutputStream outputStream) { this.status = 200; this.outputStream = outputStream; this.responseHeader = new HashMap<>(); this.responseHeader.put("Content-Type", "text/html;charset=utf-8"); this.stringWriter = new StringWriter(); } public PrintWriter getWriter(){ PrintWriter printWriter = new PrintWriter(this.stringWriter, true); return printWriter; } public void setStatus(int status) { this.status = status; } public void setContentType(String contentType){ this.setHeader("Content-Type", contentType); } public void setHeader(String key, String value) { this.responseHeader.put(key, value); } public void respond() { String reasonPhrase = " OK"; if(this.status == 404){ reasonPhrase = " Not Found"; }else if(this.status == 500){ reasonPhrase = " Server Inner Error"; } String responseLine = "HTTP/1.1 " + this.status + reasonPhrase + "\r\n"; String responseHeaderStr = ""; Set keySet = this.responseHeader.keySet(); for (String key : keySet) { responseHeaderStr += key + ": " + this.responseHeader.get(key) + "\r\n"; } //obtain line,header.... String lineHeaderStr = responseLine + responseHeaderStr + "\r\n"; byte[] lineHeaderBytes = lineHeaderStr.getBytes(StandardCharsets.UTF_8); //obtain reponsebody if(responseBody == null){ String responseBodyStr = this.stringWriter.toString(); this.responseBody = responseBodyStr.getBytes(StandardCharsets.UTF_8); } //obtain responsebyte byte[] responseByte = ArrayUtils.addAll(lineHeaderBytes, this.responseBody); try { this.outputStream.write(responseByte); this.outputStream.flush(); this.outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } public void setResponseBody(byte[] responseBody) { this.responseBody = responseBody; } } ``` ## 3. Default Application and Multi-Application 仿照Tomcat服务器的直接部署规则,即在webapps目录下存在缺省ROOT应用和其他自己部署多应用。 ![image-20221021155740913](README.assets/image-20221021155740913.png) 图中有有图片(二进制文件)和index.html(欢迎文件),第五部分会讲解。 现在主要来响应其他静态资源(app.html、1.html...),我们可以看到webapps目录下有许多应用,同时在下一部分配置型应用也是同样的结构,所以我们使用一个Context来封装每一个应用及路径。 ```java package com.yun.server.catalina; public class Context { private String path; private String docBase; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; } public String getPath() { return path; } public String getDocBase() { return docBase; } } ``` 其次,我们目前主要解析应用的业务逻辑写在Request类中,即 ```java public Request(InputStream inputStream) throws IOException { this.inputStream = inputStream; this.header = new HashMap<>(); parseRequestStr(); parseRequestLine(); parseRequestHeader(); parsePath(); scanContext(); //解析应用 System.out.println("contextPath:" + this.contextPath + "\n" + "servletPath:" + this.servletPath); } private void scanContext() throws IOException { this.contextMap = new HashMap<>(); scanContextOfWebapps(); scanContextOfServerXml();//第四部分内容 } private void scanContextOfWebapps() { File webapps = Constant.webapps; if(webapps.isDirectory()){ File[] files = webapps.listFiles(); for (File file : files) { if(file.isDirectory()){ String path = file.getName(); String docBase = file.getAbsolutePath(); if(path.equals("ROOT")){ // when scanedpath is ROOT, simultaneously add ROOT and /... Context context = new Context("/ROOT", docBase); this.contextMap.put("/ROOT", context); path = "/"; }else { path = "/" + path; } Context context = new Context(path, docBase); this.contextMap.put(path, context); } } } } ``` ## 4. Configure Application 其实就是虚拟映射,不要让应用部署在Tomcat项目内,而是部署在本地硬盘路径上。同时我们仿照Tomcat路径命名规则,创建conf/server.xml ![image-20221021155859891](README.assets/image-20221021155859891.png) ```xml ``` 解析配置型应用(需要使用Jsoup来进行解析.xml文件,.xml文件类似于.html都是在内容形成dom树) ```java private void scanContextOfServerXml() throws IOException { File serverXml = Constant.ServerXML; Document document = Jsoup.parse(serverXml, "utf-8"); Elements elements = document.select("Context"); for (Element element : elements) { String path = element.attr("path"); String docBase = element.attr("docBase"); Context context = new Context(path, docBase); this.contextMap.put(path, context); } } ``` 目前完整的Request类代码 ```java package com.yun.server.http; import com.yun.server.catalina.Context; import com.yun.server.utils.Constant; import com.yun.server.utils.StringUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; /** * 请求行 * GET /http/demo1/2.html HTTP/1.1 * * 请求头 * Host: localhost:63342 * Connection: keep-alive * Upgrade-Insecure-Requests: 1 * User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 * Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,;q=0.8,application/signed-exchange;v=b3;q=0.9 * Sec-Fetch-Site:none * Sec-Fetch-Mode:navigate * Sec-Fetch-Dest:document * Accept-Encoding:gzip,deflate,br * Accept-Language:zh-CN,zh;q=0.9 * Cookie:Idea-31d5f4eb=af750744-2158-4f3c-bba9-e7eae63a38fd * * 请求体 */ public class Request { private String method; private String requestURI; private String requestURL; private String contextPath; private String servletPath; private String protocol; private Map header; private InputStream inputStream; private String requestStr; private Map contextMap; public Request(InputStream inputStream) throws IOException { this.inputStream = inputStream; this.header = new HashMap<>(); parseRequestStr(); parseRequestLine(); parseRequestHeader(); parsePath(); scanContext(); System.out.println("contextPath:" + this.contextPath + "\n" + "servletPath:" + this.servletPath); } private void scanContext() throws IOException { this.contextMap = new HashMap<>(); scanContextOfWebapps(); scanContextOfServerXml(); } private void scanContextOfServerXml() throws IOException { File serverXml = Constant.ServerXML; Document document = Jsoup.parse(serverXml, "utf-8"); Elements elements = document.select("Context"); for (Element element : elements) { String path = element.attr("path"); String docBase = element.attr("docBase"); Context context = new Context(path, docBase); this.contextMap.put(path, context); } } private void scanContextOfWebapps() { File webapps = Constant.webapps; if(webapps.isDirectory()){ File[] files = webapps.listFiles(); for (File file : files) { if(file.isDirectory()){ String path = file.getName(); String docBase = file.getAbsolutePath(); if(path.equals("ROOT")){ // when scanedpath is ROOT, simultaneously add ROOT and /... Context context = new Context("/ROOT", docBase); this.contextMap.put("/ROOT", context); path = "/"; }else { path = "/" + path; } Context context = new Context(path, docBase); this.contextMap.put(path, context); } } } } private void parsePath() { parseContextPath(); parseServletPath(); parseRequestURL(); } private void parseServletPath() { //obtain servletPath String removedURI = StringUtils.removePrefix(this.requestURI, this.contextPath); if(removedURI.length() == 0){ this.servletPath = "/"; }else { this.servletPath = removedURI; } } private void parseContextPath() { //obtain contextPath int index = this.requestURI.indexOf("/", 1); if(index != -1){ this.contextPath = this.requestURI.substring(0, index); } else { this.contextPath = this.requestURI; } } private void parseRequestURL() { String host = this.getHeader("Host"); this.requestURL = host + this.requestURI; } private void parseRequestHeader() { int startHeaderIndex = this.requestStr.indexOf("\r\n"); int endHeaderIndex = this.requestStr.indexOf("\r\n\r\n"); String headerStr = this.requestStr.substring(startHeaderIndex + 1, endHeaderIndex); String[] partsHeader = headerStr.split("\r\n"); for (String header : partsHeader) { int middle = header.indexOf(":"); String headerKey = header.substring(0, middle).trim(); String headerValue = header.substring(middle + 1).trim(); this.header.put(headerKey, headerValue); } } private void parseRequestLine() { int index = this.requestStr.indexOf("\r\n"); String requestLine = this.requestStr.substring(0, index); String[] parts = requestLine.split(" "); for (int i = 0; i < parts.length; i++) { if(i == 0){ this.method = parts[i]; }else if(i == 1){ this.requestURI = parts[i]; }else { this.protocol = parts[i]; } } } private void parseRequestStr() throws IOException { int count = 0; count = this.inputStream.available(); while (count == 0){ count = this.inputStream.available(); } byte[] bytes = new byte[count]; this.inputStream.read(bytes); this.requestStr = new String(bytes, 0, count); } public Map getContextMap() { return contextMap; } public String getMethod() { return method; } public String getRequestURI() { return requestURI; } public String getContextPath() { return contextPath; } public String getServletPath() { return servletPath; } public String getProtocol() { return protocol; } public String getHeader(String key) { return this.header.get(key); } public String getRequestURL() { return requestURL; } public InputStream getInputStream() { return inputStream; } public String getRequestStr() { return requestStr; } } ``` 注意目前第三四部分代码主要实现是将所有应用保存在一个contextMap中,并没有处理响应逻辑,这部分将在第五部分实现。 ## 5. Welcome-File and Respond Binary File 欢迎界面即当前无请求资源时(servletPath为"/"),会给用户默认显示一个界面。在Tomcat服务器通过解析web.xml文件中welcome-file-list标签,将index.html,index.html,index.jsp来作为欢迎界面,那如果在Tomcat服务器应用中没有这些资源时,会给用户返回一个404。 二进制文件,什么时二进制文件呢 ?图片、视频都属于二进制文件,这些文件不可以使用文本字符集,因为这些文件有属于自己的编码格式。所以想让游览器渲染这些二进制文件,则必须告诉游览器,当前文件类型。Tomcat则通过解析web.xml文件中mime-mapping标签最后响应给游览器进行响应渲染。 我们也仿照这个逻辑写个欢迎界面和响应二进制文件,首先在项目中conf目录中配置一个web.xml ```xml index.html index.htm index.jsp html text/html png image/png jpeg image/jpeg jpg image/jpeg mp3 audio/mpeg mp4 video/mp4 ``` 解析文件标签代码逻辑在Utils中。 准备好了第3,4,5部分后,来实现响应逻辑业务。根据客户端发来的请求,我们将通过解析网络路径得到当前的context,并根据context内容进行相应的处理,最后得到context的stream进行相应,处理逻辑如下(如果不知道如何复现,请先画下流程图,在代码下方): ```java package com.yun.server; import com.yun.server.catalina.Context; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.FileUtils; import com.yun.server.utils.WebXmlUtils; import org.omg.CORBA.RepositoryIdHelper; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Map; public class BootStrap { public static void main(String[] args) { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(8080); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Request request = new Request(requestInputStream); response = new Response(responseOutputStream); /*---------------------------------------------*/ Map contextMap = request.getContextMap(); Context context = contextMap.get(request.getContextPath()); String servletPath = request.getServletPath(); File file = null; //如果是缺省请求资源则显示欢迎界面 if("/".equals(servletPath)){ file = WebXmlUtils.getWelcomeFile(context); }else { // 如果不是,要看看请求资源是否是二进制文件,如果是就需要响应给游览器响应文件类型 if(servletPath.contains(".")){ String mimeType = WebXmlUtils.getMimeType(servletPath); if(!mimeType.equals("text/html")){ response.setContentType(mimeType); } } //没有二进制文件,则直接得到当前请求资源对应本地硬盘路径 file = new File(context.getDocBase() + servletPath); } //最终通过流写入响应报文中.... if(file.exists() && !file.isDirectory()){ byte[] bytes = FileUtils.getBytes(file); response.setResponseBody(bytes); return; } //如果服务器中没有这样的应用就直接返回404 response.setStatus(404); response.getWriter().println("400
"); response.getWriter().println("File Not Found..."); /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` image-20221021170136157 ## 6. Component Object 目前为止,我们其实已经实现了完整静态服务器功能。但是所有的逻辑都堆积在BootStrap中(我们可以看到监听的端口号,默认主机机业务都在在BootStrap中),随着后续功能的展开,额外功能的扩充,代码会变得越来越庞杂,越来越难以维护。所以可以设置多个组件对象,可以降低代码之间的耦合,同时后续功能扩展时,也更加方便。 **Server**代表了当前服务器,有且只有一个。 **Service**代表了一组提供服务、处理请求、做出响应的组件。 **Connector**代表了客户端和服务器进行交互的组件,给Engine提供统一的服务,将engine的功能与各种协议分隔开,比如HTTP协议、HTTPS协议、AJP协议等。 **Engine**代表了运行Servlet的环境,主要职责是将Connector发送过来的请求进一步交给虚拟主机Host来处理。 **Host**代表了虚拟主机,负责管理应用Context。 **Context**代表了一个应用,提供Servlet运行的具体环境。 ![image-20221021155859891](README.assets/image-20221021155859891.png) 组件结构在Server.xml文件中,即 ```xml ``` 读取组件信息代码在ServerXmlUtils中,同时需要为每一个组件设置相应的类 **Server**组件 ```java package com.yun.server.catalina; import com.yun.server.utils.ServerXmlUtils; import java.util.List; public class Server { private List serviceList; public Server() { this.serviceList = ServerXmlUtils.getServiceList(); } public void start(){ for (Service service : this.serviceList) { service.start(); } } } ``` **Service**组件 ```java package com.yun.server.catalina; import java.util.List; public class Service { private String name; private List connectorList; private Engine engine; public void setName(String name) { this.name = name; } public void setConnectorList(List connectorList) { this.connectorList = connectorList; } public void setEngine(Engine engine) { this.engine = engine; } public Engine getEngine() { return engine; } public void start() { for (Connector connector : connectorList) { new Thread(connector).start(); } } } ``` **Connector**组件 ```java package com.yun.server.catalina; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.FileUtils; import com.yun.server.utils.WebXmlUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Map; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ Context context = request.getContext(); if(context != null){ String servletPath = request.getServletPath(); //welcome interface、binary file File file = null; if("/".equals(servletPath)){ file = WebXmlUtils.getWelcomeFile(context); }else { if(servletPath.contains(".")){ String mimeType = WebXmlUtils.getMimeType(servletPath); if(!mimeType.equals("text/html")){ response.setContentType(mimeType); } } file = new File(context.getDocBase() + servletPath); } if(file.exists() && !file.isDirectory()){ byte[] bytes = FileUtils.getBytes(file); response.setResponseBody(bytes); return; } } response.setStatus(404); response.getWriter().println("400
"); response.getWriter().println("File Not Found..."); /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` **Engine**组件 ```java package com.yun.server.catalina; import java.util.List; public class Engine { private String name; private String defaultHost; private List hostList; public void setName(String name) { this.name = name; } public void setDefaultHost(String defaultHost) { this.defaultHost = defaultHost; } public void setHostList(List hostList) { this.hostList = hostList; } public List getHostList() { return hostList; } public String getDefaultHost() { return defaultHost; } } ``` **Host**组件 ```java package com.yun.server.catalina; import java.util.List; import java.util.Map; public class Host { private String name; private String appBase; private Map contextMap; public void setName(String name) { this.name = name; } public void setAppBase(String appBase) { this.appBase = appBase; } public void setContextMap(Map contextMap) { this.contextMap = contextMap; } public String getAppBase() { return appBase; } public String getName() { return name; } public Map getContextMap() { return contextMap; } } ``` **Context**组件 ```java package com.yun.server.catalina; public class Context { private String path; private String docBase; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; } public String getPath() { return path; } public String getDocBase() { return docBase; } } ``` BootStrap:启动服务器 ```java package com.yun.server; import com.yun.server.catalina.Server; public class BootStrap { public static void main(String[] args) { Server server = new Server(); server.start(); } } ``` ## Utils ### Constant ```java package com.yun.server.utils; import java.io.File; public class Constant { public final static File webapps = new File(System.getProperty("user.dir"), "webapps"); public final static File ROOT = new File(webapps, "ROOT"); public final static File conf = new File(System.getProperty("user.dir"), "conf"); public final static File ServerXml = new File(conf, "server.xml"); public final static File WebXml = new File(conf, "web.xml"); } ``` ### FileUtils ```java package com.yun.server.utils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class FileUtils { public static byte[] getBytes(File file) throws IOException { FileInputStream fileInputStream = new FileInputStream(file); int account = fileInputStream.available(); byte[] bytes = new byte[account]; fileInputStream.read(bytes, 0, account); return bytes; } } ``` ### StringUtils ```java package com.yun.server.utils; public class StringUtils { public static boolean isEmpty(String content){ if(content == null || content.equals("")){ return true; } return false; } public static String removePrefix(String content, String prefix){ if(!content.isEmpty()){ int i = content.indexOf(prefix); if(i == -1){ return content; } String removedContent = content.substring(i + prefix.length()); return removedContent; } return null; } } ``` ### WebXmlUtils ```java package com.yun.server.utils; import com.yun.server.catalina.Context; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.util.*; public class WebXmlUtils { private static List welcomeFileList = new ArrayList<>(); private static Map mimeMapping = new HashMap<>(); static { parseWelcomeFile(); parseMimeMapping(); } private static void parseMimeMapping() { try { Document document = Jsoup.parse(Constant.WebXML, "utf-8"); Elements elements = document.select("mime-mapping"); for (Element element : elements) { Elements extensionElement = element.select("extension"); Elements mimeTypeElement = element.select("mime-type"); String extension = extensionElement.text(); String mimeType = mimeTypeElement.text(); mimeMapping.put(extension, mimeType); } } catch (IOException e) { e.printStackTrace(); } } private static void parseWelcomeFile() { try { Document document = Jsoup.parse(Constant.WebXML, "utf-8"); Elements elements = document.select("welcome-file-list welcome-file"); for (Element element : elements) { String welcomeFile = element.text(); welcomeFileList.add(welcomeFile); } } catch (IOException e) { e.printStackTrace(); } } public static File getWelcomeFile(Context context){ String docBase = context.getDocBase(); for (String welcomeFile : welcomeFileList) { File file = new File(docBase + "/" + welcomeFile); if(file.exists() && !file.isDirectory()){ return file; } } return new File(docBase + "/" + "index.html"); } public static String getMimeType(String servletPath) { Set extensionSet = mimeMapping.keySet(); for (String extension : extensionSet) { if(servletPath.endsWith(extension)){ return mimeMapping.get(extension); } } return "text/html"; } } ``` ### ServerXmlUtils ```java package com.yun.server.utils; import com.yun.server.catalina.*; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ServerXmlUtils { public static List getServiceList() { List serviceList = new ArrayList<>(); try { Document document = Jsoup.parse(Constant.ServerXml, "utf-8"); Elements servicesElements = document.select("Server Service"); for (Element serviceElement : servicesElements) { String name = serviceElement.attr("name"); Service service = new Service(); service.setName(name); service.setConnectorList(getConnectorList(serviceElement, service)); service.setEngine(getEngine(serviceElement)); serviceList.add(service); } } catch (IOException e) { e.printStackTrace(); } return serviceList; } private static Engine getEngine(Element serviceElement) { Engine engine = new Engine(); Elements engineElement = serviceElement.select("Engine"); String name = engineElement.attr("name"); String defaultHost = engineElement.attr("defaultHost"); engine.setName(name); engine.setDefaultHost(defaultHost); engine.setHostList(getHostList(engineElement)); return engine; } private static List getConnectorList(Element serviceElement, Service service) { List connectorList = new ArrayList<>(); Elements connectorElements = serviceElement.select("Connector"); for (Element connectorElement : connectorElements) { String port = connectorElement.attr("port"); Connector connector = new Connector(); connector.setPort(Integer.parseInt(port)); connector.setService(service); connectorList.add(connector); } return connectorList; } private static List getHostList(Elements engineElement) { List hostList = new ArrayList<>(); Elements hostElements = engineElement.select("Host"); for (Element hostElement : hostElements) { String name = hostElement.attr("name"); String appBase = hostElement.attr("appBase"); Host host = new Host(); host.setName(name); host.setAppBase(appBase); host.setContextMap(getContextMap(host, hostElement)); hostList.add(host); } return hostList; } private static Map getContextMap(Host host, Element hostElement) { Map contextMap = new HashMap<>(); try { scanContextOfServerXml(hostElement, contextMap); scanContextOfAppBase(host, contextMap); } catch (IOException e) { e.printStackTrace(); } return contextMap; } private static void scanContextOfServerXml(Element hostElement, Map contextMap) throws IOException { Elements contextElements = hostElement.select("Context"); for (Element contextElement : contextElements) { String path = contextElement.attr("path"); String docBase = contextElement.attr("docBase"); Context context = new Context(path, docBase); contextMap.put(path, context); } } private static void scanContextOfAppBase(Host host, Map contextMap) throws FileNotFoundException { String appBaseStr = host.getAppBase(); File appBase = new File(appBaseStr); if(!appBase.exists()){ throw new FileNotFoundException("appBase[" + appBase + "]不存在"); } File[] files = appBase.listFiles(); for (File file : files) { if(file.isDirectory()){ String path = file.getName(); String docBase = file.getAbsolutePath(); if(path.equals("ROOT")){ // when scanedPath is ROOT, simultaneously add ROOT and /... Context context = new Context("/ROOT", docBase); contextMap.put("/ROOT", context); path = "/"; }else { path = "/" + path; } Context context = new Context(path, docBase); contextMap.put(path, context); } } } } ``` # Phase2-Dynamic Server 对静态服务器和动态服务器,不要从语义上理解,而是要是要看客户端的响应时如何产生的。如果是程序产生的结果那么服务器是动态的,如是只是响应图片,文本文件那么就是静态的。 第二阶段,我们将要实现可以处理动态资源请求,也就是可以处理Servlet。 回顾一下,之前学习EE时,EE项目应该具有哪些特点? 1.应用根目录内需要有一个WEB-INF目录,在该目录下需要有classes、lib、web.xml 2.一个请求到来时,执行逻辑应该是什么样的呢?首先先找有没有对应的servlet可以处理该请求,如果有的话,则调用相应的servlet来处理,如果没有的话,则调用缺省Servlet来处理。 ## 1. Configure Servlet 在EE阶段,使用ee项目,我们要先编写一个servlet,编译,利用我们编写的服务器虚拟映射部署该应用。根据得到的servletPath,需要读取web.xml配置文件(不同servlet的url-pattern不能冲突),实例化一个servlet对象(反射),根据url-pattern,调用对应的servlet.service(request,response)方法,与此同时,将请求报文的封装对象request、以及response对象作为参数传递进去,执行完之后,返回结果,读取response,做出响应。 仿照Tomcat的目录结构,我们在服务器conf目录下新建context.xml文件 ```xml WEB-INF/web.xml ${catalina.base}/conf/web.xml ``` 为什么tomcat会自动到我们应用WEB-INF目录下寻找对应的配置文件,原因就在于这。同时需要工具来解析得到他。 此外,我们需要再server.xml添加servlet应用 ```xml ``` 可是我们在解析应用组件时怎么知道它是否为servlet应用?我们需要对每一个context组件进行判断,当前docBase是否有WEB-INF/web.xml文件,此外我们处理的逻辑应该在哪里写呢?context类中写。如果当前docBaseweb.xml则将他们进行封装,代码如下: ```java package com.yun.server.catalina; import com.yun.server.utils.ContextXmlUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; public class Context { private String path; private String docBase; private Map servletNameUrlPattern; private Map servletNameServletClass; //检验是否有多个servlet对应一个urlPattern private Map servletClassUrlPattern; private Map urlPatternServletClass; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; this.servletClassUrlPattern = new HashMap<>(); this.urlPatternServletClass = new HashMap<>(); this.servletNameUrlPattern = new HashMap<>(); this.servletNameServletClass = new HashMap<>(); //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists()){ deploy(servletFile); } } private void deploy(File servletFile) { //主要是来得到servletClassUrlPattern parseServlet1(servletFile); //检查是否有多个servlet对应一个urlPattern checkServlet(); //得到最终想要的urlPatternServletClass parseServlet2(); } private void parseServlet2() { Set servletNameKeys = this.servletNameUrlPattern.keySet(); Set servletMappingNameKeys = this.servletNameServletClass.keySet(); for (String servletNameKey : servletNameKeys) { for (String servletMappingNameKey : servletMappingNameKeys) { if(servletNameKey.equals(servletMappingNameKey)){ String servletClass = this.servletNameServletClass.get(servletNameKey); String urlPattern = this.servletNameUrlPattern.get(servletNameKey); this.urlPatternServletClass.put(urlPattern, servletClass); } } } } private void checkServlet() { Set servletClass1 = this.servletClassUrlPattern.keySet(); Set servletClass2 = this.servletClassUrlPattern.keySet(); for (String class1 : servletClass1) { for (String class2 : servletClass2) { if(!class1.equals(class2)){ String urlPattern1 = this.servletClassUrlPattern.get(class1); String urlPattern2 = this.servletClassUrlPattern.get(class2); if(urlPattern1.equals(urlPattern2)){ throw new IllegalArgumentException("The servlets named [" + servletClass1 + "] and [" + servletClass1 + "] " + "are both mapped to the url-pattern [" + urlPattern1 + "] which is not permitted" ); } } } } } private void parseServlet1(File servletFile) { try { Document document = Jsoup.parse(servletFile, "utf-8"); Elements servletElements = document.select("servlet"); for (Element servletElement : servletElements) { Elements servletNameElement = servletElement.select("servlet-name"); Elements servletClassElement = servletElement.select("servlet-class"); String servletName = servletNameElement.text(); String servletClass = servletClassElement.text(); this.servletNameServletClass.put(servletName, servletClass); } Elements servletMappingElements = document.select("servlet-mapping"); for (Element servletMappingElement : servletMappingElements) { Elements servletNameElement = servletMappingElement.select("servlet-name"); Elements urlPatternElement = servletMappingElement.select("url-pattern"); String servletName = servletNameElement.text(); String urlPattern = urlPatternElement.text(); this.servletNameUrlPattern.put(servletName, urlPattern); } Set servletNameKeys = this.servletNameUrlPattern.keySet(); Set servletMappingNameKeys = this.servletNameServletClass.keySet(); for (String servletNameKey : servletNameKeys) { for (String servletMappingNameKey : servletMappingNameKeys) { if(servletNameKey.equals(servletMappingNameKey)){ String servletClass = this.servletNameServletClass.get(servletNameKey); String urlPattern = this.servletNameUrlPattern.get(servletNameKey); this.servletClassUrlPattern.put(servletClass,urlPattern); } } } } catch (IOException e) { e.printStackTrace(); } } public String getPath() { return path; } public String getDocBase() { return docBase; } } ``` ## 2. Call Servlet 在调用servlet逻辑之前,别忘了,我们是功能的提供者,servlet需要传入HttpServletRequest和HttpServletResponse接口子对象,那么我们服务器request和response满足这个条件吗?当然不满足,所以我们需要继承这些类。注意最好不要直接implements,因为那样我们需要实现所有接口方法,中间可以加一个基类作为中介来让我们的request、response成为子对象。 ```java package com.yun.server.http; import javax.servlet.*; import javax.servlet.http.*; import java.io.BufferedReader; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.Principal; import java.util.Collection; import java.util.Enumeration; import java.util.Locale; import java.util.Map; public class BaseRequest implements HttpServletRequest { @Override public String getAuthType() { return null; } @Override public Cookie[] getCookies() { return new Cookie[0]; } @Override public long getDateHeader(String s) { return 0; } @Override public String getHeader(String s) { return null; } @Override public Enumeration getHeaders(String s) { return null; } @Override public Enumeration getHeaderNames() { return null; } @Override public int getIntHeader(String s) { return 0; } @Override public String getMethod() { return null; } @Override public String getPathInfo() { return null; } @Override public String getPathTranslated() { return null; } @Override public String getContextPath() { return null; } @Override public String getQueryString() { return null; } @Override public String getRemoteUser() { return null; } @Override public boolean isUserInRole(String s) { return false; } @Override public Principal getUserPrincipal() { return null; } @Override public String getRequestedSessionId() { return null; } @Override public String getRequestURI() { return null; } @Override public StringBuffer getRequestURL() { return null; } @Override public String getServletPath() { return null; } @Override public HttpSession getSession(boolean b) { return null; } @Override public HttpSession getSession() { return null; } @Override public String changeSessionId() { return null; } @Override public boolean isRequestedSessionIdValid() { return false; } @Override public boolean isRequestedSessionIdFromCookie() { return false; } @Override public boolean isRequestedSessionIdFromURL() { return false; } @Override public boolean isRequestedSessionIdFromUrl() { return false; } @Override public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { return false; } @Override public void login(String s, String s1) throws ServletException { } @Override public void logout() throws ServletException { } @Override public Collection getParts() throws IOException, ServletException { return null; } @Override public Part getPart(String s) throws IOException, ServletException { return null; } @Override public T upgrade(Class aClass) throws IOException, ServletException { return null; } @Override public Object getAttribute(String s) { return null; } @Override public Enumeration getAttributeNames() { return null; } @Override public String getCharacterEncoding() { return null; } @Override public void setCharacterEncoding(String s) 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 s) { return null; } @Override public Enumeration getParameterNames() { return null; } @Override public String[] getParameterValues(String s) { return new String[0]; } @Override public Map 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 s, Object o) { } @Override public void removeAttribute(String s) { } @Override public Locale getLocale() { return null; } @Override public Enumeration getLocales() { return null; } @Override public boolean isSecure() { return false; } @Override public RequestDispatcher getRequestDispatcher(String s) { return null; } @Override public String getRealPath(String s) { 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; } } ``` ```java package com.yun.server.http; import com.sun.corba.se.spi.ior.EncapsulationFactoryBase; import com.yun.server.catalina.Context; import com.yun.server.catalina.Engine; import com.yun.server.catalina.Host; import com.yun.server.utils.StringUtils; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 请求行 * GET /http/demo1/2.html HTTP/1.1 * * 请求头 * Host: localhost:63342 * Connection: keep-alive * Upgrade-Insecure-Requests: 1 * User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 * Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,;q=0.8,application/signed-exchange;v=b3;q=0.9 * Sec-Fetch-Site:none * Sec-Fetch-Mode:navigate * Sec-Fetch-Dest:document * Accept-Encoding:gzip,deflate,br * Accept-Language:zh-CN,zh;q=0.9 * Cookie:Idea-31d5f4eb=af750744-2158-4f3c-bba9-e7eae63a38fd * * 请求体 */ public class Request extends BaseRequest{ private String method; private String requestURI; private String requestURL; private String contextPath; private String servletPath; private String protocol; private Map header; private InputStream inputStream; private String requestStr; private Map contextMap; //来选择合适的组件对象... private Engine engine; private Host host; private Context context; public Request(InputStream inputStream, Engine engine) throws IOException { this.inputStream = inputStream; this.engine = engine; parseRequestStr(); parseRequestLine(); parseRequestHeader(); parsePath(); parseHost(); System.out.println("contextPath:" + this.contextPath + "\n" + "servletPath:" + this.servletPath); } //Engine只有一个所以不需要解析,但是Host,Context有多个需要解析.... private void parseHost() { List hostList = this.engine.getHostList(); String domainName = this.header.get("Host"); for (Host host : hostList) { //如果有相应的Host,则选择 if(host.getName().equals(domainName)){ this.host = host; } } //遍历了所有Host发现没有相应的Host,则选择缺省Host if(this.host == null){ String defaultHost = this.engine.getDefaultHost(); for (Host localhost : hostList) { if(localhost.getName().equals(defaultHost)){ this.host = localhost; } } } //选择了Host,就该选择Context parseContext(); } private void parseContext() { Map contextMap = this.host.getContextMap(); this.contextMap = contextMap; this.context = this.contextMap.get(getContextPath()); } private void parsePath() { parseContextPath(); parseServletPath(); parseRequestURL(); } private void parseServletPath() { //obtain servletPath String removedURI = StringUtils.removePrefix(this.requestURI, this.contextPath); if(removedURI.length() == 0){ this.servletPath = "/"; }else { this.servletPath = removedURI; } } private void parseContextPath() { //obtain contextPath int index = this.requestURI.indexOf("/", 1); if(index != -1){ this.contextPath = this.requestURI.substring(0, index); } else { this.contextPath = this.requestURI; } } private void parseRequestURL() { String host = this.getHeader("Host"); this.requestURL = host + this.requestURI; } private void parseRequestHeader() { this.header = new HashMap<>(); int startHeaderIndex = this.requestStr.indexOf("\r\n"); int endHeaderIndex = this.requestStr.indexOf("\r\n\r\n"); String headerStr = this.requestStr.substring(startHeaderIndex + 1, endHeaderIndex); String[] partsHeader = headerStr.split("\r\n"); for (String header : partsHeader) { int middle = header.indexOf(":"); String headerKey = header.substring(0, middle).trim(); String headerValue = header.substring(middle + 1).trim(); this.header.put(headerKey, headerValue); } } private void parseRequestLine() { int index = this.requestStr.indexOf("\r\n"); String requestLine = this.requestStr.substring(0, index); String[] parts = requestLine.split(" "); for (int i = 0; i < parts.length; i++) { if(i == 0){ this.method = parts[i]; }else if(i == 1){ this.requestURI = parts[i]; }else { this.protocol = parts[i]; } } } private void parseRequestStr() throws IOException { int count = 0; count = this.inputStream.available(); while (count == 0){ count = this.inputStream.available(); } byte[] bytes = new byte[count]; this.inputStream.read(bytes); this.requestStr = new String(bytes, 0, count); } public String getMethod() { return method; } public String getRequestURI() { return requestURI; } public String getContextPath() { return contextPath; } public String getServletPath() { return servletPath; } public String getProtocol() { return protocol; } public String getHeader(String key) { return this.header.get(key); } public String getRequestStr() { return requestStr; } public Context getContext() { return context; } } ``` 调用Servlet逻辑: ```java package com.yun.server.catalina; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.FileUtils; import com.yun.server.utils.StringUtils; import com.yun.server.utils.WebXmlUtils; import javax.servlet.Servlet; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { System.out.println("Starting ProtocolHandler [HTTP/1.1-bio-" + this.port + "]"); serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ Context context = request.getContext(); if(context != null){ String servletPath = request.getServletPath(); //welcome interface、binary file File file = null; if("/".equals(servletPath)){ file = WebXmlUtils.getWelcomeFile(context); }else { if(servletPath.contains(".")){ String mimeType = WebXmlUtils.getMimeType(servletPath); if(!mimeType.equals("text/html")){ response.setContentType(mimeType); } } //调用servlet逻辑 String servletClass = context.getUrlPatternServletClass(servletPath); if(!StringUtils.isEmpty(servletClass)){ Class aClass = Class.forName(servletClass); Servlet servlet = (Servlet) aClass.newInstance(); servlet.service(request, response); return; } file = new File(context.getDocBase() + servletPath); } if(file.exists() && !file.isDirectory()){ byte[] bytes = FileUtils.getBytes(file); response.setResponseBody(bytes); return; } } response.setStatus(404); response.getWriter().println("400
"); response.getWriter().println("File Not Found..."); /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` 调用后,你会发现当前错误 ![image-20221023221537090](README.assets/image-20221023221537090.png) 客户端会如下显示 ![image-20221023221558097](README.assets/image-20221023221558097.png) ## 3. Class Loading(重点) 造就当前的原因时因为当前类(servlet)根本就没有加载到JVM内存中... ### 类加载 首先触发类加载的条件有: 1.第一次new一个对象时 2.调用静态方法或变量时,例如,类.方法,工具类.. 3.反射,例如Class.forName(全限定类名) 4.初始化一个子类(会首先初始化父类) 5.JVM启动时标明的启动类,例如java -BootStrp,这个BootStrap.class首发会被加载到内存中 可以看到我们使用反射为什么没有加载到内存中呢?首先类加载这个动作的执行者是类加载器,类加载通过类加载器来把类加载到内存中,那么类加载器具体又是什么呢? ### 类加载器 类加载器主要有以下三类: 1.BootStrap类加载器,负责加载 %JDK_HOME%\jre\lib 目录下的 rt.jar, tools .jar 等 jdk 核心常见类。这就是为什么我们使用JDK中的类不需要额外导包的原因(导包其实就是完成这个步骤) 2.Extension类加载器,负责加载%JDK_HOME%\jre\lib\ext 目录下的类 3.Application类加载器,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中的默认类加载器。 所以当前就可以回答上边那个问题了,为什么没有加载到内存中呢?因为这个servlet程序根本就不存在于-classpath路径下,那么怎么办呢?可不可以自己设计一个类加载器来加载其他路径下的class文件呢?当然可以。 同时有没有想过,平时敲代码为什么没有感受到这些类加载的操作呢?那是因为idea(集成开发环境)都帮你实现好了这些过程。 image-20221023230657914 idea帮你实现的过程:蓝色的java目录,就是classpath路径(应用类加载器加载的路径),idea中需要你把一个目录变成蓝色(source文件),就会变成classpath路径。idea会使用指令连同项目的其他依赖(External libraries)一起加载到JVM内存中。如果我们不使用idea,像Tomcat那样不依赖于IDE独立运行,那么这些操作就需要我们实现。 在实现这些操作之前,我们还需要了解类加载器的加载机制。 ### 双亲委派机制 当一个ClassLoader对象需要加载某个类时,在它试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,父类加载器继续向上委托,直到BootstrapClassLoader类加载器为止。即,首先由最顶层的类加载器BootstrapClassLoader在指定目录试图加载目标类,如果没加载到,则把任务回退给ExtensionClassLoader,让它在指定目录进行加载,如果它也没加载到,则继续回退给AppClassLoader 进行加载,以此类推。如果所有的加载器都没有找到该类,则抛出ClassNotFoundException异常。否则将这个找到的“*.class”文件进行解析,最后返回表示该类的Class对象。 image-20221023231213641 ### 双亲委派机制的意义 1)这样就保证了一个类在JVM内存中只有一个类对象,避免重复加载,当父加载器已经加载了该类的时候,子加载器就没有必要加载该类,也不应该再加载一次。例如,JDK中的这些类,按照双亲委派的规则,一定会从BootStrap中去查找,不会用自定义的类加载器去加载,这样就可以保障无论在任何环境下,使用这些基本类时,使用的都是相同的类。不会因为环境的不同而加载不同的类,执行结果不同。但是如果有多个类加载器对象加载同一个类,就会产生多个类对象(这就是传说中的打破双亲委派机制)。 2)核心类通过Java自带的加载器加载,可以确保这些类的字节码没有被篡改,保证代码的安全性。 (JVM在判定两个**Class对象**是否相同时,不仅要满足两个类名相同,而且要满足由同一个类加载器加载。只有两者同时满足的情况下,JVM才认为这两个Class对象是相同的) 但是在tomcat中,在某些地方,却需要打破双亲委派的这种机制。比如服务器里部署了很多应用,每个应用都需要加载fileupload相关的jar包,但是使用的版本不同,如果按照双亲委派机制,不管任何应用,全部都只能使用同一个jar包的同一个版本,这在实际情况中肯定是不可以的,所以需要重新设计类加载器,即每个Servlet应用都用一个新的类加载加载从而来打破这个限制。 所以现在我们有两个需求: 1. 当我们不借用IDE来运行服务器时,需要一个自定义类加载器来加载idea中的所有class文件包括依赖。 2. 为每个servlet应用设计一个自定义类加载器来加载WEB-INF目录下的classes和lib ### 自定义类加载器 如果需要独立运行服务器,那么我们需要把需要依赖都拿出来: image-20221025221919012 首先看一下类加载器的定义 A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system. The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance. A sample implementation is: ```java class NetworkClassLoader extends ClassLoader { public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String name) { // load the class data from the connection . . . } } ``` ```java package com.yun.server.classloader; import com.yun.server.utils.FileUtils; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class testClassLoader extends ClassLoader{ //通过全类名去加载类 public Class findClass(String name) { byte[] b = loadClassData(name); //利用全类名、class文件的二进制数据定义一个Class return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String name) { String fileName = name.replaceAll("\\.", "/") + ".class"; //通过IO流来读取文件信息,可以自己找个已经编译过的java文件,来尝试此案例 File file = new File("F:\\JAVA\\idea_project\\Maven_se\\target\\classes", fileName); return FileUtils.getBytes(file); } public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { testClassLoader testClassLoader = new testClassLoader(); Class aClass = testClassLoader.loadClass("com.yun.Demo.ServerDemo"); Object o = aClass.newInstance(); Method start = aClass.getDeclaredMethod("start"); start.invoke(o); } } ``` 可是此案例只能加载一个目录中的class文件,在实际使用的过程中,如果处理jar包里面的class比较麻烦,我们可以使用URLClassloader来实现当前功能。 该类加载器负责加载服务器lib目录下面的jar包: ```java package com.yun.server.classloader; import com.yun.server.utils.FileUtils; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class CommonClassLoader extends URLClassLoader { public CommonClassLoader() { super(new URL[]{}); File file = new File(System.getProperty("user.dir"), "lib"); File[] files = file.listFiles(); for (File jar : files) { if(jar.getName().endsWith(".jar")){ try { //当前方法无非就是把上述案例进行了包装,让加载器可以加载多个jar包 addURL(new URL("file:" + jar.getAbsolutePath())); } catch (MalformedURLException e) { e.printStackTrace(); } } } } } ``` BootStrap代码也要进行修改 ```java public class BootStrap { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CommonClassLoader commonClassLoader = new CommonClassLoader(); //方面后续应用设置父类加载器,因为我们需要CommonClassLoader变成servlet加载器的父加载器... Thread.currentThread().setContextClassLoader(commonClassLoader); //这地方使用类加载器加载的原因是:当时候要将整个项目打成jar包,方便运行 Class aClass = commonClassLoader.loadClass("com.yun.server.catalina.Server"); Object server = aClass.newInstance(); Method start = aClass.getDeclaredMethod("start"); start.invoke(server); } } ``` 定义一个CommonClassloader来加载整个服务器的资源,但是此时类加载器仍然是App类加载器。为什么呢?因为在IDEA集成开发环境下,当Common类加载器加载Server的时候,发现App类加载器已经加载了Server类,所以会向上追溯。可以使用批处理运行,观察这一现象。 ### 批处理运行服务器 不借助IDEA集成开发环境,使用自定义的类加载器来完成类加载 在应用根目录下新建一个bat文件,里面有如下指令: image-20221025193628087 1.首先删除根目录下面的bootstrap.jar文件,/q不提示确认删除对话框。 2.-C表示的是分别在该目录下执行jar指令,最终的效果就是将BootStrap.class以及classloader/CommonClassloader.class打在jar包内,后续执行时,只有这两个类是由App类加载器加载的,其余的所有的类都是由CommonClassloader加载的。 3.将应用所有的clas文件全部打包放置于lib目录中,形成tomcat.jar,这样CommonClassloader不仅加载了项目依赖还有整个项目。 4.最终执行。 ``` del /q bootstrap.jar jar cvf bootstrap.jar -C target/classes com/yun/server/BootStrap.class -C target/classes com/yun/server/classloader/CommonClassLoader.class del /q tomcat.jar cd target cd classes jar cvf ../../lib/tomcat.jar * cd .. cd .. java -cp bootstrap.jar com.yun.server.BootStrap pause ``` ![image-20221025223629060](README.assets/image-20221025223629060.png) ### Servlet类加载器 一个web应用,应该有一个独立的类加载器,负责加载应用根目录WEB-INF目录下的classes下的class文件以及lib目录下的jar包 ```java package com.yun.server.classloader; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class ServletClassLoader extends URLClassLoader { //只有请求资源时servlet时才使用此加载器 public ServletClassLoader(String docBase, ClassLoader parent) { super(new URL[]{}, parent); File WEBINF = new File(docBase, "WEB-INF"); if(!WEBINF.exists()){ return; } File libDirectory = new File(WEBINF, "lib"); File[] libFiles = libDirectory.listFiles(); for (File lib : libFiles) { if(lib.getName().endsWith(".jar")){ try { addURL(new URL("file:" + lib.getAbsolutePath())); } catch (MalformedURLException e) { e.printStackTrace(); } } } File classesDirectory = new File(WEBINF, "classes"); if(classesDirectory.exists()){ try { addURL(new URL("file:" + classesDirectory.getAbsolutePath() + "/")); } catch (MalformedURLException e) { e.printStackTrace(); } } } } ``` 如果当前应用是servlet应用,那么就需要创建一个新的类加载进行加载(打破双亲委派机制),同时设置为CommonClassLoader为父加载器(原因是CommonClassLoader加载了javax.servlet-api-3.1.0.jar包,让所有servletClassLoader都用这个父类加载器中的同名包),操作逻辑如下: ```java package com.yun.server.catalina; import com.yun.server.classloader.ServletClassLoader; import com.yun.server.utils.ContextXmlUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; public class Context { private String path; private String docBase; private Map servletNameUrlPattern; private Map servletNameServletClass; //检验是否有多个servlet对应一个urlPattern private Map servletClassUrlPattern; private Map urlPatternServletClass; private ServletClassLoader servletClassLoader; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; this.servletClassUrlPattern = new HashMap<>(); this.urlPatternServletClass = new HashMap<>(); this.servletNameUrlPattern = new HashMap<>(); this.servletNameServletClass = new HashMap<>(); //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists()){ deployServlet(servletFile); deployClassLoader(); } } private void deployClassLoader() { //如果未设置,则默认为父线程的上下文ClassLoader... ClassLoader commonClassLoader = Thread.currentThread().getContextClassLoader(); this.servletClassLoader = new ServletClassLoader(this.docBase, commonClassLoader); } private void deployServlet(File servletFile) { //主要是来得到servletClassUrlPattern parseServlet1(servletFile); //检查是否有多个servlet对应一个urlPattern checkServlet(); //得到最终想要的urlPatternServletClass parseServlet2(); } private void parseServlet2() { Set servletNameKeys = this.servletNameUrlPattern.keySet(); Set servletMappingNameKeys = this.servletNameServletClass.keySet(); for (String servletNameKey : servletNameKeys) { for (String servletMappingNameKey : servletMappingNameKeys) { if(servletNameKey.equals(servletMappingNameKey)){ String servletClass = this.servletNameServletClass.get(servletNameKey); String urlPattern = this.servletNameUrlPattern.get(servletNameKey); this.urlPatternServletClass.put(urlPattern, servletClass); } } } } private void checkServlet() { Set servletClass1 = this.servletClassUrlPattern.keySet(); Set servletClass2 = this.servletClassUrlPattern.keySet(); for (String class1 : servletClass1) { for (String class2 : servletClass2) { if(!class1.equals(class2)){ String urlPattern1 = this.servletClassUrlPattern.get(class1); String urlPattern2 = this.servletClassUrlPattern.get(class2); if(urlPattern1.equals(urlPattern2)){ throw new IllegalArgumentException("The servlets named [" + servletClass1 + "] and [" + servletClass1 + "] " + "are both mapped to the url-pattern [" + urlPattern1 + "] which is not permitted" ); } } } } } private void parseServlet1(File servletFile) { try { Document document = Jsoup.parse(servletFile, "utf-8"); Elements servletElements = document.select("servlet"); for (Element servletElement : servletElements) { Elements servletNameElement = servletElement.select("servlet-name"); Elements servletClassElement = servletElement.select("servlet-class"); String servletName = servletNameElement.text(); String servletClass = servletClassElement.text(); this.servletNameServletClass.put(servletName, servletClass); } Elements servletMappingElements = document.select("servlet-mapping"); for (Element servletMappingElement : servletMappingElements) { Elements servletNameElement = servletMappingElement.select("servlet-name"); Elements urlPatternElement = servletMappingElement.select("url-pattern"); String servletName = servletNameElement.text(); String urlPattern = urlPatternElement.text(); this.servletNameUrlPattern.put(servletName, urlPattern); } Set servletNameKeys = this.servletNameUrlPattern.keySet(); Set servletMappingNameKeys = this.servletNameServletClass.keySet(); for (String servletNameKey : servletNameKeys) { for (String servletMappingNameKey : servletMappingNameKeys) { if(servletNameKey.equals(servletMappingNameKey)){ String servletClass = this.servletNameServletClass.get(servletNameKey); String urlPattern = this.servletNameUrlPattern.get(servletNameKey); this.servletClassUrlPattern.put(servletClass,urlPattern); } } } } catch (IOException e) { e.printStackTrace(); } } public String getPath() { return path; } public String getDocBase() { return docBase; } public String getUrlPatternServletClass(String servletPath) { return this.urlPatternServletClass.get(servletPath); } public ServletClassLoader getServletClassLoader() { return servletClassLoader; } } ``` Connector逻辑也要进行一定的修改 ```java package com.yun.server.catalina; import com.yun.server.classloader.ServletClassLoader; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.FileUtils; import com.yun.server.utils.StringUtils; import com.yun.server.utils.WebXmlUtils; import javax.servlet.Servlet; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { System.out.println("Starting ProtocolHandler [HTTP/1.1-bio-" + this.port + "]"); serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ Context context = request.getContext(); if(context != null){ String servletPath = request.getServletPath(); //welcome interface、binary file File file = null; if("/".equals(servletPath)){ file = WebXmlUtils.getWelcomeFile(context); }else { if(servletPath.contains(".")){ String mimeType = WebXmlUtils.getMimeType(servletPath); if(!mimeType.equals("text/html")){ response.setContentType(mimeType); } } String servletClass = context.getUrlPatternServletClass(servletPath); if(!StringUtils.isEmpty(servletClass)){ ServletClassLoader servletClassLoader = context.getServletClassLoader(); Class aClass = servletClassLoader.loadClass(servletClass); Servlet servlet = (Servlet) aClass.newInstance(); servlet.service(request, response); return; } file = new File(context.getDocBase() + servletPath); } if(file.exists() && !file.isDirectory()){ byte[] bytes = FileUtils.getBytes(file); response.setResponseBody(bytes); return; } } response.setStatus(404); response.getWriter().println("400
"); response.getWriter().println("File Not Found..."); /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` ## 4. ServletContext 在学习JavaEE阶段时候,想要获得context域有两种方法,一种是通过request.getServletContext(),另一种是直接在service()方法里边调用getServletContext()。 ### request.getServletContext() 像Request,Response首发要设置一个中间基类 ```java package com.yun.server.http; import javax.servlet.*; import javax.servlet.descriptor.JspConfigDescriptor; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import java.util.EventListener; import java.util.Map; import java.util.Set; public class BaseServletContext implements ServletContext { @Override public String getContextPath() { return null; } @Override public ServletContext getContext(String s) { return null; } @Override public int getMajorVersion() { return 0; } @Override public int getMinorVersion() { return 0; } @Override public int getEffectiveMajorVersion() { return 0; } @Override public int getEffectiveMinorVersion() { return 0; } @Override public String getMimeType(String s) { return null; } @Override public Set getResourcePaths(String s) { return null; } @Override public URL getResource(String s) throws MalformedURLException { return null; } @Override public InputStream getResourceAsStream(String s) { return null; } @Override public RequestDispatcher getRequestDispatcher(String s) { return null; } @Override public RequestDispatcher getNamedDispatcher(String s) { return null; } @Override public Servlet getServlet(String s) throws ServletException { return null; } @Override public Enumeration getServlets() { return null; } @Override public Enumeration getServletNames() { return null; } @Override public void log(String s) { } @Override public void log(Exception e, String s) { } @Override public void log(String s, Throwable throwable) { } @Override public String getRealPath(String s) { return null; } @Override public String getServerInfo() { return null; } @Override public String getInitParameter(String s) { return null; } @Override public Enumeration getInitParameterNames() { return null; } @Override public boolean setInitParameter(String s, String s1) { return false; } @Override public Object getAttribute(String s) { return null; } @Override public Enumeration getAttributeNames() { return null; } @Override public void setAttribute(String s, Object o) { } @Override public void removeAttribute(String s) { } @Override public String getServletContextName() { return null; } @Override public ServletRegistration.Dynamic addServlet(String s, String s1) { return null; } @Override public ServletRegistration.Dynamic addServlet(String s, Servlet servlet) { return null; } @Override public ServletRegistration.Dynamic addServlet(String s, Class aClass) { return null; } @Override public T createServlet(Class aClass) throws ServletException { return null; } @Override public ServletRegistration getServletRegistration(String s) { return null; } @Override public Map getServletRegistrations() { return null; } @Override public FilterRegistration.Dynamic addFilter(String s, String s1) { return null; } @Override public FilterRegistration.Dynamic addFilter(String s, Filter filter) { return null; } @Override public FilterRegistration.Dynamic addFilter(String s, Class aClass) { return null; } @Override public T createFilter(Class aClass) throws ServletException { return null; } @Override public FilterRegistration getFilterRegistration(String s) { return null; } @Override public Map getFilterRegistrations() { return null; } @Override public SessionCookieConfig getSessionCookieConfig() { return null; } @Override public void setSessionTrackingModes(Set set) { } @Override public Set getDefaultSessionTrackingModes() { return null; } @Override public Set getEffectiveSessionTrackingModes() { return null; } @Override public void addListener(String s) { } @Override public void addListener(T t) { } @Override public void addListener(Class aClass) { } @Override public T createListener(Class aClass) throws ServletException { return null; } @Override public JspConfigDescriptor getJspConfigDescriptor() { return null; } @Override public ClassLoader getClassLoader() { return null; } @Override public void declareRoles(String... strings) { } @Override public String getVirtualServerName() { return null; } } ``` 然后继承这个基类 ```java package com.yun.server.http; import com.yun.server.catalina.Context; import java.util.*; public class ApplicationContext extends BaseServletContext{ private Context context; private Map servletContextMap = new HashMap<>(); public ApplicationContext(Context context) { this.context = context; } @Override public void setAttribute(String s, Object o) { this.servletContextMap.put(s, o); } @Override public Object getAttribute(String s) { return this.servletContextMap.get(s); } @Override public void removeAttribute(String s) { this.servletContextMap.remove(s); } @Override public Enumeration getAttributeNames() { Set keySet = this.servletContextMap.keySet(); return Collections.enumeration(keySet); } @Override public String getRealPath(String s) { if(!s.startsWith("/") && !s.startsWith("\\")){ s = "/" + s; } return this.context.getDocBase() + s; } } ``` 需要每个servlet应用,创建一个servletcontext,即 ```java private ApplicationContext servletContext; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; this.servletClassUrlPattern = new HashMap<>(); this.urlPatternServletClass = new HashMap<>(); this.servletNameUrlPattern = new HashMap<>(); this.servletNameServletClass = new HashMap<>(); //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists()){ deployServlet(servletFile); deployClassLoader(); //只有servlet应用才需要设置context域 this.servletContext = new ApplicationContext(this); } } public ApplicationContext getServletContext() { return servletContext; } ``` 最后通过request取出servletContext ```java package com.yun.server.http; import com.sun.corba.se.spi.ior.EncapsulationFactoryBase; import com.yun.server.catalina.Context; import com.yun.server.catalina.Engine; import com.yun.server.catalina.Host; import com.yun.server.utils.StringUtils; import javax.servlet.ServletContext; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 请求行 * GET /http/demo1/2.html HTTP/1.1 * * 请求头 * Host: localhost:63342 * Connection: keep-alive * Upgrade-Insecure-Requests: 1 * User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 * Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,;q=0.8,application/signed-exchange;v=b3;q=0.9 * Sec-Fetch-Site:none * Sec-Fetch-Mode:navigate * Sec-Fetch-Dest:document * Accept-Encoding:gzip,deflate,br * Accept-Language:zh-CN,zh;q=0.9 * Cookie:Idea-31d5f4eb=af750744-2158-4f3c-bba9-e7eae63a38fd * * 请求体 */ public class Request extends BaseRequest{ private String method; private String requestURI; private String requestURL; private String contextPath; private String servletPath; private String protocol; private Map header; private InputStream inputStream; private String requestStr; private Map contextMap; //来选择合适的组件对象... private Engine engine; private Host host; private Context context; public Request(InputStream inputStream, Engine engine) throws IOException { this.inputStream = inputStream; this.engine = engine; parseRequestStr(); parseRequestLine(); parseRequestHeader(); parsePath(); parseHost(); System.out.println("contextPath:" + this.contextPath + "\n" + "servletPath:" + this.servletPath); } //Engine只有一个所以不需要解析,但是Host,Context有多个需要解析.... private void parseHost() { List hostList = this.engine.getHostList(); String domainName = this.header.get("Host"); for (Host host : hostList) { //如果有相应的Host,则选择 if(host.getName().equals(domainName)){ this.host = host; } } //遍历了所有Host发现没有相应的Host,则选择缺省Host if(this.host == null){ String defaultHost = this.engine.getDefaultHost(); for (Host localhost : hostList) { if(localhost.getName().equals(defaultHost)){ this.host = localhost; } } } //选择了Host,就该选择Context parseContext(); } private void parseContext() { Map contextMap = this.host.getContextMap(); this.contextMap = contextMap; this.context = this.contextMap.get(getContextPath()); } private void parsePath() { parseContextPath(); parseServletPath(); parseRequestURL(); } private void parseServletPath() { //obtain servletPath String removedURI = StringUtils.removePrefix(this.requestURI, this.contextPath); if(removedURI.length() == 0){ this.servletPath = "/"; }else { this.servletPath = removedURI; } } private void parseContextPath() { //obtain contextPath int index = this.requestURI.indexOf("/", 1); if(index != -1){ this.contextPath = this.requestURI.substring(0, index); } else { this.contextPath = this.requestURI; } } private void parseRequestURL() { String host = this.getHeader("Host"); this.requestURL = host + this.requestURI; } private void parseRequestHeader() { this.header = new HashMap<>(); int startHeaderIndex = this.requestStr.indexOf("\r\n"); int endHeaderIndex = this.requestStr.indexOf("\r\n\r\n"); String headerStr = this.requestStr.substring(startHeaderIndex + 1, endHeaderIndex); String[] partsHeader = headerStr.split("\r\n"); for (String header : partsHeader) { int middle = header.indexOf(":"); String headerKey = header.substring(0, middle).trim(); String headerValue = header.substring(middle + 1).trim(); this.header.put(headerKey, headerValue); } } private void parseRequestLine() { int index = this.requestStr.indexOf("\r\n"); String requestLine = this.requestStr.substring(0, index); String[] parts = requestLine.split(" "); for (int i = 0; i < parts.length; i++) { if(i == 0){ this.method = parts[i]; }else if(i == 1){ this.requestURI = parts[i]; }else { this.protocol = parts[i]; } } } private void parseRequestStr() throws IOException { int count = 0; count = this.inputStream.available(); while (count == 0){ count = this.inputStream.available(); } byte[] bytes = new byte[count]; this.inputStream.read(bytes); this.requestStr = new String(bytes, 0, count); } public String getMethod() { return method; } public String getRequestURI() { return requestURI; } public String getContextPath() { return contextPath; } public String getServletPath() { return servletPath; } public String getProtocol() { return protocol; } public String getHeader(String key) { return this.header.get(key); } public String getRequestStr() { return requestStr; } public Context getContext() { return context; } //拿到当前应用的servletContext @Override public ServletContext getServletContext() { return this.context.getServletContext(); } } ``` ### getServletContext() 看一下当前方法的源码 ![image-20221026123338266](README.assets/image-20221026123338266.png) 可以看到getServletContext()方法,是调用GenericServlet类中的config对象得到的servletContext,所以我们最后也要通过init()方法传入一个servletconfig对象,并通过这个对象来获得servletcontext。 设置基类 ```java package com.yun.server.http; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import java.util.Enumeration; public class BaseConfig implements ServletConfig { @Override public String getServletName() { return null; } @Override public ServletContext getServletContext() { return null; } @Override public String getInitParameter(String s) { return null; } @Override public Enumeration getInitParameterNames() { return null; } } ``` 继承基类并实现相应方法 ```java package com.yun.server.http; import com.yun.server.catalina.Context; import javax.servlet.ServletContext; public class ApplicationConfig extends BaseConfig{ private Context context; public ApplicationConfig(Context context) { this.context = context; } @Override public ServletContext getServletContext() { return this.context.getServletContext(); } } ``` 为每个应用添加servletConfig ```java private ServletConfig servletConfig; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; this.servletClassUrlPattern = new HashMap<>(); this.urlPatternServletClass = new HashMap<>(); this.servletNameUrlPattern = new HashMap<>(); this.servletNameServletClass = new HashMap<>(); //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists()){ deployServlet(servletFile); deployClassLoader(); //只有servlet应用才需要设置context域 this.servletContext = new ApplicationContext(this); //设置servletConfig this.servletConfig = new ApplicationConfig(this); } } //获取servletConfig的get方法 public ApplicationConfig getServletConfig() { return servletConfig; } ``` 响应处理逻辑也要修改 ```java package com.yun.server.catalina; import com.yun.server.classloader.ServletClassLoader; import com.yun.server.http.ApplicationConfig; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.FileUtils; import com.yun.server.utils.StringUtils; import com.yun.server.utils.WebXmlUtils; import javax.servlet.GenericServlet; import javax.servlet.Servlet; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { System.out.println("Starting ProtocolHandler [HTTP/1.1-bio-" + this.port + "]"); serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ Context context = request.getContext(); if(context != null){ String servletPath = request.getServletPath(); //welcome interface、binary file File file = null; if("/".equals(servletPath)){ file = WebXmlUtils.getWelcomeFile(context); }else { //请求资源是否为二进制文件 if(servletPath.contains(".")){ String mimeType = WebXmlUtils.getMimeType(servletPath); if(!mimeType.equals("text/html")){ response.setContentType(mimeType); } } //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); if(!StringUtils.isEmpty(servletClass)){ ServletClassLoader servletClassLoader = context.getServletClassLoader(); Class aClass = servletClassLoader.loadClass(servletClass); //由原来的Servlet改成GenericServlet,因为只有GenericServlet才有这个 //getServletContext()方法 GenericServlet servlet = (GenericServlet) aClass.newInstance(); ApplicationConfig servletConfig = context.getServletConfig(); servlet.init(servletConfig); servlet.service(request, response); return; } file = new File(context.getDocBase() + servletPath); } if(file.exists() && !file.isDirectory()){ byte[] bytes = FileUtils.getBytes(file); response.setResponseBody(bytes); return; } } response.setStatus(404); response.getWriter().println("400
"); response.getWriter().println("File Not Found..."); /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` ## 5. Servlet单例 顾名思义就是让服务器启动的时候每个servlet对象只产生一次,之前的处理逻辑可以看到,每次都需要使用类加载器加载Class对象,然后创建一个新的servlet对象。 单例业务的处理逻辑要写在哪里?Request中还是Context中,其实都可以,但是个人认为写在Context中更好一些,因为context中负责遍历所有应用并存储还有类加载器在context内部,为后续扩展新的功能也更方便些。 ```java //单例Servlet public GenericServlet getServlet(String servletClass) throws ClassNotFoundException, InstantiationException, IllegalAccessException, ServletException { GenericServlet genericServlet = this.servletPool.get(servletClass); if(genericServlet == null){ Class aClass = this.servletClassLoader.loadClass(servletClass); //由原来的Servlet改成GenericServlet,因为只有GenericServlet才有这个getServletContext()方法 GenericServlet servlet = (GenericServlet) aClass.newInstance(); //想想当前这个地方是key设置为servletClass还是设置servletPath比较好?servletClass较好一些,因为多个url-Pattern //(servletPath)对应一个servletClass,所以存储一个servletClass就可以拿出单例Servlet... this.servletPool.put(servletClass, servlet); return servlet; } return genericServlet; } ``` 响应处理逻辑 ```java //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); if(!StringUtils.isEmpty(servletClass)){ GenericServlet servlet = context.getServlet(servletClass); System.out.println(servlet);//可用来判断是否为同一servlet ApplicationConfig servletConfig = context.getServletConfig(); servlet.init(servletConfig); servlet.service(request, response); return; } ``` ## 6. Life Cycle servlet的生命周期init,service,destory方法。 目前我们已经实现了service方法。init方法其实也已经实现了但是逻辑不对,init方法只有在第一次访问该servlet时才使用,后续在访问的时候就不会执行init方法了。所以我们只需要把响应处理逻辑的init方法调用放在request.getServlet()即可: ```java //单例Servlet public GenericServlet getServlet(String servletClass) throws ClassNotFoundException, InstantiationException, IllegalAccessException, ServletException { GenericServlet genericServlet = this.servletPool.get(servletClass); if(genericServlet == null){ Class aClass = this.servletClassLoader.loadClass(servletClass); //由原来的Servlet改成GenericServlet,因为只有GenericServlet才有这个getServletContext()方法 GenericServlet servlet = (GenericServlet) aClass.newInstance(); //实现了init()方法,并且只有在被第一次访问的时候才使用... servlet.init(this.servletConfig); //想想当前这个地方是key设置为servletClass还是设置servletPath比较好?servletClass较好一些,因为多个url-Pattern //(servletPath)对应一个servletClass,所以存储一个servletClass就可以拿出单例Servlet... this.servletPool.put(servletClass, servlet); return servlet; } return genericServlet; } ``` 响应处理逻辑 ```java //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); if(!StringUtils.isEmpty(servletClass)){ GenericServlet servlet = context.getServlet(servletClass); servlet.service(request, response); return; } ``` ## 7. 自启动 servlet的随着应用的加载而启动、开机自启动。需要在servlet应用中web.xml中load-on-startup参数:设置一个非负数,可以让servlet随着应用的加载而执行servlet的init方法而不是在第一次访问servlet之前才执行,让init的时机提前了一些。 1.需要去扫描load-on-startup节点,将这些servlet-class加入到一个集合中 2.直接去实例化该servlet,调用init ```java private List loadOnStartupList; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; this.servletClassUrlPattern = new HashMap<>(); this.urlPatternServletClass = new HashMap<>(); this.servletNameUrlPattern = new HashMap<>(); this.servletNameServletClass = new HashMap<>(); //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists()){ deployServlet(servletFile); deployClassLoader(); //只有servlet应用才需要设置context域,同时这地方也体现了一个应用下有且只有这一个servletContext.. this.servletContext = new ApplicationContext(this); //设置servletConfig this.servletConfig = new ApplicationConfig(this); //为了设置单例Servlet this.servletPool = new HashMap<>(); //部署自启动需要使用到单例,init方法,所以写在这些功能之后 deploySelfStarting(servletFile); } } private void deploySelfStarting(File servletFile) { this.loadOnStartupList = new ArrayList<>(); try { Document document = Jsoup.parse(servletFile, "utf-8"); Elements elements = document.select("servlet load-on-startup"); for (Element element : elements) { String text = element.text(); if(Integer.parseInt(text) > 0){ Element servletElement = element.parent(); Elements servletClassElement = servletElement.select("servlet-class"); String servletClass = servletClassElement.text(); this.loadOnStartupList.add(servletClass); } } for (String loadOnStartup : loadOnStartupList) { getServlet(loadOnStartup); } } catch (Exception e) { e.printStackTrace(); } } ``` ## 8. Cookie 这部分可先回想一下,什么时会话技术?什么是无状态协议? response主要实现response.add(Cookie cookie)功能 ```java @Override public void addCookie(Cookie cookie){ this.responseHeader.put("Set-cookie", cookie.getName() + "=" + cookie.getValue()); } ``` request主要实现Cookie[] request.getCookies()功能 ```java public void parseCookie(){ List cookieList = new ArrayList<>(); String cookie = this.header.get("Cookie"); if(cookie != null){ String[] parts = cookie.split(";"); for (String part : parts) { int index = part.indexOf("="); Cookie cookiePart = new Cookie(part.substring(0, index).trim(), part.substring(index+1).trim()); cookieList.add(cookiePart); } this.cookies = new Cookie[cookieList.size()]; for (int i = 0; i < cookies.length; i++) { cookies[i] = cookieList.get(i); } } } ``` ## 9. Session(重点) 当游览器第一次访问服务器Servlet的时候,此时,游览器给请求报文封装Cookie时,并没有JSESSIONID。当前Servlet的request.getSession(),会解析请求报文中是否有JSESSIONID,如果有的话就会解析JESSIONID值,并根据这个值,找到当前游览器对应绑定的session。如果没有那么会在response对象的响应头封装cookie("JSESSIONID","@123456789"),游览器进行解析得到JSESSIONID。执行流程如下,只需要复现当前流程即可: ![image-20221027155150143](README.assets/image-20221027155150143.png) 同时,可以从定义中看出,服务器会为每一个游览器(客户端)设置一个session,那么也就意味着session不是依赖于任何应用(context),并且根据ee规范可知获取session需要通过request.getSession()方法获取。 我们也模仿tomcat在web.xml文件中配置session的存活周期 ```xml 30 ``` 解析代码在WebXmlUtils中... 设置session生命周期,配置session对象代码 ```java package com.yun.server.utils; import com.yun.server.http.ApplicationSession; import com.yun.server.http.Request; import com.yun.server.http.Response; import javax.servlet.http.Cookie; import java.util.*; public class SessionUtils { //String代表JSESSIONID的值 public static final Map sessionPool = new HashMap<>(); private static Integer defaultTimeOut = WebXmlUtils.getSessionTimeout(); static { validateSession(); } //设置了Session的生命周期 private static void validateSession() { new Thread(new Runnable() { @Override public void run() { while (true){ Set jsessionids = sessionPool.keySet(); List expiredSessionIds = new ArrayList<>(); for (String jsessionid : jsessionids) { ApplicationSession session = sessionPool.get(jsessionid); //默认是30s(Tomcat设置的是30分钟),如果想修改时间则可以通过配置文件直接来修改,我们要验证所以设置为秒... long interval = System.currentTimeMillis() - session.getLastAccessedTime(); if(interval > session.getDefaultTimeOut() * 1000){ expiredSessionIds.add(jsessionid); } } //session是会话技术中服务器端技术,直接在sessionPool中删除该id相当于删除携带该id游览器对应的session //因为下次服务器再次携带该sessionid时,服务器发现没有该id则会新建一个session.. //同时注意会话技术中客户端技术,想让cookie失效则要通过该方法cookie.setMaxAge()来告诉游览器 for (String expiredSessionId : expiredSessionIds) { sessionPool.remove(expiredSessionId); } //每隔20s运行一次 try { Thread.sleep(20 * 1000); System.out.println("----session life cycle----"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public static void configSession(Request request, Response response) { ApplicationSession session = null; //请求头里边的Cookie值直接拿出来 String cookieValues = request.getHeader("Cookie"); if(cookieValues.contains("JSESSIONID")){ Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if(cookie.getName().equals("JSESSIONID")){ session = sessionPool.get(cookie.getValue()); if(session != null){ //每次进来的应用可能会不同,所以要设置当前应用的servletContext session.setServletContext(request.getContext().getServletContext()); }else { session = createSession(request, response); break; } } } }else { session = createSession(request, response); } //每一次访问 session.setLastAccessedTime(System.currentTimeMillis()); //request需要包装session对象 request.setSession(session); } private static ApplicationSession createSession(Request request, Response response) { //通过MD5的方式进行加密,此代码了解即可.... String id = generateJSESSIONID(); Cookie jsessionid = new Cookie("JSESSIONID", id); response.addCookie(jsessionid); ApplicationSession session = new ApplicationSession(jsessionid); session.setDefaultTimeOut(defaultTimeOut); session.setServletContext(request.getContext().getServletContext()); sessionPool.put(id, session); return session; } private static String generateJSESSIONID() { return SecurityUtils.toMD5(UUID.randomUUID().toString()); } } ``` ## 10. Default Servlet、Dynamic Servlet 同时当我们引入了Servlet后,响应处理逻辑有一点变化,就是都要交给Servlet来处理。 ![image-20221023214729580](README.assets/image-20221023214729580.png) DefaultServlet ```java package com.yun.server.servlet; import com.yun.server.catalina.Context; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.FileUtils; import com.yun.server.utils.WebXmlUtils; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.File; import java.io.IOException; //JSP文件(欢迎界面)、静态资源文件都交给缺省servlet来处理 public class DefaultServlet extends GenericServlet { public static final DefaultServlet INSTANCE = new DefaultServlet(); private DefaultServlet(){} @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { Request request = (Request) servletRequest; Response response = (Response) servletResponse; File file = null; Context context = request.getContext(); String servletPath = request.getServletPath(); if("/".equals(servletPath)){ file = WebXmlUtils.getWelcomeFile(context); }else { //请求资源是否为二进制文件 if(servletPath.contains(".")){ String mimeType = WebXmlUtils.getMimeType(servletPath); if(!mimeType.equals("text/html")){ response.setContentType(mimeType); } } file = new File(context.getDocBase() + servletPath); } if(file.exists() && !file.isDirectory()){ byte[] bytes = FileUtils.getBytes(file); response.setResponseBody(bytes); return; } response.setStatus(404); response.getWriter().println("400
"); response.getWriter().println("File Not Found..."); } } ``` Dynamic Servlet ```java package com.yun.server.servlet; import com.yun.server.catalina.Context; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.utils.StringUtils; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.IOException; public class DynamicServlet extends GenericServlet { public static final DynamicServlet INSTANCE = new DynamicServlet(); private DynamicServlet(){} @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { Request request = (Request) servletRequest; Response response = (Response) servletResponse; String servletPath = request.getServletPath(); Context context = request.getContext(); //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); GenericServlet servlet = context.getServlet(servletClass); servlet.service(request, response); return; } } ``` 请求到来时响应处理逻辑 ```java package com.yun.server.catalina; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.servlet.DefaultServlet; import com.yun.server.servlet.DynamicServlet; import com.yun.server.utils.SessionUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { System.out.println("Starting ProtocolHandler [HTTP/1.1-bio-" + this.port + "]"); serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ //解析网络路径得到当前应用... Context context = request.getContext(); if(context != null){ String servletPath = request.getServletPath(); //welcome interface、binary file if("/".equals(servletPath)){ DefaultServlet.INSTANCE.service(request, response); }else { //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); if(servletClass != null){ //只有是servlet应用时,才需要设置session,并且需要用到request,response对象来生产session SessionUtils.configSession(request, response); DynamicServlet.INSTANCE.service(request, response); }else { //处理二进制文件 DefaultServlet.INSTANCE.service(request, response); } } } /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` ## Utils ### ContextUtils ```java package com.yun.server.utils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; import java.io.IOException; public class ContextXmlUtils { public static String getWatchedResource() { String watchedResource = null; try { Document document = Jsoup.parse(Constant.ContextXml, "utf-8"); Elements element = document.select("Context WatchedResource"); watchedResource = element.text(); } catch (IOException e) { e.printStackTrace(); } return watchedResource; } } ``` ### WebXmlUtils ```java package com.yun.server.utils; import com.yun.server.catalina.Context; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.util.*; public class WebXmlUtils { private static List welcomeFileList = new ArrayList<>(); private static Map mimeMapping = new HashMap<>(); private static int sessionTimeout = 30; static { parseWelcomeFile(); parseMimeMapping(); parseSessionConfig(); } private static void parseSessionConfig() { Document document = null; try { document = Jsoup.parse(Constant.WebXml, "utf-8"); } catch (IOException e) { e.printStackTrace(); } Elements elements = document.select("session-config session-timeout"); if(elements.isEmpty()){ return; } sessionTimeout = Integer.parseInt(elements.first().text()); } private static void parseMimeMapping() { try { Document document = Jsoup.parse(Constant.WebXml, "utf-8"); Elements elements = document.select("mime-mapping"); for (Element element : elements) { Elements extensionElement = element.select("extension"); Elements mimeTypeElement = element.select("mime-type"); String extension = extensionElement.text(); String mimeType = mimeTypeElement.text(); mimeMapping.put(extension, mimeType); } } catch (IOException e) { e.printStackTrace(); } } private static void parseWelcomeFile() { try { Document document = Jsoup.parse(Constant.WebXml, "utf-8"); Elements elements = document.select("welcome-file-list welcome-file"); for (Element element : elements) { String welcomeFile = element.text(); welcomeFileList.add(welcomeFile); } } catch (IOException e) { e.printStackTrace(); } } public static File getWelcomeFile(Context context){ String docBase = context.getDocBase(); for (String welcomeFile : welcomeFileList) { File file = new File(docBase + "/" + welcomeFile); if(file.exists() && !file.isDirectory()){ return file; } } return new File(docBase + "/" + "index.html"); } public static String getMimeType(String servletPath) { Set extensionSet = mimeMapping.keySet(); for (String extension : extensionSet) { if(servletPath.endsWith(extension)){ return mimeMapping.get(extension); } } return "text/html"; } public static int getSessionTimeout(){ return sessionTimeout; } } ``` ### SecurityUtils ```java package com.yun.server.utils; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class SecurityUtils { private static final String[] hexDigits = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}; private static final String salt = "www.yun.com"; public static String toMD5(String message){ message = message + salt; MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); md5.update(message.getBytes("utf-8")); byte[] digest = md5.digest(); return byteArrayToHexString(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return null; } private static String byteArrayToHexString(byte[] digest) { StringBuffer buffer = new StringBuffer(); for (int i = 0; i < digest.length; i++) { buffer.append(byteToHexString(digest[i])); } return buffer.toString(); } private static String byteToHexString(byte b) { int n = b; if(n < 0){ n += 256; } int d1 = n / 16; int d2 = n % 16; return hexDigits[d1] + hexDigits[d2]; } } ``` # Phase3-Advanced Feature 本项目将实现一个功能健全的Web服务器,一个遵循JavaEE规范的服务器,可以处理静态web资源,同时也可以处理动态web资源,比如servlet等。项目一共可以分为三个阶段,阶段一,使用SE阶段学习的基础知识实现一个静态web资源服务器,可以处理静态web资源,包含文本文件以及二进制文件等;阶段二,在阶段一的基础上加上处理动态web资源的功能,实现Servlet规范,本阶段也是本项目最为核心 的一块内容;阶段三,阶段二已经是一个功能比较完善的服务器,但是还有一些高级特性没有实现,在最后这一阶段,我们要实现Filter、Listener等高级特性。 ## 1. Filter责任链(重点) filter过滤器就是当匹配相应servlet时,会执行所有filter之后再执行最后的servlet,将所有filter对象和servlet对象串起来像链一样一步一步执行,同时也是一种设计模式,叫**责任链设计模式**。 注意filter与servlet区别,filter可以让多个filter对应一个url-pattern;同时filter不像servlet那样在第一次访问时进行实例化,而是直接进行实例化,将所有filter实例化对象封装进filterPool中。此部分实现也是在应用中: ```java /*---------------------filter-------------------------*/ private Map filterNameFilerClass; private Map filterNameUrlPattern; private Map> urlPatternFilterClasses; //filter-class和实例化对象 private Map filterPool; public Context(String path, String docBase) { this.path = path; this.docBase = docBase; this.servletClassUrlPattern = new HashMap<>(); this.urlPatternServletClass = new HashMap<>(); this.servletNameUrlPattern = new HashMap<>(); this.servletNameServletClass = new HashMap<>(); this.filterNameFilerClass = new HashMap<>(); this.filterNameUrlPattern = new HashMap<>(); this.urlPatternFilterClasses = new HashMap<>(); deploy(); } //部署servlet、filter、listener private void deploy() { //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists() && !servletFile.isDirectory()){ /*-------servlet------*/ deployServlet(servletFile); deployClassLoader(); //只有servlet应用才需要设置context域,同时这地方也体现了一个应用下有且只有这一个servletContext.. this.servletContext = new ApplicationContext(this); //设置servletConfig this.servletConfig = new ApplicationConfig(this); //为了设置单例Servlet this.servletPool = new HashMap<>(); //部署自启动需要使用到单例,init方法,所以写道这些功能之后 deploySelfStarting(servletFile); /*-------filter------*/ this.filterPool = new HashMap<>(); deployFilter(servletFile); } } private void deployFilter(File servletFile) { //封装filter、filter-mapping标签 parseFilter1(servletFile); //封装url-pattern和filter-class键值对 parseFilter2(); //将每一个filter-class进行实例化,并封装进filterPool中 instantiationFilter(); } private void instantiationFilter() { Collection> filterClassesList = this.urlPatternFilterClasses.values(); for (List filterClasses : filterClassesList) { for (String filterClass : filterClasses) { Filter filter = this.filterPool.get(filterClass); if(filter == null){ try { Class aClass = this.servletClassLoader.loadClass(filterClass); Filter o = (Filter) aClass.newInstance(); o.init(null); this.filterPool.put(filterClass, o); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ServletException e) { e.printStackTrace(); } } } } } private void parseFilter2() { Set filterNameKeys = this.filterNameUrlPattern.keySet(); Set filterMappingNameKeys = this.filterNameFilerClass.keySet(); for (String filterNameKey : filterNameKeys) { for (String filterMappingNameKey : filterMappingNameKeys) { if(filterNameKey.equals(filterMappingNameKey)){ String servletClass = this.filterNameFilerClass.get(filterNameKey); String urlPattern = this.filterNameUrlPattern.get(filterNameKey); List filterClassList = this.urlPatternFilterClasses.get(urlPattern); if(filterClassList == null){ filterClassList = new ArrayList<>(); this.urlPatternFilterClasses.put(urlPattern, filterClassList); } filterClassList.add(servletClass); } } } } private void parseFilter1(File servletFile) { try { Document document = Jsoup.parse(servletFile, "utf-8"); Elements filterElements = document.select("filter"); for (Element filterElement : filterElements) { Elements filterNameElement = filterElement.select("filter-name"); Elements filerClassElement = filterElement.select("filter-class"); String servletName = filterNameElement.text(); String servletClass = filerClassElement.text(); this.filterNameFilerClass.put(servletName, servletClass); } Elements filterMappingElements = document.select("filter-mapping"); for (Element filterMappingElement : filterMappingElements) { Elements filterNameElement = filterMappingElement.select("filter-name"); Elements urlPatternElement = filterMappingElement.select("url-pattern"); String servletName = filterNameElement.text(); String urlPattern = urlPatternElement.text(); this.filterNameUrlPattern.put(servletName, urlPattern); } } catch (IOException e) { e.printStackTrace(); } } ``` 让所有应用都取出filter,虽然逻辑上不太合理,但是后边会进行判断,根据filter的数据来判断执不执行filter ```java //context中实现 public List getFilters(String servletPath) { List filterClasses = this.urlPatternFilterClasses.get(servletPath); List filterList = new ArrayList<>(); if(filterClasses != null){ for (String filterClass : filterClasses) { Filter filter = this.filterPool.get(filterClass); filterList.add(filter); } return filterList; } return filterList; } ``` 创建一个ApplicationFilter类来处理,根据filterList.size()判断是否处理filter,当所有filter执行完毕后最后执行servlet(责任链设置模式) ```java package com.yun.server.http; import javax.servlet.*; import java.io.IOException; import java.util.List; public class ApplicationFilter implements FilterChain { private List filters; private Servlet servlet; private int flag = 0; public ApplicationFilter(List filters, Servlet servlet) { this.filters = filters; this.servlet = servlet; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException { if(flag < this.filters.size()){ Filter filter = filters.get(flag++); filter.doFilter(servletRequest, servletResponse, this); }else { //直到所有filter都执行完了,才能执行这个servlet servlet.service(servletRequest, servletResponse); } } } ``` 访问相应逻辑 ```java package com.yun.server.catalina; import com.yun.server.http.ApplicationFilter; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.servlet.DefaultServlet; import com.yun.server.servlet.DynamicServlet; import com.yun.server.utils.SessionUtils; import javax.servlet.Filter; import javax.servlet.Servlet; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.List; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { System.out.println("Starting ProtocolHandler [HTTP/1.1-bio-" + this.port + "]"); serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ //解析网络路径得到当前应用... Context context = request.getContext(); if(context != null){ String servletPath = request.getServletPath(); List filters = context.getFilters(servletPath); Servlet servlet = null; //welcome interface、binary file if("/".equals(servletPath)){ servlet = DefaultServlet.INSTANCE; }else { //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); if(servletClass != null){ //只有是servlet应用时,才需要设置session,并且需要用到request,response对象来生产session SessionUtils.configSession(request, response); servlet = DynamicServlet.INSTANCE; }else { //处理二进制文件、静态资源文件 servlet = DefaultServlet.INSTANCE; } } //我们将所有访问处理交给了servlet对象,方便责任链处理 ApplicationFilter applicationFilter = new ApplicationFilter(filters, servlet); applicationFilter.doFilter(request, response); } /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } ``` ## 2. Listener 监听器监听谁?监听servletContext的创建和销毁(只实现了创建) ```java //部署servlet、filter、listener private void deploy() { //创建每一个应用时,都查看当前应用是否有WEB-INF/web.xml文件,如果有的话那么当前应用需要对应的servlet处理 File servletFile = new File(this.docBase, ContextXmlUtils.getWatchedResource()); if(servletFile.exists() && !servletFile.isDirectory()){ /*-------servlet------*/ deployServlet(servletFile); deployClassLoader(); //只有servlet应用才需要设置context域,同时这地方也体现了一个应用下有且只有这一个servletContext.. this.servletContext = new ApplicationContext(this); //设置servletConfig this.servletConfig = new ApplicationConfig(this); //为了设置单例Servlet this.servletPool = new HashMap<>(); //部署自启动需要使用到单例,init方法,所以写道这些功能之后 deploySelfStarting(servletFile); /*-------filter------*/ this.filterPool = new HashMap<>(); deployFilter(servletFile); /*-------listener------*/ listenerPool = new ArrayList<>(); deployListener(servletFile); } } private void deployListener(File servletFile) { parseListener(servletFile); instantiationListener(); } private void instantiationListener() { if(this.listenerClasses.size() > 0){ for (String listenerClass : listenerClasses) { try { Class aClass = this.servletClassLoader.loadClass(listenerClass); ServletContextListener listener = (ServletContextListener) aClass.newInstance(); ServletContextEvent servletContextEvent = new ServletContextEvent(this.servletContext); listener.contextInitialized(servletContextEvent); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } private void parseListener(File servletFile) { try { Document document = Jsoup.parse(servletFile, "utf-8"); Elements listenerClassElements = document.select("listener listener-class"); for (Element listenerClassElement : listenerClassElements) { String listenerClass = listenerClassElement.text(); this.listenerClasses.add(listenerClass); } } catch (IOException e) { e.printStackTrace(); } } ``` ## 3. ThreadPool 没有引入线程池之前的工作方式,每当一个任务到来,都会创建一个新的线程来处理该任务,创建线程多了之后,对于操作系统来说是一个很大的开销,且创建完的线程使用之后就不再使用了,非常的浪费,类似下图所示 image-20221101234658491 我们考虑到线程并不是每时每刻都在工作着,之前创建的线程如果执行完任务之后,完全可以回头继续处理后续的线程,没必要重新创建一个新的线程。 ![image-20221101234805213](README.assets/image-20221101234805213.png) 新建一个任务列表,在列表内添加任务,然后线程池中初始化一定数目的线程,线程的很饥饿,会互相去争夺任务列表里面的任务来执行,争夺到一个任务执行,那么其他线程等待其他的任务到来,刚刚获取到任务的线程如果执行完了之后,也会重新加入到该场争夺战中。 ```java package com.yun.server.utils; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadUtils { // 参数的含义: //1.初始化时候线程池内有20个线程 //2.最大可扩充到100个线程 //3和4参数表示新增出来的线程如果在60s内没有被执行,就会被回收,最后保留20 //5.当大量的任务过来时,20根线程都被占用了,但是并不会立刻去创建新的线程,而是将任务放入queue队列中 //线程处理完之后会再次回来继续处理这里面的请求,只有当处理不过来的请求数目超过capacity10个时,才扩展线程 private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 100, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10)); public static void add(Runnable runnable){ threadPoolExecutor.execute(runnable); } } ``` ```java package com.yun.server.catalina; import com.yun.server.http.ApplicationFilter; import com.yun.server.http.Request; import com.yun.server.http.Response; import com.yun.server.servlet.DefaultServlet; import com.yun.server.servlet.DynamicServlet; import com.yun.server.utils.SessionUtils; import com.yun.server.utils.ThreadUtils; import javax.servlet.Filter; import javax.servlet.Servlet; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.List; public class Connector implements Runnable{ private int port; //当前Connector想要找到Engine,不可以让Connector包含,因为他们是平级关系 //但是可以通过Service找到Engine,Service包含connector和Engine private Service service; public void setPort(int port) { this.port = port; } public void setService(Service service) { this.service = service; } @Override public void run() { ServerSocket serverSocket = null; try { System.out.println("Starting ProtocolHandler [HTTP/1.1-bio-" + this.port + "]"); serverSocket = new ServerSocket(this.port); while (true){ Socket server = serverSocket.accept(); Runnable runnable = new Runnable() { @Override public void run() { InputStream requestInputStream = null; OutputStream responseOutputStream = null; Response response = null; try { requestInputStream = server.getInputStream(); responseOutputStream = server.getOutputStream(); Engine engine = service.getEngine(); Request request = new Request(requestInputStream, engine); response = new Response(responseOutputStream); /*---------------------------------------------*/ //解析网络路径得到当前应用... Context context = request.getContext(); if (context != null) { String servletPath = request.getServletPath(); List filters = context.getFilters(servletPath); Servlet servlet = null; //welcome interface、binary file if ("/".equals(servletPath)) { servlet = DefaultServlet.INSTANCE; } else { //是否是servlet资源 String servletClass = context.getUrlPatternServletClass(servletPath); if (servletClass != null) { //只有是servlet应用时,才需要设置session,并且需要用到request,response对象来生产session SessionUtils.configSession(request, response); servlet = DynamicServlet.INSTANCE; } else { //处理二进制文件、静态资源文件 servlet = DefaultServlet.INSTANCE; } } //我们将所有访问处理交给了servlet对象,方便责任链处理 ApplicationFilter applicationFilter = new ApplicationFilter(filters, servlet); applicationFilter.doFilter(request, response); } /*---------------------------------------------*/ } catch (Exception e) { e.printStackTrace(); response.setStatus(500); response.getWriter().println("500"); response.getWriter().println(e); } finally { response.respond(); } } }; ThreadUtils.add(runnable); } } catch (IOException e) { e.printStackTrace(); } } } ```