探秘Tomcat——一个简易的Servlet容器
即便再简陋的服务器也是服务器,今天就来循着书本的第二章来看看如何实现一个servlet容器。
背景知识
既然说到servlet容器这个名词,我们首先要了解它到底是什么。
servlet
相比你或多或少有所了解。servlet是用java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。
容器
容器的概念很大,在这里可以理解为能够管理对象(servlet)的生命周期,对象与对象之间的依赖关系。
基于对以上两个概念的解释,那么对于serelvet容器的概念也就不再那么陌生了。
servlet容器
就是创建、管理servlet规范中相关对象、生命周期的应用程序。
Servlet接口
servlet是一种编程规范,要实现servlet编程需要用到javax.servlet和javax.servlet.http。所有的servlet程序都需要实现或继承自实现了javax.servlet.servlet接口。
Servlet接口的方法
- init():servlet容器的初始化方法,该方法只会被调用一次;
- service():不同于init只会触发一次,service在客户端请求后就会被调用。同时需要传入参数servletRequest和servletResponse。从字面意思就能知道,servletRequest携带了客户端发送的HTTP请求的信息,而servletResponse则用于封装servlet的响应信息。
- destroy():当servlet实例调用完毕要被移除时,destroy方法将被调用。
- getServletConfig():该方法用于取得<servlet> <init-param>配置的参数
- getServletInfo():该方法提供有关servlet的信息,如作者、版本、版权。
servlet容器的职责
- 第一次调用servlet时,需要载入serlvet类并调用init方法;
- 针对客户端的request请求,创建一个servletRequest对象和一个servletResponse对象;
- 传参servletRequest和servletResponse,调用service方法;
- 当关闭servlet类时,调用destroy方法。
简陋的servlet容器
之所以说是简陋的servlet容器,因为这里并没有实现servlet所有的方法,该容器只能支持很简单的servlet,也没有init方法和destroy方法。主要实现功能如下:
- 等待HTTP请求;
- 创建serlvetRequest和servletResponse对象;
- 能够分别处理静态资源和servlet,当客户端请求静态资源时,则调用StaticResourceProcessor对象的process方法;当请求为serlvet则载入请求的servlet类并调用service方法。
主要包括6个类
- HttpServer1:程序的入口,负责创建Request和Response对象,并根据HTTP请求类型将其转给相应的处理器处理;
- Request:用于封装客户端HTTP请求信息;
- Response:用于封装服务器响应信息;
- StaticResourceProcessor:静态资源处理器;
- ServletProcessor1:servlet处理器;
- Constants:用于定义一些常量,如WEB_ROOT
HttpServer1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | package day0522; import java.net.Socket; import java.net.ServerSocket; import java.net.InetAddress; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; public class HttpServer1 { /** WEB_ROOT is the directory where our HTML and other files reside. * For this package, WEB_ROOT is the "webroot" directory under the working * directory. * The working directory is the location in the file system * from where the java command was invoked. */ // shutdown command private static final String SHUTDOWN_COMMAND = "/SHUTDOWN" ; // the shutdown command received private boolean shutdown = false ; public static void main(String[] args) { HttpServer1 server = new HttpServer1(); server.await(); } public void await() { ServerSocket serverSocket = null ; int port = 8080 ; try { serverSocket = new ServerSocket(port, 1 , InetAddress.getByName( "127.0.0.1" )); } catch (IOException e) { e.printStackTrace(); System.exit( 1 ); } // Loop waiting for a request while (!shutdown) { Socket socket = null ; InputStream input = null ; OutputStream output = null ; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // create Request object and parse Request request = new Request(input); request.parse(); // create Response object Response response = new Response(output); response.setRequest(request); // check if this is a request for a servlet or a static resource // a request for a servlet begins with "/servlet/" if (request.getUri().startsWith( "/servlet/" )) { ServletProcessor1 processor = new ServletProcessor1(); processor.process(request, response); } else { StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); } // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); System.exit( 1 ); } } } } |
从代码可以看出,该类主要内容与上篇的HttpServer类似,不同点有:
- await会一直等待HTTP请求,如果等到请求,该方法会根据请求类型分发给对应的处理器来处理;
- 支持静态资源的请求,可以通过类似http://localhost:8080/index.html这样的请求来访问
- index.html页面;
- 支持servlet的请求和解析,可以通过类似http://localhost:8080/PrimitiveServlet来访问PrimitiveServlet
Request与上篇介绍的Request无异,不再介绍,但是需要说明一点,这里的Request实现了ServletRequest接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | package day0522; import java.io.InputStream; import java.io.IOException; import java.io.BufferedReader; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Locale; import java.util.Map; import javax.servlet.RequestDispatcher; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; public class Request implements ServletRequest { private InputStream input; private String uri; public Request(InputStream input) { this .input = input; } public String getUri() { return uri; } private String parseUri(String requestString) { int index1, index2; index1 = requestString.indexOf( ' ' ); if (index1 != - 1 ) { index2 = requestString.indexOf( ' ' , index1 + 1 ); if (index2 > index1) return requestString.substring(index1 + 1 , index2); } return null ; } public void parse() { // Read a set of characters from the socket StringBuffer request = new StringBuffer( 2048 ); int i; byte [] buffer = new byte [ 2048 ]; try { i = input.read(buffer); } catch (IOException e) { e.printStackTrace(); i = - 1 ; } for ( int j= 0 ; j<i; j++) { request.append(( char ) buffer[j]); } System.out.print(request.toString()); uri = parseUri(request.toString()); } /* implementation of the ServletRequest*/ public Object getAttribute(String attribute) { return null ; } public Enumeration getAttributeNames() { return null ; } public String getRealPath(String path) { return null ; } public RequestDispatcher getRequestDispatcher(String path) { return null ; } public boolean isSecure() { return false ; } public String getCharacterEncoding() { return null ; } public int getContentLength() { return 0 ; } public String getContentType() { return null ; } public ServletInputStream getInputStream() throws IOException { return null ; } public Locale getLocale() { return null ; } public Enumeration getLocales() { return null ; } public String getParameter(String name) { return null ; } public Map getParameterMap() { return null ; } public Enumeration getParameterNames() { return null ; } public String[] getParameterValues(String parameter) { return null ; } public String getProtocol() { return null ; } public BufferedReader getReader() throws IOException { return null ; } public String getRemoteAddr() { return null ; } public String getRemoteHost() { return null ; } public String getScheme() { return null ; } public String getServerName() { return null ; } public int getServerPort() { return 0 ; } public void removeAttribute(String attribute) { } public void setAttribute(String key, Object value) { } public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { } @Override public int getRemotePort() { // TODO Auto-generated method stub return 0 ; } @Override public String getLocalName() { // TODO Auto-generated method stub return null ; } @Override public String getLocalAddr() { // TODO Auto-generated method stub return null ; } @Override public int getLocalPort() { // TODO Auto-generated method stub return 0 ; } } |
Response
同理,这里的Response也不在赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | package day0522; import java.io.OutputStream; import java.io.IOException; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.File; import java.io.PrintWriter; import java.util.Locale; import javax.servlet.ServletResponse; import javax.servlet.ServletOutputStream; public class Response implements ServletResponse { private static final int BUFFER_SIZE = 1024 ; Request request; OutputStream output; PrintWriter writer; public Response(OutputStream output) { this .output = output; } public void setRequest(Request request) { this .request = request; } /* This method is used to serve a static page */ public void sendStaticResource() throws IOException { byte [] bytes = new byte [BUFFER_SIZE]; FileInputStream fis = null ; try { /* request.getUri has been replaced by request.getRequestURI */ File file = new File(Constants.WEB_ROOT, request.getUri()); fis = new FileInputStream(file); /* HTTP Response = Status-Line *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF */ int ch = fis.read(bytes, 0 , BUFFER_SIZE); while (ch!=- 1 ) { output.write(bytes, 0 , ch); ch = fis.read(bytes, 0 , BUFFER_SIZE); } } catch (FileNotFoundException e) { String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>" ; output.write(errorMessage.getBytes()); } finally { if (fis!= null ) fis.close(); } } /** implementation of ServletResponse */ public void flushBuffer() throws IOException { } public int getBufferSize() { return 0 ; } public String getCharacterEncoding() { return null ; } public Locale getLocale() { return null ; } public ServletOutputStream getOutputStream() throws IOException { return null ; } public PrintWriter getWriter() throws IOException { // autoflush is true, println() will flush, // but print() will not. writer = new PrintWriter(output, true ); return writer; } public boolean isCommitted() { return false ; } public void reset() { } public void resetBuffer() { } public void setBufferSize( int size) { } public void setContentLength( int length) { } public void setContentType(String type) { } public void setLocale(Locale locale) { } @Override public String getContentType() { // TODO Auto-generated method stub return null ; } @Override public void setCharacterEncoding(String charset) { // TODO Auto-generated method stub } } |
这里的getWriter方法中新建了PrintWriter,其中第二个参数是一个boolean类型,表示是否启动autoFlush。
StaticResourceProcessor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package day0522; import java.io.IOException; public class StaticResourceProcessor { public void process(Request request, Response response) { try { response.sendStaticResource(); } catch (IOException e) { e.printStackTrace(); } } } |
看代码可以看出:
该类相较上篇是新建的类,主要实现的方法有sendStaticResource,实际上这个方法在上篇中也有,只是直接放在Response中出现,并在HttpServer中声明调用,而这里是将两种请求类型分别封装成类。
ServletProcessor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | package day0522; import java.net.URL; import java.net.URLClassLoader; import java.net.URLStreamHandler; import java.io.File; import java.io.IOException; import javax.servlet.Servlet; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class ServletProcessor1 { public void process(Request request, Response response) { String uri = request.getUri(); String servletName = uri.substring(uri.lastIndexOf( "/" ) + 1 ); URLClassLoader loader = null ; try { // create a URLClassLoader URL[] urls = new URL[ 1 ]; URLStreamHandler streamHandler = null ; File classPath = new File(Constants.WEB_ROOT); // the forming of repository is taken from the createClassLoader method in // org.apache.catalina.startup.ClassLoaderFactory String repository = ( new URL( "file" , null , classPath.getCanonicalPath() + File.separator)).toString() ; // the code for forming the URL is taken from the addRepository method in // org.apache.catalina.loader.StandardClassLoader class. urls[ 0 ] = new URL( null , repository, streamHandler); loader = new URLClassLoader(urls); } catch (IOException e) { System.out.println(e.toString() ); } Class myClass = null ; try { myClass = loader.loadClass(servletName); } catch (ClassNotFoundException e) { System.out.println(e.toString()); } Servlet servlet = null ; try { servlet = (Servlet) myClass.newInstance(); servlet.service((ServletRequest) request, (ServletResponse) response); } catch (Exception e) { System.out.println(e.toString()); } catch (Throwable e) { System.out.println(e.toString()); } } } |
从代码看出:
- 该类只有一个方法process,接收Request和Response两个参数;
- 通过uri.substring来获取请求的servlet名;
- 通过新建一个类加载器来装载请求的servlet类,用的类加载器为java.net.URLClassLoader;
- 有了类加载器后,通过loadClass方法载入serlvet类;
- 创建一个载入类的实例,并调用其service方法。
至此,我们明白了:
- servlet容器会等待http请求;
- request负责封装http请求信息;
- response负责封装相应信息;
- staticResourceProcessor负责静态资源请求处理;
- servletProcessor负责servlet的请求处理;
- 一个简易的servlet容器的运作原理。
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
友情赞助
如果你觉得博主的文章对你那么一点小帮助,恰巧你又有想打赏博主的小冲动,那么事不宜迟,赶紧扫一扫,小额地赞助下,攒个奶粉钱,也是让博主有动力继续努力,写出更好的文章^^。
1. 支付宝 2. 微信
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步