Tomcat剖析(二):一个简单的Servlet服务器
Tomcat剖析(二):一个简单的Servlet服务器
- 1. Tomcat剖析(一):一个简单的Web服务器
- 2. Tomcat剖析(二):一个简单的Servlet服务器
- 3. Tomcat剖析(三):连接器(1)
- 4. Tomcat剖析(三):连接器(2)
- 5. Tomcat剖析(四):Tomcat默认连接器(1)
- 6. Tomcat剖析(四):Tomcat默认连接器(2)
- 7. Tomcat剖析(五):容器
第一部分:概述
这一节基于《深度剖析Tomcat》第二章: 一个简单的Servlet服务器 总结而成。
上一节,我们了解了一个简单的Web服务器的总体处理流程是怎样的;这一节,我们开始搭建一个简单的Servlet容器,也就是增加实现了servlet的简单加载执行,而不仅仅是将文件内容输出到浏览器上。当然,这一节的servlet的实现是最简单的,用来了解整个Servlet的大致工作流程。
最好先到我的github上下载本书相关的代码,同时去上网下载这本书。
说到servlet,首先得先了解 javax.servlet.Servlet 接口,Servlet接口中的方法描述了servlet的生命周期方法,即init、service和destroy。我们自己所有的servlet必须实现这个接口或者继承实现了这个接口的类(最一般的情况就是继承HttpServlet)。对于servlet生命周期的方法就不在这里具体描述了,可以自行查询关于“servlet生命周期”的资料。在这里只要知道Servlet接口可以完成“在请求第一次到来时初始化servlet、对每次请求调用service方法执行请求、servlet关闭时调用destroy方法销毁servlet”3个功能就够了。
一个Http请求过来时,Servlet服务器经历以下过程:
- Server创建一个serverSocket对象,等待Client发送请求
- Client发送请求后,Server获取用户socket,从而得到请求的输入输出流
- 从输入输出流中创建request和response对象
- 解析request,如果是静态资源就创建StaticResourceProcessor实例,传递请求和响应给对应的方法,具体方法就是:从request中获取URI,判断文件是否存在,如果不存在就响应404 ,存在则将文件内容写到浏览器;如果是servlet请求就创建ServletProcessor1实例,同样传递请求和响应给对应的方法,完成加载servlet和调用Servlet的service方法执行。
- 关闭用户socket
- 判断URI是否为/SHUTDOWN,如果不是则重新进入等待请求状态,回到第2步,否则关闭服务器
整个流程主要包含5个类,HttpServer1、ServletProcessor1、StaticResourceProcessor、Request、Response。
HttpServer1.java:完成创建ServerSocket对象,获取Socket对象及其输入输出流,解析请求,创建Request对象和Response对象,并将不同类型的请求分派给不同的Processor处理
ServletProcessor1.java:当请求servlet时创建的对象,用于加载和执行servlet。
StaticResourceProcessor.java:当请求的资源是静态资源时创建的对象,调用Response对象的sendStaticResource()处理静态资源
Request.java:与上一节基本一样,是对输入流解析实现,获取URI。不同之处在于实现javax.servlet.ServletRequest 接口,这一节中除了解析请求的方法外,其它未实现的方法置为默认值。
Reponse.java:与上一节基本一样,完成对浏览器的响应,包含对请求文件存在与不存在的处理。不同之处在于实现 javax.servlet.ServletResponse接口,这一节中除了getWriter方法外,其它未实现的方法置为默认值。
注意:在这一节中有些东西看起来是不合理的,以后的章节中会改进:
1. Servlet接口仅仅是调用了service方法,没有调用int方法和destroy方法
2. 每一个servlet被请求时,servlet类就被加载一次
代码陈述更方便,下面开始将结合代码讲解。
第二部分:代码讲解
HttpServer1.java
这个类和上一节HttpServer的非常相似。
不同之处只有在判断请求的类型时进行if else处理,而不是直接用上一节的response.sendStaticResource()直接将文件内容写到浏览器
对应的,如果判断确实是请求静态资源(即URL不以/servlet/开头)就调用StaticResourceProcessor处理器的process方法,显示文件到浏览器中
反之,如果判断是servlet,调用ServletProcessor1的process方法加载和执行servlet。
package ex02.pyrmont;
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 {
// 用于判断是否需要关闭服务器,默认是false
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
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
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();//Server一直等待直到Client发送请求
input = socket.getInputStream();//接收请求后获取输入输出流
output = socket.getOutputStream();
//创建request对象,传入input参数用于获取输入流的参数
Request request = new Request(input);
request.parse();//解析request对象,这一节同样也只是获取请求中请求行的URI
//创建response对象,传入output对象用于获取Writer对象
Response response = new Response(output);
response.setRequest(request);
//下面是这个类的关键所在
//当URL是以/servlet/开头时说明请求servlet,使用ServletProcessor1处理器处理
//否则说明是请求静态资源,由StaticResourceProcessor处理器处理
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
} else {
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
}
//关闭用户socket
socket.close();
//如果URI是/SHUTDOWN说明需要关闭服务器
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
}
HttpServer1.java的详细说明
可以参照第一节的HttpServer.java的详细说明
由此类可以看出:
- 要请求一个静态资源,你可以在你的浏览器地址栏或者网址框里边敲入一个URL:http://machineName:port/staticResource。
- 要请求一个 servlet,你可以使用下面的URL:http://machineName:port/servlet/servletClass
ServletProcessor1.java
这个类是这一节的关键类,
仅仅只有一个process方法,但方法内部却没那么简单。
先上代码
package ex02.pyrmont;
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();// 获取URI,如/servlet/className
// 要知道servlet名,截取第二段,即可获得className
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
URLClassLoader loader = null;
try {
//try块中用于创建URLClassLoader对象
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
//Constants类存储静态常量,在本节最后贴上。
//Constants.WEB_ROOT即System.getProperty("user.dir") + File.separator + "webroot";
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);
//经过以上过程后可以得到类似“file:E:/java/tomcat/servletName/”的路径
//最后URLClassLoader对象根据这个url获取到相应路径下serlvet的类加载器
loader = new URLClassLoader(urls);
} catch (IOException e) {
System.out.println(e.toString());
}
Class myClass = null;
try {
myClass = loader.loadClass(servletName); //根据反射获取Class对象
} catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();//创建Servlet实例
servlet.service((ServletRequest) request,//执行servlet
(ServletResponse) response);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
}
}
}
ServletProcess1.java详细说明:
- 要加载 servlet,你可以使用 java.net.URLClassLoader 类,它是 java.lang.ClassLoader类的一个直接子类。
- public URLClassLoader(URL[] urls); 这里 urls 是一个 java.net.URL 的对象数组,这些对象指向了加载类时候查找的位置。任何以/结尾的 URL 都假设是一个目录。
- 类加载器必须查找的地方只有一个,如工作目录下面的 webroot目录。因此,我们首先创建一个单个 URL 组成的数组。URL 类提供了一系列的构造方法,所以有很多构造一个 URL 对象的方式。
- 一旦你拥有一个 URLClassLoader 实例,你使用它的 loadClass 方法去加载一个 servlet 类。(实在不懂怎么用可以自己查看API或者百度谷歌一下)
- 然后,process 方法创建一个 servlet 类加载器的实例, 把它向下转换(downcast) 为 javax.servlet.Servlet, 并调用 servlet 的 service 方法
这里的servlet是自己定义的,如PrimitiveServlet.java,前面说了,我们自己所有的servlet必须实现这个接口或者继承实现了这个接口的类
访问时用http://machineName:port/servlet/PrimitiveServlet访问即可
package ex02.pyrmont;
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class PrimitiveServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {
System.out.println("init");
}
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
System.out.println("from service");
PrintWriter out = response.getWriter();
out.println("Hello. Roses are red.");
out.print("Violets are blue.");
}
public void destroy() {
System.out.println("destroy");
}
public String getServletInfo() {
return null;
}
public ServletConfig getServletConfig() {
return null;
}
}
Request.java、Response.java
这两个类内容很多而且不是本节关键代码,大家可以根据概述部分对这两个类的简单介绍,并结合我的github上的Tomcat4的代码找到ex02.pyrmont包下的这两个类,最后与上一节这两个类的代码(对应ex01.pyrmont包)对比以下。应该就没什么大问题,这里就不说了
下面的是Constants.java,打消大家的疑虑
package ex02.pyrmont;
import java.io.File;
public class Constants {
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
}
ServletProcessor2.java
ServletProcess1.java改成ServletProcess2.java,使用RequestFacade和ResponseFacade进行封装
改变的内容不多,如下
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}
当然,HttpServer1.java改为HttpServer2.java,改变之处只有一个地方,即创建ServletProcess1实例改成ServletProcess2实例
第三部分:代码安全性问题
在 ServletProcessor1 类的 process 方法,你向上转换ex02.pyrmont.Request 实例为 javax.servlet.ServletRequest ,并作为第一个参数传递给servlet 的 service 方 法 。 你 也 向 下 转 换 ex02.pyrmont.Response 实 例 为javax.servlet.ServletResponse,并作为第二个参数传递给 servlet 的 service 方法。
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request,(ServletResponse) response);
}
这会危害安全性。知道这个 servlet 容器的内部运作的 Servlet 程序员可以分别把ServletRequest 和 ServletResponse 实例向下转换为 ex02.pyrmont.Request 和 ex02.pyrmont.Response,并调用他们的公共方法。拥有一个 Request 实例,它们就可以调用 parse方法。拥有一个 Response 实例,就可以调用 sendStaticResource 方法。
为了解决这个问题,我们增加了两个 façade 类: RequestFacade 和 ResponseFacade。RequestFacade 实现了 ServletRequest 接口并通过在构造方法中传递一个引用了 ServletRequest 对象的 Request 实例作为参数来实例化。ServletRequest 接口中每个方法的实现都调用了 Request 对象的相应方法。然而 ServletRequest 对象本身是私有的,并不能在类的外部访问。我们构造了一个 RequestFacade 对象并把它传递给 service 方法,而不是向下转换Request 对象为 ServletRequest 对象并传递给 service 方法。 Servlet 程序员仍然可以向下转换ServletRequest 实例为 RequestFacade,不过它们只可以访问 ServletRequest 接口里边的公共方法。现在 parseUri 方法就是安全的了。
上面这段话怎么理解呢?
其实就是门面模式(Facade Pattern)。门面模拟式简单来说就说屏蔽某些方法,让外部无法访问。字面上意思就是只看到门口的内容。
现在用上面的例子中的PrimitiveServlet.java,如果在service()添加如下注释掉的代码
前两行注释:结果是虽然可以强制转换为Request对象且可以调用parse()这个私有方法,但是是运行不了了,将会抛异常
后两行注释:结果是第一行可以正常的转换,但是第二行是没有这个方法的,调用不了
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
System.out.println("from service");
//Request requestTest = (Request)request;
//requestTest.parse(); //虽然可以调用,但是是报错的
//RequestFacade requestFacadeTest = (RequestFacade)request;
//requestFacadeTest.parse(); 发现没有这个方法
PrintWriter out = response.getWriter();
out.println("Hello. Roses are red.");
out.print("Violets are blue.");
}
如果还是不懂,下面有个我自己写来测试的代码。
ServletReq模拟javax.servlet.ServletRequest
Req模拟ex02.pyrmont.Request接口(所以这个类实现了模拟Tomcat的标准接口ServletRequest的ServletReq)
ReqFacade模拟ex02.pyrmont.RequestFacade(封装了Req)
setAttribute方法模拟了大家都通用的方法
main函数中,对应代码第8行中标准的ServletReq确实可以顺利转换为Req实体类
但是调用方法后,会报如下错误:
Exception in thread "main" java.lang.ClassCastException: ex02.pyrmont.ReqFacade cannot be cast to ex02.pyrmont.Req at ex02.pyrmont.MyTest.main(MyTest.java:10)
这就保证了安全性,明白了吗?
public class MyTest {
public static void main(String[] args){
Req request = new Req();
ReqFacade requestFacade = new ReqFacade(request);
ServletReq servletReq = (ServletReq)requestFacade;
Req req = (Req)servletReq;
req.parse();
}
}
interface ServletReq{
void setAttribute();
}
class Req implements ServletReq{
@Override
public void setAttribute() {
System.out.println("AAA");
}
public void parse(){
System.out.println("AAA2");
}
}
class ReqFacade implements ServletReq{
private Req b;
ReqFacade(Req b){
this.b = b;
}
@Override
public void setAttribute() {
b.setAttribute();
}
}
第四部分:小结
这一节,我们完成了根据判断不同的URI对servlet请求和静态资源请求分别处理的简单实现,相比上一节难度大了一些。
下一节开始Tomcat的连接器。
附
相应代码可以在我的github找到下载,拷贝到eclipse,然后打开对应包的代码即可。
如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。
如果有什么疑问或错误,可以发表评论或者加我QQ:1096101803告知,谢谢。
Tomcat剖析(二):一个简单的Servlet服务器的更多相关文章
- how tomcat works 读书笔记(二)----------一个简单的servlet容器
app1 (建议读者在看本章之前,先看how tomcat works 读书笔记(一)----------一个简单的web服务器 http://blog.csdn.net/dlf123321/arti ...
- Tomcat剖析(一):一个简单的Web服务器
Tomcat剖析(一):一个简单的Web服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器 ...
- 一个简单的Web服务器-支持Servlet请求
上接 一个简单的Web服务器-支持静态资源请求,这个服务器可以处理静态资源的请求,那么如何处理Servlet请求的呢? 判断是否是Servlet请求 首先Web服务器需要判断当前请求是否是Servle ...
- 转:【专题十二】实现一个简单的FTP服务器
引言: 休息一个国庆节后好久没有更新文章了,主要是刚开始休息完心态还没有调整过来的, 现在差不多进入状态了, 所以继续和大家分享下网络编程的知识,在本专题中将和大家分享如何自己实现一个简单的FTP服务 ...
- 专题十二:实现一个简单的FTP服务器
引言: 在本专题中将和大家分享如何自己实现一个简单的FTP服务器.在我们平时的上网过程中,一般都是使用FTP的客户端来对商家提供的服务器进行访问(上传.下载文件),例如我们经常用到微软的SkyDriv ...
- 《深度解析Tomcat》 第一章 一个简单的Web服务器
本章介绍Java Web服务器是如何运行的.从中可以知道Tomcat是如何工作的. 基于Java的Web服务器会使用java.net.Socket类和java.net.ServerSocket类这两个 ...
- 在cmd下编译一个简单的servlet时出现程序包javax.servlet不存在
由于servlet和JSP不是Java平台JavaSE(标准版)的一部分,而是Java EE(企业版)的一部分,因此,必须告知编译器servlet的位置. 解决“软件包 javax.servlet不存 ...
- 开发部署一个简单的Servlet
Servlet是一个执行在服务器端的Java Class文件,载入前必须先将Servlet程序代码编译成.class文件,然后将此class文件放在servlet Engline路径下.Servlet ...
- 自己模拟的一个简单的web服务器
首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...
随机推荐
- std::string 不支持back
string s = "abc"; if ( s.back() == 'c' ) .... , 不支持back, 但是用VS2010好吧 后来发现, string的back/fr ...
- 【JAVA】【NIO】3、Java NIO Channel
Java NIO和流量相似,但有些差异: ·通道可读写,流仅支持单向.读或写 ·异步通道读取 ·通道读写器,他们是和Buffer交替 道的实现 下面是Java NIO中最重要的通道的实现: ·File ...
- 导致Asp.Net站点重启的10个原因
原文:导致Asp.Net站点重启的10个原因 Asp.Net站点有时候会莫名其妙的重启,什么原因导致的却不得而知,经过一番折腾后,我总结了导致Asp.Net站点重启的10个原因 1. 回收应用程序池会 ...
- RedHat Linux乱码解决方案(转)
RedHat Linux中出现中文乱码主要是由于没有安装中文字体,因此解决方案主要是安装中文字体,所以 第一步,挂载安装的光盘 在虚拟机的菜单栏里,选择:VM->Settings,点击Setti ...
- [DEEP LEARNING An MIT Press book in preparation]Deep Learning for AI
动人的DL我们有六个月的时间,积累了一定的经验,实验,也DL有了一些自己的想法和理解.曾经想扩大和加深DL相关方面的一些知识. 然后看到了一个MIT按有关的对出版物DL图书http://www.iro ...
- apache-maven-3.2.1设备
maven 是一个项目管理工具,并建立自己主动.本文所讲apache-maven-3.2.1设备. 它的下载: http://maven.apache.org/download.cgi ,选apach ...
- HDU 4896 Minimal Spanning Tree(矩阵高速功率)
意甲冠军: 给你一幅这样子生成的图,求最小生成树的边权和. 思路:对于i >= 6的点连回去的5条边,打表知907^53 mod 2333333 = 1,所以x的循环节长度为54,所以9个点为一 ...
- Java学习路径:不走弯路,这是一条捷径
1.如何学习编程? JAVA是一种平台.也是一种程序设计语言,怎样学好程序设计不只适用于JAVA,对C++等其它程序设计语言也一样管用.有编程高手觉得,JAVA也好C也好没什么分别,拿来就用.为什么他 ...
- php学习之路:WSDL详细解释(两)
3.定义服务使用的逻辑消息 当服务的操作被调用时.服务被定义为消息交换.在wsdl文档中,这些消息被定义message元素. 这些消息由称之为part元素的部分组成. 一个服务的操作,通过指定逻辑消息 ...
- mongodb.conf
# mongodb.conf # Where to store the data. dbpath=/var/lib/mongodb #where to log logpath=/var/log/mon ...