您的位置:首页 > 运维架构 > Tomcat

tomcat web应用服务器原理讲解分析 待续

2012-03-27 23:44 387 查看
1.1.1. 引言


现在网络是人们日常生活的一部分,是连接外部世界获得外界信息的主要途径之一。我们其中的任何一个人如果打开电脑,启动浏览器,输入网址,那么浏览器里边就展现了互联网上的信息了。实际上,我们的浏览器信息来自网络服务供应商,但是网络服务商并不是自己将自己的信息发送到我们的计算机里,而是依靠中间件——WEB服务器来完成这个任务。WEB服务器为网络服务供应商的软件提供一个平台,网络服务商可以将他的软件部署在这个平台上,让WEB服务器来负责完成客户和自己软件之间信息的交互和交互过程中所有状态的维护。

我们想象下面场景,国家在某地建立了科技园区,科技园区有如下设计:

第一,科技园区为某一行业的企业提供入住,比如生活日用品。

第二,为这一行业提供所有需要服务,比如,对企业的开业,经营,破产等的管理,对物业,通信,外联服务的提供等等。

第三,任何生产日用品的企业可以随时入住,只要填写必要的信息,办理注册就可以了,科技园会安排开业事宜,期间企业在科技园安排差不多的时候可以介入,安排自己的事情。

第四、开业后,企业负责生产和经营,期间需要的条件由科技园来提供。

第五,当企业需要破产的时候,科技园负责企业的破产事宜。

其实,这就是一个服务器和部署在上面的应用的形象描述。科技园就是服务器,负责对企业的生命周期管理,运行期间的服务,企业的对外通信服务;而企业就是部署在服务器上的应用,负责完成具体的事情,然后科技园负责和客户交互信息。

1.1.2. Web服务器特点


WEB服务器也称为WWW(WORLD WIDE WEB)服务器,具有如下显著特点:

(1)和客户端的浏览器一起工作

(2)浏览器统一资源定位器(URL)

(3)客户端一般使用HTTP协议向服务器发送请求

(4)服务器发送给客户浏览器的文本格式是HTML

(5)服务器可以支持多种协议,但是必须支持HTTP协议

(6)服务器一般都支持J2EE标准

我们简单介绍了WEB服务器的概念,那么下面我们将编写一个最简单的服务器。你将会了解WEB服务器的原理

1.2. 服务器的相关知识准备


我们服务器实现简单客户端和服务器的通信。和现行大多数WEB服务器一样,这个通信使用套接字作为底层基础,在套接字上使用HTTP协议进行信息通信。其中套接字通信涉及JDK(java development kit)的两个重要的类Socket 和 ServerSocket。. 因此,在编写我们的简单服务器的之前,我们先讨论HTTP协议和这2个类,接下来温习javax.servlet.Servlet接口。然后开始我们的简单服务器的编写。

1.2.1. HTTP协议


协议的含义

在讨论HTTP协议之前,我们先阐述一下协议的概念。协议,就是双方双方都遵守的契约。假设张三和李四是同班同学,都是中学生,按照中学生守则,中学生是不许谈恋爱的,但是两个人相互有好感,私下谈了恋爱。为了避免被家长或者学校发觉,他们决定,在风声紧的时候两个人装作不认,但是要定期传纸条,同时要纸条内容不能外泄,即使外泄也要别人也不知道纸条上文字的意思,但是他们自己要明白。于是,他们订立如下协议(契约):

1、纸条上是字母”J“代表要对方小心,最近风声有点紧

2、纸条上是“O”代表我爱你

3、纸条上是”Y“,代表有事要和对方说

4、纸条上”D“,代表我等你

5、纸条上是”F“,代表放学后

现在,张三和李四可以准备有这几个字母的纸条,当需要的时候,将纸条传给对方,比如一天,张三向李四传递了一个纸条,内容是”Y F D“,于是放学后,李四留下等张三商谈事宜。

从上面例子我们可以看出协议有如下特点

1、协议是通信双方都认同并遵守的。比如张三和李四都认同这个协议,通信时都遵守了此协议。

2、协议的规定是双发都明白的,比如上面的协议,张三和李四都知道字母代表的意思。

3、只要了解了协议的内容,就可以用协议通信。比如王五知道了张三和李四的协议,那么王五得到张三传给李四的纸条后,王五就会明白张三要对李四说什么。

4、要想和使用协议的对象通信,那么他必须遵守这个协议。比如王五看纸条时,他无形中遵守了张三和李四的协议。

对于WEB服务器而言,协议就是客户端向服务器端传送数据时要遵守的格式,服务器端会按照这个格式解析客户端请求数据。

HTTP协议

现在的所有WEB服务器都支持HTTP协议,它规定了客户端向服务器发送请求时要遵守的规则。

HTTP协议是英文The Hypertext Transfer Protocol ,中文意思是“超文本传输协议”。

