通过上一篇文章《一步一步实现Tomcat——实现一个简单的Web服务器》,咱们实现了一个简单的Web服务器,能够响应浏览器请求显示静态Html页面,本文更进一步,实现一个Servlet容器,咱们不仅能响应静态页面请求,还能响应Servlet请求,虽然如今咱们只能在本身的Servlet中打印出“Hello World!”,可是咱们离Tomcat服务器更近了一步。html
相信你们应该对Java EE编程比较熟悉,故在此只简单的描述一下基本概念。java
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,也能够说是一组规范,只要按照规范实现本身的类,就能够在相应的Servlet服务器(Tomcat、Jetty等)中运行,响应浏览器请求,动态生成内容。git
注:本文使用Servlet 2.3规范程序员
是Servlet规范中的一个接口,咱们能够本身实现这个接口在service
方法中实现本身的业务逻辑。service
方法签名以下:github
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
表示一次请求的接口,由服务器生成相应的实现类并传送给上面的service(ServletRequest req, ServletResponse res)
使用,用户在实现本身的Servlet
类是可使用传入的ServletRequest
实现类中的各类方法,如请求地址,获取请求参数,获取cookie等。web
表示一次相应的接口,由服务器生成相应的实现类并传送给上面的service(ServletRequest req, ServletResponse res)
使用,用户在实现本身的Servlet
类是可使用传入的ServletRequest
实现类中的各类方法,如设置http相应头部,向浏览器打印数据,跳转页面等。apache
【图一】编程
如图所示,一个简单的Servlet容器处理流程很是简单,咱们只须要在上篇文章中代码基础上稍加改动,就能够实现咱们想要的功能。segmentfault
接收http请求工做咱们已经知道如何实现了,咱们先从后两项工做开始。api
上篇文章咱们也抽象了一个Request和Response类,可是这两类并无继承ServletRequest
和ServletResponse
接口,因此Servlet没法使用,因此咱们须要分别继承相应的接口。
原来Request
中的方法都没有变化,由于实现了ServletRequest
接口,因此必须实现接口中定义的方法,可是如今咱们还无需具体实现,大多都是返回null
或留白。
/** * 表示请求值 */ public class Request implements ServletRequest { private InputStream input; private String uri; // private String request; public Request(InputStream input) { this.input = input; } public void parse() { StringBuilder request = new StringBuilder(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]); } uri = parseUri(request.toString()); } 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 String getUri() { return uri; } @Override public Object getAttribute(String name) { return null; } @Override public Enumeration getAttributeNames() { return null; } @Override public String getCharacterEncoding() { return null; } //其余方法省略... }
同新Request
类同样,新Response
类也保留了原来的方法只是实现了ServletResponse
接口,除了getWriter()
方法由于稍后要用而实现了,其余ServletResponse
接口方法均返回null
或留白。
/** * 表示返回值 */ public class Response implements ServletResponse { private static final int BUFFER_SIZE = 1024; private Request request; private OutputStream output; public Response(OutputStream output) { this.output = output; } public void setRequest(Request request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; //读取访问地址请求的文件 File file = new File(Constants.WEB_ROOT, request.getUri()); try (FileInputStream fis = new FileInputStream(file)){ if (file.exists()) { //若是文件存在 //添加相应头。 StringBuilder heads=new StringBuilder("HTTP/1.1 200 OK\r\n"); heads.append("Content-Type: text/html\r\n"); //头部 StringBuilder body=new StringBuilder(); //读取响应主体 int len ; while ((len=fis.read(bytes, 0, BUFFER_SIZE)) != -1) { body.append(new String(bytes,0,len)); } //添加Content-Length heads.append(String.format("Content-Length: %d\n",body.toString().getBytes().length)); heads.append("\r\n"); output.write(heads.toString().getBytes()); output.write(body.toString().getBytes()); } else { response404(output); } }catch (FileNotFoundException e){ response404(output); } } private void response404(OutputStream output) throws IOException { StringBuilder response=new StringBuilder(); response.append("HTTP/1.1 404 File Not Found\r\n"); response.append("Content-Type: text/html\r\n"); response.append("Content-Length: 23\r\n"); response.append("\r\n"); response.append("<h1>File Not Found</h1>"); output.write(response.toString().getBytes()); } @Override public PrintWriter getWriter() throws IOException { return new PrintWriter(output,true); } @Override public String getCharacterEncoding() { return null; } //省略其余方法。 }
这里须要注意是new PrintWriter(output,true)
方法,阅读方法注释,摘录以下内容:
autoFlush – A boolean; if true, the println, printf, or format methods will flush the output buffer
也就是说调用print
方法不会输出到浏览器页面。原书中说这是一个问题须要解决。
我又阅读了Servlet API文档getWriter()
相关内容(传送门),摘录以下内容:
Returns a PrintWriter object that can send character text to the client. The PrintWriter uses the character encoding returned by getCharacterEncoding(). If the response's character encoding has not been specified as described in getCharacterEncoding (i.e., the method just returns the default value ISO-8859-1), getWriter updates it to ISO-8859-1.Calling flush() on the PrintWriter commits the response.
我理解此方法返回的PrintWriter
是须要调用flush()才会刷新,因此我对全部的打印方法println();printf();print()
等是否须要每次都自动刷新产生了疑惑,姑且先到这,看书中后面的处理可否能答疑解惑。
咱们只是实现一个简单的Servlet容器示例,因此ServletRequest
和ServletResponse
其余方法留待之后实现。
Servlet
上篇文章咱们直接读取静态Html文件,而后将内容直接返回给浏览器,其实处理Servlet
也差很少,只不过咱们面对的class文件,咱们须要利用ClassLoader将类加载进虚拟机,而后利用反射原理生成Servlet
类的对象,而后就能够调用相应service()
方法,运行编写Servlet
类程序员的代码了。
Servlet
的方法/** * Servlet的处理类 */ public class ServletProcessor { /** * Servlet处理方法。 * * @param request * @param response */ public void process(Request request, Response response) { //解析Servlet类名 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); //类加载器加载路径 String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ; urls[0] = new URL(null, repository, streamHandler); loader = new URLClassLoader(urls); } catch (IOException e) { throw new IllegalStateException(e); } Class clazz = null; try { //加载Servlet类 clazz = loader.loadClass(servletName); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } try { //初始化Servlet类 Servlet servlet = (Servlet) clazz.newInstance(); //写入响应头部,不然浏览器没法解析。 PrintWriter writer=response.getWriter(); writer.print("HTTP/1.1 200 OK\r\n"); writer.print("Content-Type: text/html\r\n"); writer.print("\r\n"); //print方法不会自动刷新。 writer.flush(); //调用Servlet类中service方法。 servlet.service(request,response); } catch (Exception e) { e.printStackTrace(); } } }
注意这这三行代码,书中原始代码没有相应逻辑。
writer.print("HTTP/1.1 200 OK\r\n"); writer.print("Content-Type: text/html\r\n"); writer.print("\r\n");
和上篇文章同样,也须要加响应头部,不然浏览器没法解析,不过这个添加头部的方法十分不简陋,之后咱们会优雅的实现。
注意这行代码:servlet.service(request,response);
咱们将Request
类和Response
类直接传入了service
方法,若是熟悉这个容器的程序员就能够在本身的Servlet
使用这两个内部类和他的方法。
public class HelloWorldServlet implements Servlet { @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { Request request=(Request)req; Response response=(Response)res; request.parse(); } }
parse()
方法并非ServletRequest
接口方法,咱们不想暴露给程序员,但也不能parse()
改为private
由于容器中其余类也须要使用。
Tomcat用了一个很是巧妙的外观模式(Facade)解决了这个问题。
Request
和Response
的外观模式既然是由于Request
和Response
向上转换类型后传输出现了问题,咱们就从这两个类入手改造,引入RequestFacade
和ResponseFacade
两个类,这两个类和Request
和Response
同样须要实现ServletRequest
和ServletResponse
接口。
- RequestFacade类
【图二】
public class RequestFacade implements ServletRequest { private ServletRequest request = null; public RequestFacade(Request request) { this.request = request; } //实现ServletRequest中方法 public Object getAttribute(String attribute) { return request.getAttribute(attribute); } public Enumeration getAttributeNames() { return request.getAttributeNames(); } public String getRealPath(String path) { return request.getRealPath(path); } //其余方法省略...
- ResponseFacade类
【图三】
public class ResponseFacade implements ServletResponse { private ServletResponse response; public ResponseFacade(Response response) { this.response = response; } //实现ServletResponse 中方法 public void flushBuffer() throws IOException { response.flushBuffer(); } public int getBufferSize() { return response.getBufferSize(); } public String getCharacterEncoding() { return response.getCharacterEncoding(); } //其余方法省略... }
经过观察两个外观类,其实他们什么也没有作,全部的接口实现方法都是调用内部的ServletRequest
和ServletResponse
的具体实现类来处理的。咱们能够这样改造咱们上面ServletProcessor
类中的代码
RequestFacade requestFacade = new RequestFacade(request); ResponseFacade responseFacade = new ResponseFacade(response); servlet.service( requestFacade, responseFacade);
传入Servlet
实现类中service
方法的参数变成了RequestFacade
和ResponseFacade
类型,程序员就不能再代码中使用类型转换转换为Request
和Response
类型,因此RequestFacade
和ResponseFacade
避免了原来Request
和Response
类不但愿对外可见的方法的暴露。
注:
1.其实从RequestFacade
和ResponseFacade
实现和类图上更像是代理模式,可是此处使用场景确实起到了对外提供统一接口的做用,因此从功能上讲,叫外观模式也无可或非。
2.即便采用了外观类,程序员依然能够在Servlet
中使用反射获取到外观类中private
属性的内部类型,可是和强制转型相同,程序员应该按照Servlet协议编写程序,不然除非清楚本身目的,否则我想不到这样作的意义。
public class HttpServer { private static final String SHUTDOWN_COMMAND = "shutdown"; private boolean shutdown = false; public static void main(String[] args) { HttpServer httpServer=new HttpServer(); httpServer.await(); } public void await() { try (ServerSocket serverSocket = new ServerSocket(8080)) { serverProcess(serverSocket); } catch (IOException e) { e.printStackTrace(); } } private void serverProcess(ServerSocket serverSocket) { while (!shutdown) { try (Socket socket = serverSocket.accept()) { // System.out.println(socket.hashCode()); InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream(); //建立Request对象 Request request = new Request(input); request.parse(); //建立Response对象 Response response = new Response(output); response.setRequest(request); if (request.getUri().startsWith("/servlet/")) { //若是地址以/servlet开头就做为Servlet处理 ServletProcessor processor = new ServletProcessor(); processor.process(request, response); }else { //不然做为静态资源使用 StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); } shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (IOException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); } } } }
和上篇文章中处理用户请求相似,咱们保留了处理处理静态资源的能力(StaticResourceProcessor
具体实现见源码),又增长了处理Servlet
的功能。
首先编写一个本身的Servlet
实现类。
public class HelloWorldServlet implements Servlet { @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { res.getWriter().println("<h1>Hello World!</h1>"); } //其余方法省略 }
注意,这个HelloWorldServlet
不在任何package
下,由于加载的时候就是用请求地址携带的类名加载,若是添加了包名,反射的时候会加载失败,之后咱们会修复这个问题。
编译这个类,将编译好的class文件放入D:\webRoot
文件夹(代码中定义的路径)。
在浏览器地址栏输入http://localhost:8080/servlet/HelloWorldServlet,浏览器会打印出Hello World!。
至此咱们实现了一个简单的Servlet
容器,虽然咱们的功能很是简陋,可是经过两篇文章的讲解,你们应该能理解一个浏览器请求是如何通过服务器处理最终返回能够显示页面的大体流程。是否是颇有成就感,简单的几行代码就能演示咱们平常使用的Tomcat服务器的基本功能。不过咱们只看到了冰山一角,从此的文章会逐步一览全貌。
文中源码地址:https://github.com/TmTse/tiny...
《深刻剖析Tomcat》