它是一种请求——响应式的协议,客户端发起一个请求,服务器解析,完成请求,给客户端响应。HTTP协议的第一个版本是 HTTP/0.9, 然后是 HTTP/1.0. 当前版本是由 RFC 2616(.pdf) 定义的 HTTP/1.1.。由于现在绝大多数网络应用都遵守HTTP1.1,为了理解WEB服务器的原理,这里简单介绍一下HTTP1.1协议。如果你想详细了解HTTP1.1的细节,请阅读官方文档RPC2616。(至于HTTP0.9、HTTP1.0和HTTP1.1的区别将在以后章节中阐述)。

使用 HTTP 的时候, 客户端总是先和服务器建立连接,建立连接后,发送给服务器一个 HTTP 请求.。服务器解析客户端的请求,代理客户端的任务,将结果返回给客户端。服务器不会主动和客户端建立连接。客户端和服务器都可以提前中断一个连接。例如, 当你使用 web 浏览器的浏览网页的时候,在载入网页的时候,你可以点击浏览器上的停止按钮结束网页打开过程,关闭与 web 服务器的连接.。

--------------------------------------------------------------

HTTP Requests

上面提过,HTTP1.1协议是请求/应答模式的协议,那么HTTP协议分为两个部分,一是请求协议,一是应答协议。下面看一下请求协议。

HTTP请求的格式如下:

1、方法-统一资源标识符-协议/协议版本

2、请求头

3、请求实体

下面是一个HTTP请求实例

POST /servlet/testt.jsp HTTP/1.1

Accept: text/plain; text/html

Accept-Language: en-us

Connection: Keep-Alive

Host: localhost

Referer: http://localhost/begin.jsp

User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)

Content-Length: 33

Content-Type: application/x-www-form-urlencoded

Accept-Encoding: gzip

name=lisi&age=40

为了使您对HTTP协议有个大概印象,下面将就上面涉及的HTTP请求的相关项做以下介绍:

POST /servlet/test.jsp HTTP/1.1

POST是请求的方式, HTTP 1.1 支持7种请求,分别是::GET, POST, HEAD, OPTIONS, PUT, DELETE, 和 TRACE. GET 和 POST 。其中最常用的是GET和POST,他们区别如下:

1、Post传输数据时,不需要在URL中显示出来,而Get方法要在URL中显示。

2、Post传输的数据量大,可以达到2M,而GET方法只能传递大约1024字节。

3、Post目的是将数据传送到服务器段。而Get目的是给服务器消息,要从服务器下载数据。、所以我们可以轻而易举的明白:GET既然只是通知服务器需要什么数据,那么它需要传递的信息量就很少,用来传送数据到服务器,那么它需要的信息量就很大,比如传递文件。这也是上面第二条之所以那样规定的理由。

POST /servlet/testt.jsp HTTP/1.1

其中POST规定了传输数据使用的方法,/servlet/test.jsp则是请求要访问的统一资源标识符(URI),要访问根目录下servlet目录下的test.jsp页面。URI 通常以相对于服务器的根目录来进行解析.,因此,它总是以 / 开始,比如上面的/servlet/test.jsp。(URL 实际是 URI 的一种)。HTTP表明使用的是HTTP协议,1.1表示使用的版本是1.1。

Accept: text/plain; text/html

客户浏览器可以接受的内容类型,这里是简单文本和html文档。

Accept-Language: en-us

客户浏览器希望从服务器接收的内容使用的语言。当服务器提供多于一种语言的时,这个选项起作用。

Connection: Keep-Alive

Content-Length: 33

Keep-Alive表示需要持久连接。如果Servlet看到这里的值为“Keep-Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),他就建立持久连接,当页面包含多个元素时(例如Applet,图片)时,使用这个连接将全部资源发给客户端后,才关闭连接,这样可以大量节约宝贵的网络和服务器资源,减少页面下载时间。这个选项和Content-Length一起使用。 因为使用持久连接,服务器需要知道何时读完请求字符串。

Host: localhost

指定请求资源的Intenet主机

Referer: http://localhost/begin.jsp

表示访问网络资源的出发点

User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)

浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用

Content-Type: application/x-www-form-urlencoded

表示后面的文档属于什么类型。Servlet默认为text/plain,但通常需要显式地指定。这里是指定为表单。

Accept-Encoding: gzip

文档的编码方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩。

HTTP请求使用两个回车换行将请求头和请求体分开,也就是说在请求头和请求体之间有一个空白行。

请求主体很简单,内容如下

name=lisi&age=40

当然我们使用的是post方法,请求体可以有很长,最大是2M。

HTTP Responses

HTTP 响应也是由3部分组成:

1、协议-状态代码-描述

2、响应头

3、响应实体

下面是一个 HTTP 响应的例子:

HTTP/1.1 200 OK

Server: Microsoft-IIS/4.0

Date: Thu, 13 Jul 1998 13:13:33 GMT

Content-Type: text/html

Content-Length: 84

<html>

<head>

<title>HTTP Response</title></head><body>

Welcome to HTTP word!

</body>

</html>

HTTP/1.1 200 OK

标示使用的是HTTP协议,版本是1.1,状态200,标示成功,ok,标示一切正确。

Server: Microsoft-IIS/4.0

标示服务器类型

Content-Type: text/html

内容类型是HTML

Content-Length: 112

内容长度是112字符

下面是两个回车换行,也就是隔一行。后面是响应体。

1.1.1. Socket类


此类是程序进行网络数据交换使用的类,通常用来发送请求和读取响应。位于java.net包中。下面我们看一下Socket类的相关重要方法方法签名。

1、构造方法签名

public Socket(String host, int port)

host是远程服务器名称或者IP 地址,port 是服务器的端口号。例如

new Socket("127.0.0.1", 80)

2、 获得向发送数据的输出流

public OutputStream getOutputStream() throws IOException

当你创建了套接字实例后,你就可以使用它发送和接收消息。发送消息的时候, 使用Socket 类的 getOutputStream 方法获得 java.io.OutputStream 对象。为了方便,可以对OutputStream 进行包装,包装成一个 java.io.PrintWriter 对象,用来发送文本。

3、获得从服务器读取消息的输入流

public InputStream getInputStream()throws IOException

输入流用来从目标机器读取消息。

--------------------------------------------------

[b]ServerSocket类
[/b]

此类通常服务器端,通常使用它来接收和响应客户请求。在互联网中,客户端可能有好多,每个客户随时可能向服务器发送请求,服务器根本不知道谁什么时间要和自己通信。所以,服务器端必须一直处于运行状态,等待客户连接请求。ServerSocket正是为此设计。

此类位于java.net包中,下面看一下它的其中几个重要方法签名。

1、其中的一个构造方法

public ServerSocket(int port, int backLog, InetAddress bindingAddress)

此类的构造器共有四个,我们来看一下其中的一个,也就是我们的简单服务器将要用到的构造器。其余三个可以参看JDK文档。

在此构造器中,port是服务器监听的端口,所有来自port端口指定的请求都会被监听到,也就是说,每个从port端口传来的数据都会由套接字来处理。backLog是服务器所能容忍的最大请求队列长度,当请求数量大约此数值时,服务器将拒绝响应新的请求。bindingAddress是服务器绑定的地址,它必须是java.net.InetAddress 的一个实例,构造此实例的方法之一是调用方法InetAddress.getByName("127.0.0.1")。

2、accept 方法

public Socket accept() throws IOException

创建了ServerSocket 实例,后,你就可以调用它的 accept 方法使服务器进入监听状态。它只有在有连接请求的时候才会有返回。它返回一个 Socket 类的实例,用于和客户端通信。

1.1.2. Servlet接口

J2EE作为一个标准,它规定了J2EE编程需要遵循的规范。servlet接口就是此标准中的一个规定。也就是说所有的servlet都必须实现标准中规定servlet接口。

Servlet接口有5个方法,其签名如下:

public void init(ServletConfig config) throws ServletException

public void service(ServletRequest request, ServletResponse response) throws ServletException, java.io.IOException

public void destroy()

public ServletConfig getServletConfig()

public java.lang.String getServletInfo()

其中,init, service, 和 destroy 方法是 servlet 的生命周期方法。init 方法是初始化方法,只在Servlet初始化时调用一次。初始化成功后,servlet调用service 方法.,对客户端进行服务。它接受 服务器传递的一个 javax.servlet.ServletRequest 对象和一个 javax.servlet.ServletResponse 对象。ServletRequest 对象包含客户端 HTTP 请求信息,ServletResponse 封装了servlet 的响应。当服务器需要关闭或者需要释放内存时,服务器删会除一个servlet实例,在删除之前会执行destroy方法,允许servlet程序员做一些清理工作。getServletConfig()方法用来获得servlet配置对象,getServletInfo()用来获得servlet的信息。
1.1. 一个简单服务器


通过上面的准备,我们已经有了编写简单服务器的知识准备。现在,我们开始我们简单服务器。此简单服务器实现对客户端请求的静态资源和动态servlet的处理和响应。

1.1.1. HttpWebServer类


此类是我们简单服务器的核心类,在随书光盘中的chapter/execixe1/src/server/core目录下可以看到此类的源码。下面是此类的关键代码:

public class HttpWebServer {

private boolean shut = false;

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);

}

while (!shut) {

Socket socket = null;

InputStream is = null;

OutputStream os = null;

try {

socket = serverSocket.accept();

is = socket.getInputStream();

os = socket.getOutputStream();

HttpRequest request = new HttpRequest();

request.setIs(is);

request.parseRequest();

HttpResponse response = new HttpResponse();

response.setRequest(request);

response.setOs(os);

RequestFacade requestFacade = new RequestFacade(request);

ResponseFacade responseFacade = new ResponseFacade(response);

//响应动态servlet

if (request.getUri().startsWith("/servlet/")) {

ServletProcessor processor = new ServletProcessor();

processor.process(requestFacade, responseFacade);

}else{

StaticResourceProcessor processor = new StaticResourceProcessor();

processor.process(requestFacade, responseFacade);

}

socket.close();

shut = request.getUri().equals(GlobalConstants.SHUTDOWN_COMMAND);

}

catch (Exception e) {

e.printStackTrace();

continue;

}

}

}

}

2011-03-28 14:12

我们看到,此类中只有一个方法await,此方法首先创建一个ServerSocket 对象分配给变量serverSocket :

serverSocket = new ServerSocket(port, 1,InetAddress.getByName("127.0.0.1"));

此ServerSocket 类的构造方法表明,我们服务器绑定的是本机的IP地址(127.0.0.1表示本机地址),端口是8080,最大处理的并发请求是1个,如果多于一个请求并发,那么将拒绝服务。此语句使用try...catch语句包围,如果创建服务器套接字失败,那么进入异常处理,打印堆栈后,退出系统。

接下来是一个while循环,循环的条件是shut为true(关于此变量稍后讨论)。在循环中,首先调用上面构建的ServerSocket 对象的accept方法:

socket = serverSocket.accept();

此方法只有有客户端的http请求到来时才返回,返回套接字对象。有了套接字对象,我们就可以通过套接字对象获得输入流和输出流来接收和发送信息。

is = socket.getInputStream();

os = socket.getOutputStream();

获得输入流对象后,我们就可以通过输入流来读取客户端的请求,并且解析它,这个是由HttpRequest 对象来完成的。所以程序构造了HttpRequest 对象,并在将输入流对象传递给它后,调用HttpRequest 对象的parse方法来解析http请求:

HttpRequest request = new HttpRequest();

request.setIs(is);

request.parseRequest();

构造并且解析请求对象后,紧接着构造了响应对象(HttpResponse ),并且将构造好的请求对象和输入流传递给HttpResponse 响应对象。在这里之所以将请求对象传递给响应对象,是因为我们在响应对象中做了响应客户端静态资源处理,在处理中需要用到我们构造的请求对象中的变量和方法。

当现在为止,我们分别构造好了请求对象和响应对象。那么我们就可以根据用户的请求类型构造不同的处理对象并调用相应的处理方法(process),从上面的代码列表1.1我们可以看到,process方法需要传递请求和响应对象。但是在这里我们并没有直接将我们构造好的对象传递给process方法,相反,我们对我们构造的请求对象(HttpRequest )和响应对象(HttpResponse )进行了包装,然后将我们的包装对象(门面)传递给process方法(具体原因在讨论RequestFacade 和ResponseFacade 时可以详细解释)::

RequestFacade requestFacade = new RequestFacade(request);

ResponseFacade responseFacade = new ResponseFacade(response);

if (request.getUri().startsWith("/servlet/")) {

ServletProcessor processor = new ServletProcessor();

processor.process(requestFacade, responseFacade);

}else{

StaticResourceProcessor processor = new StaticResourceProcessor();

processor.process(requestFacade, responseFacade);

}

区分不同用户的请求是根据请求对象解析的用户请求的uri来判定,如果请求的是以/servlet/开始的,那么响应servlet,否则响应静态资源。

处理完请求后我们关闭套接字,收回资源。

socket.close();

最后,判断服务器是否应该关闭:在此类中,我们定义一个布尔变量shut,用来标示当前服务器是否应该关闭,默认值是false,表明服务器处于运行状态。

shut = request.getUri().equals(GlobalConstants.SHUTDOWN_COMMAND);

此方法判断客户的请求是否是关闭服务器命令,也就是看请求对象中的uri是否是常量SHUTDOWN_COMMAND定义的字符串。

1.1.1. HttpRequest类


此类负责解析客户的http请求,并且获得客户请求的uri。此类在随书光盘中的chapter/execixe1/src/server/request目录下。此类实现了HttpServletRequest接口,主要目的是使服务器支持符合javaee标准的servlet。因为servlet程序员编写servlet是符合javaee标准的,在程序中他们会调用符合javaee标准的HttpServletRequest对象。下面是此类相关的重要代码:

public class HttpRequest implements HttpServletRequest{

private InputStream is = null;

private String uri = null;

public InputStream getIs() {

return is;

}

public void setIs(InputStream is) {

this.is = is;

}

public String getUri() {

return uri;

}

public void setUri(String uri) {

this.uri = uri;

}

public void parseRequest() {

byte[] content = new byte[0];

try{

int len;

byte[] buffer = new byte[1024];

len = is.read(buffer);

if(len>0){

byte[] tmp = new byte[content.length + len];

System.arraycopy(content, 0, tmp, 0, content.length);

System.arraycopy(buffer, 0, tmp, content.length, len);

content = tmp;

}

}catch (IOException e) {

e.printStackTrace();

}

uri = getRequestUri(new String(content));

}

private String getRequestUri(String content) {

int uirStartIndex = -1;

int uriEndIndex = -1;

uirStartIndex = content.indexOf(' ');

if (uirStartIndex >-1) {

uriEndIndex = content.indexOf(' ', uirStartIndex + 1);

if (uriEndIndex > uirStartIndex)

return content.substring(uirStartIndex + 1, uriEndIndex);

}

return null;

}

//以下省略一些没有用到的方法

......

}

此类关键方法有两个:parseRequest和getRequestUri。首先看一下getRequestUri,在这个方法中,我们读入1024个字节,因为我们的简单服务器使用GET方式提交请求,那么我们可以肯定,请求不会大于1024字节。所以我们只读取一次。当然实际的服务器要支持POST提交方式,在这里我们主要说明web服务器的工作原理,所以没有讨论POST提交方式。

byte[] buffer = new byte[1024];

len = is.read(buffer);

if(len>0){

byte[] tmp = new byte[content.length + len];

System.arraycopy(content, 0, tmp, 0, content.length);

System.arraycopy(buffer, 0, tmp, content.length, len);

content = tmp;

}

获得请求后,我们开始解析请求,调用获得uri的方法:

uri = getRequestUri(new String(content));

在getRequestUri方法中。根据http协议的特性获得uri,回忆上面我们介绍的http协议的格式,我们会发现,在形如POST /servlet/testt.jsp HTTP/1.1 的格式总,uri正位于请求的第一个空格和第二个空格之间。那么我们根据此特性获得uri。

int uirStartIndex = -1;

int uriEndIndex = -1;

uirStartIndex = content.indexOf(' ');

if (uirStartIndex >-1) {

uriEndIndex = content.indexOf(' ', uirStartIndex + 1);

if (uriEndIndex > uirStartIndex)

return content.substring(uirStartIndex + 1, uriEndIndex);

}

当然,真正的服务器的请求对象并不是这么简单,方法也要多的多。

2011-03-28 14:13
1.1.1. HttpResponse类


此类负责响应客户的请求,但是这里仅限于响应静态请求和错误页面。此类也实现javaee的标准接口HttpServletResponse。

public class HttpResponse implements HttpServletResponse{

private HttpRequest request = null;

private OutputStream os = null;

public void setRequest(HttpRequest request) {

this.request = request;

}

public OutputStream getOs() {

return os;

}

public void setOs(OutputStream os) {

this.os = os;

}

public void responsStaticResource() {

byte[] buffer = new byte[1024];

FileInputStream fis = null;

try {

File file = new File(GlobalConstants.ROOT, request.getUri());

if (file.exists()) {

long length = file.length();

fis = new FileInputStream(file);

int len = fis.read(buffer);

long count = len;

while (true) {

os.write(buffer, 0, len);

len = fis.read(buffer);

if(count+len>=length){

break;

}

count = count+len;

}

}

else {

String error = "<h1>resourse Not Found</h1>";

String msg = "HTTP/1.1 404 resourse Not Found\r\n" +

"Content-Type: text/html\r\n" +

"Content-Length: "+error.length()+"\r\n" +

"\r\n" +error;

os.write(msg.getBytes());

}

}

catch (Exception e) {

e.printStackTrace();

}

finally {

if (fis != null)

try {

fis.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

public PrintWriter getWriter() throws IOException {

PrintWriter printWrite = new PrintWriter(os, true);

return printWrite;

}

//以下省略一些没有用到的方法

......

}

在这个类中中有一个主要方法responsStaticResource,在这个方法中,响应了客户的静态资源的请求。首先,建立一个ROOT常量(ROOT常量是规定服务器静态资源存在的根根目录)规定的目录下的uri文件对象,如果文件是存在的那么响应此文件,将它发送给客户端:如果文件不存在,发送给客户端错误页面。在这里,文件的发送使用了一个while循环,每次读取文件的数据发送给客户端,同时记录已经发送的数据的大小总数,如果字数大于等于文件的大小则跳出循环。

if (file.exists()) {

long length = file.length();

fis = new FileInputStream(file);

int len = fis.read(buffer);

long count = len;

while (true) {

os.write(buffer, 0, len);

len = fis.read(buffer);

if(count+len>=length){

break;

}

count = count+len;

}

}

else {

String error = "<h1>resourse Not Found</h1>";

String msg = "HTTP/1.1 404 resourse Not Found\r\n" +

"Content-Type: text/html\r\n" +

"Content-Length: "+error.length()+"\r\n" +

"\r\n" +error;

os.write(msg.getBytes());

}

}

在发送错误信息时,使用了对象PrintWriter 。getWriter() 负责构造此对象,它的构造参数是输出流和一个布尔变量,当为true表示缓冲将自动冲刷(flush),为false表示不会自动冲刷,要程序员手工冲刷:

PrintWriter printWrite = new PrintWriter(os, true);

1.1.2. ServletProcessor类


此类用来响应客户的servlet请求。

public class ServletProcessor {

public void process(RequestFacade request, ResponseFacade response) {

String uri = request.getUri();

String servletName = uri.substring(uri.lastIndexOf("/") + 1);

Loader loader = new Loader();

ClassLoader classLoader = loader.getClassLoader();

Class servletClass = null;

try {

servletClass = classLoader.loadClass("client.webapp."+servletName);

TestServlet servlet = (TestServlet) servletClass.newInstance();

servlet.init(null);

servlet.service((ServletRequest) request, (ServletResponse) response);

}

catch (ClassNotFoundException e) {

e.printStackTrace();

} catch (InstantiationException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

} catch (ServletException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

}

2011-03-28 14:14
此类只有一个方法,process,它接受两个参数:request和response。此方法完成客户请求的servlet名称的获得,servlet类的加载,实例化调用servlet的初始化init和服务service方法。首先获得servlet名称,servlet名称的获取是通过分析请求的uri获得的,依据是通过获得最后一个路径分割符位置,然后截取后面的字符串得到的:

String uri = request.getUri();

String servletName = uri.substring(uri.lastIndexOf("/") + 1);

比如uri为 /servet/TestServlet,那么获得servlet名称就是TestServlet。然后构造我们自己的类加载器:

Loader loader = new Loader();

ClassLoader classLoader = loader.getClassLoader();

有了类加载器,我们可以根据servlet名称加载我们的servlet类:实例化,调用servlet的初始化方法init和服务方法service。

servletClass = classLoader.loadClass("client.webapp."+servletName);

TestServlet servlet = (TestServlet) servletClass.newInstance();

servlet.init(null);

servlet.service((ServletRequest) request, (ServletResponse) response);

1.1.1. Loader类

此类是我们自定义的类加载器,它扩展类java.net.ClassLoader类,如果您对此类不了解,请参数JDK文档。

public class Loader extends ClassLoader{

public ClassLoader getClassLoader(){

ClassLoader loader = null;

try {

URLStreamHandler streamHandler = null;

URL[] urls = new URL[1];

File classPath = new File(GlobalConstants.ROOT);

String repository = (new URL("file", null,

classPath.getCanonicalPath() + File.separator)).toString() ;

urls[0] = new URL(null, repository, streamHandler);

loader = new URLClassLoader(urls);

} catch (MalformedURLException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

return loader;

}

}

此类的超类是java.lang.ClassLoader的间接子类. 如果构造 URLClassLoader 类的实例, 就可以使用它的 loadClass 方法加载一个 servlet 类。这个类有3种构造器, 最简单的如下:

public URLClassLoader(URL[] urls);

urls 是 java.net.URL 对象的数组, java.net.URL 对象指向当加载一个类时所要搜寻的位置。任何以 / 结尾的 URL 都假定是一个目录;否则, URL 假定应用的是一个 .jar 文件, 必要的时候, 会下载并打开这个 .jar 文件.

在服务器中, 一个 class loader 能够查找到 servlet 类的地方叫做 repository.。

在我们的程序里, 只有一处需要class loader搜索,那就是我们servlet存放的地方。 因此,我们先创建了一个包含单个 URL 的数组. URL 类提供了好几种构造器, 所以有很多方式去构造一个 URL,对象.本类中,我们使用如下构造器。

public URL(URL context, String spec, URLStreamHandler hander)

throws MalformedURLException

你可以这样使用这个构造器: 传递一个 specification 作为第二个参数, null 作为它的第一个和第三个参数. 但是, 还有另一种构造器,它也接收3个参数:

public URL(String protocol, String host, String file)

throws MalformedURLException

因此, 如果你像下面这样写,编译器将不知道你要用的是哪个构造器:

new URL(null, aString, null);

你可以这样解决: 告诉编译器第三个参数的类型:

URLStreamHandler streamHandler = null;

new URL(null, aString, streamHandler);

对于第二个参数, 传递一个包含 repository 的 String (the directory where servlet classes can be found). 使用下面的代码创建:

String repository = (new URL("file", null,

classPath.getCanonicalPath() + File.separator)).toString();

所以此类的创建方法如下:

URL[] urls = new URL[1];

File classPath = new File(GlobalConstants.ROOT);

String repository = (new URL("file", null,

classPath.getCanonicalPath() + File.separator)).toString() ;

urls[0] = new URL(null, repository, streamHandler);

loader = new URLClassLoader(urls);

1.1.2. RequestFacade类和ResponseFacade类

这两个类分别位于在随书光盘中的chapter/execixe1/src/server/request和chapter/execixe1/src/server/response目录下。

在HttpWebServer类中,我们记得,我们并没有直接将HttpRequest和HttpResponse传递给ServletProcessor对象的process方法,而是将它们做了包装,将它们包装成RequestFacade和ResponseFacade对象。之所以这么麻烦的做这个包装,主要是处于安全考虑。如果我们将HttpRequest和HttpResponse直接传递给ServletProcessor对象的process方法,尽管我们传递给servlet的是标准的Javaee标准的请求和响应对象,但是servlet程序员可以通过类型上溯获得HttpRequest和HttpResponse对象的直接引用,从而修改服务器的HttpRequest和HttpResponse的内部实现,如果那样,不是很危险吗?所以我们使用了包装类,这样我们传递给ServletProcessor对象的process方法是我们的包装类,即使servlet程序要类型上溯,那么也只能上溯到我们的包装类。这样对HttpRequest和HttpResponse就起到了保护作用。下面是这两个类的实现:

public class RequestFacade implements ServletRequest{

private HttpRequest request = null;

public RequestFacade(HttpRequest request){

this.request = request;

}

public String getAuthType() {

return request.getAuthType();

}

public String getContextPath() {

return request.getContextPath();

}

//以下省略了其他方法,因为其他方法的实现和这个实现类似,只是简单调用了request的相//应方法

......

}

public class ResponseFacade implements ServletResponse{

private HttpResponse response = null;

public ResponseFacade(HttpResponse response){

this.response = response;

}

public void addCookie(Cookie arg0) {

response.addCookie(arg0);

}

//以下省略了其他方法,因为其他方法的实现和这个实现类似,只是简单调用了request的相//应方法

......

}

1.1.2.1. 运行简单服务器

双击随书光盘中的chapter/execixe1/classes/client目录下的startup.bat文件,然后启动浏览器输入http://localhost:8080/servlet/TestServlet。就会出现如下效果:



1.1.3. 小结


2011-03-30 15:20
概览

容器是一个组件,这个组件为Servlet处理请求并为客户端呈现响应,容器接口位于org.apache.catalina.Container。一个有四种类型的容器:(引擎)Engine、(主机)Host、(上下文)Context、(包装器)Wrapper,这章将讲述上下文(Context)、包装器(Wrapper),而将引擎和主机留在第十三章讲述。这章开始讨论容器接口(Container),接着讲述容器饿pipeline机制。然后看看上下文(Context)、包装器(Wrapper)接口,这章包括两个应用:简单包装器(Wrapper)和简单上下文(Context)。

Container接口


容器必须实现org.apache.catalina.Container接口。正想你在第四章看到的,为了在连接器(Connector)中调用容器(Container)的invoke方法,必须传递一个Container实例到连接器(Connector)的setContainer方法来设置连接器对容器的引用。回忆第四章的bootStrap类的如下代码:

HttpConnector connector = new HttpConnector();

SimpleContainer container = new SimpleContainer();

connector.setContainer(container);

第一要注意的就是在Catalina中,有四种不同概念层次的容器:

● 引擎(Engine),是整个Catalina Servlet引擎。

● 主机(Host),包含一定数量上下文(Context)的虚拟主机

● 上下文(Context),代表一个web应用。上下文包含一个或多个包装器(Wrapper)。

、 ● 包装器(Wrapper),代表一个单独的Servlet。

上面的每种不同概念层次的容器在org.apache.catalina包中有相应的接口,他们分别是:Engine、Host、Context和Wrapper,四种接口都扩展了Container接口。四种容器的标准实现是StandardEngine、StandardHost、StandardContext和StandardWrapper,他们都位于org.apahce.catalina.core包内。

下面图表展示了Container接口和它的子接口及实现的关系。

一个基础的Catalina部署并不总是一直需要四种类型的容器。例如,这章的第一个应用的容器模块只有一个包装器(Wrapper);第二个应用的容器模块包含一个上下文(Context)和一个包装器(Wrapper);两个应用都没有用到虚拟主机(Host)和引擎(Engine)。

一个容器有零个或多个低层次的子容器。例如,一个上下文(Context)通常有一个或者多个包装器,一个主机可以有零个或者多个上下文。然而,一个包装器是最低层次的容器,它不能包含子容器。将一个子容器增加到父容器中,可以使用Container接口的addChild方法,其签名如下:

public void addChild(Container child);

从一个容器产出子容器,使用removeContainer方法,这个方法签名如下。

public void removeChild(Container child);

另外,容器接口支持通过findChild和findChilren对一个子容器或者多个子容器的查询。它们的方法签名如下:

public Container findChild(String name);

public Container[] findChildren();

一个容器可以包含一些支持的组件,比如,加载器(Loader),日志记录器(Logger),管理器(Manager)、安全域(Realm)和资源(Resources)。我们将在后面的章节中讨论这些组件。重要的是,容器提供set和get方法来将这些组件和容器关联起来。这些方法包括:setLoader和getLoader,setLogger和getLogger,setManager和getManager,setRealm和getRealm, getResources和setResources。

更有意思的是,容器接口被设计成在tocmat部署的时候,一个tocmat管理员可以通过编辑配置文件(server.xml)决定执行那个容器。这是通过pipeline任务和一系列阀来达到的。下面我们将讨论pipeline任务。

注释:在tomcat4中的容器和tomcat5的容器稍有不同,例如tomcat4中有map方法,但是tomcat5中取消了此方法

2011-03-30 15:21

Pipeline任务


这节阐述当连接器调用容器的invoke方法时,invoke方法做了什么工作。然后这节,讨论四个相关接口:Pipeline,Valve,ValveContext和Contained。他们位于org.apache.catalina包中。

一个pipeline包含容器将要执行的任务,一个阀(Valve)代表一个特定任务。在pipeline中只有一个基础阀(basic Valve),但是你可以增加任意多的其他阀,有意思的是阀可以通过编辑tomcat的配置文件(server.xml)来动态增加阀。

如果你理解了Servlet过滤器(filter),那么你不难理解pipeline和阀是如何工作的。一个pipeline象一个过滤器链,而阀则象过滤器。一个阀可以维护传递给它的请求(request)和响应(response)对象。当一个阀完成处理,它调用pipeline的下一个阀,而基础阀(basic valve)总是在最后调用。

一个容器可以有一个pipeline。当一个容器的invoke方法被调用时,容器将处理传递给pipeline,pipeline执行它的第一个阀,然后将继续执行下面的阀,直到pipeline的最后一个阀执行完毕。你可能想,你可以想pipeline的invoke方法代码如下:

// invoke each valve added to the pipeline

for (int n=0; n<valves.length; n++) {

valve
.invoke( ... );

}

// then, invoke the basic valve

basicValve.invoke( ... );

然而,tomcat设计者采用不同的方法。下面通过介绍ValveContext,你将了解它如何工作。

当连接器调用容器的invoke方法时,容器没有使用硬编码要执行的任务,而是容器调用了pipeline的invoke方法。Pipeline接口的invoke方法有如下签名,和容器的invoke方法的签名完全一样。

public void invoke(Request request, Response response)

throws IOException, ServletException;

下面是Container接口的invoke方法的实现,它位于org.apache.catalina.core.ContainerBasel类中。

public void invoke(Request request, Response response)

throws IOException, ServletException {

pipeline.invoke(request, response);

}

其中pipeline是容器内Pipeline接口的一个实例。

现在,pipeline必须确定它里面的所有的阀和基础阀都必须被执行一次。这是通过创建ValveContext接口实现的。为了ValveContext可以访问所有pipeline的成员,ValveContext作为Pipeline的内部类实现的。ValveContext中最重要的方法是invokeNext:

public void invokeNext(Request request, Response response)

throws IOException, ServletException

当ValveContext创建后,pipeline调用ValveContext的invoke方法。ValveContext首先执pipeline的第一个阀。并且在第一个阀做工作前,它执行下一个阀。为了阀可以执行 invokeNext方法,ValveContext要传递自己到每个阀中。下面是阀(Valve)接口的invoke方法的签名:

public void invoke(Request request, Response response,

ValveContext ValveContext) throws IOException, ServletException

阀的实现有点类似下面的代码:

public void invoke(Request request, Response response,

ValveContext valveContext) throws IOException, ServletException {

// Pass the request and response on to the next valve in our pipeline

valveContext.invokeNext(request, response);

// now perform what this valve is supposed to do

...

}

Org.apache.catalina.core.StandardPipeline类是所有容器的pipeline的实现。在tomcat4中,这个类有个内部类叫做StandardPipelineValveContext,它是ValveContext的实现类。下面是StandardPipelineValueContext类。

protected class StandardPipelineValveContext implements ValveContext {

protected int stage = 0;

public String getInfo() {

return info;

}

public void invokeNext(Request request, Response response)

throws IOException, ServletException {

int subscript = stage;

stage = stage + 1;

// Invoke the requested Valve for the current request thread

if (subscript < valves.length) {

valves[subscript].invoke(request, response, this);

}

else if ((subscript == valves.length) && (basic != null)) {

basic.invoke(request, response, this);

}

else {

throw new ServletException

(sm.getString("standardPipeline.noValve"));

}

}

}

tomcat容器(三)
2011-03-30 15:22

InvokeNext方法使用subscript和stage来记录将要执行哪个阀。当pipeline的invoke方法执行第一个invoke时,subscript值是0,stage值是1。因此,第一个阀被执行(索引subscript是0),pipeline的第一个阀接受一个ValveContext实例并且执行它的invokeNext方法。这时,subscript值是1,所以第二个阀被执行等等。

当最后的阀的invokeNexte被调用时,subscript的值等于阀的数量,结果基础阀(basic value)被执行。

在tomcat5中,删除了StandardPipeline的StandardPipelineValveContext类,取代的是org.apache.catalina.core.StandardValveContext类,下面是代码:

package org.apache.catalina.core;

import java.io.IOException;

import javax.servlet.ServletException;

import org.apache.catalina.Request;

import org.apache.catalina.Response;

import org.apache.catalina.Valve;

import org.apache.catalina.ValveContext;

import org.apache.catalina.util.StringManager;

public final class StandardValveContext implements ValveContext {

protected static StringManager sm =

StringManager.getManager(Constants.Package);

protected String info =

"org.apache.catalina.core.StandardValveContext/1.0";

protected int stage = 0;

protected Valve basic = null;

protected Valve valves[] = null;

public String getInfo() {

return info;

}

public final void invokeNext(Request request, Response response)

throws IOException, ServletException {

int subscript = stage;

stage = stage + 1;

// Invoke the requested Valve for the current request thread

if (subscript < valves.length) {

valves[subscript].invoke(request, response, this);

}

else if ((subscript == valves.length) && (basic != null)) {

basic.invoke(request, response, this);

}

else {

throw new ServletException

(sm.getString("standardPipeline.noValve"));

}

}

void set(Valve basic, Valve valves[]) {

stage = 0;

this.basic = basic;

this.valves = valves;

}

}

你可以看到tomcat4和tomcat5的上述类的相似之处吗?

我们将更详细阐述Pipeline、Valve、ValveContext接口,也将讨论阀通常实现的接口org.apache.catalina.Contained。

Pipeline接口


我们提过,Pipeline的第一个方法是invoke方法,这个方法将调用启动pipeline的阀和基础阀。Pipeline接口允许你通过addValve方法来增加一个新的阀,也可以通过removeValve方法删除一个阀。你可以使用setBasic方法来给pipeline分配一个基础阀。基础阀最后执行,负责处理请求和相应的响应。Pipeline接口如下:

package org.apache.catalina;

import java.io.IOException;

import javax.servlet.ServletException;

public interface Pipeline {

public Valve getBasic();

public void setBasic(Valve valve);

public void addValve(Valve valve);

public Valve[] getValves();

public void invoke(Request request, Response response)

throws IOException, ServletException;

public void removeValve(Valve valve);

}

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: