[How Tomcat Works]第1章 一个简单的Web服务器

译者 jarfield

博客 http://jarfield.javaeye.com      

    本章解释了Java Web 服务器是如何工作的。Web 服务器又被称为超文本传输协议(Hypertext Transport  Protocol , HTTP )服务器,因为它和客户端(通常是浏览器)使用HTTP 协议进行通信。基于Java 开发的Web 服务器都使用到两个重要的类 :java.net.Socketjava.net.ServerSocket , 并通过HTTP 消息完成通信。因此,本章的开头就开始讨论HTTP 和这两个类。然后,继续介绍本章附带的应用程序。

超文本传输协议(HTTP

    HTTP 协议,允许Web 服务器和浏览器在Internet 上发送和接受数据。HTTP 是一种基于“请求-响应”模式的协议。客户端请求一个文件(file ),服务器针对该请求给出响应。HTTP 使用可靠的TCP 连接——默认端口是80 。HTTP的最初版本是HTTP/0.9 ,后来被HTTP/1.0 重写。HTTP/1.0 的替代者是当前的HTTP/1.1HTTP/1.1 定义在RFC 2612 中,可以从 http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf 下载。


    提示:本节只是简短地介绍HTTP ,目的是帮助你理解Web 服务器发送的HTTP 消息。如果你想更深入得了解HTTP ,可以读读RFC 2616

    HTTP 的通信总是由客户端主动初始化:建立连接并发送HTTP 请求。Web 服务器从来不主动联系(contact )客户端,或者建立到客户端的回调(callback )连接。无论客户端还是服务器,都可以随时(prematurely )中断连接。例如,当你在下载文件时,点击浏览器的“停止”按钮,就关闭了浏览器和服务器之间的HTTP 连接。

HTTP 请求

    HTTP 请求包含3 个组成部分:

  • Method-Uniform Resource Identifier (URI)-Protocol/Version
  • Request headers (请求头部)
  • Entity body (实体主体)

    下面是HTTP 请求的一个例子:

Html代码 复制代码
  1. POST /examples/default.jsp HTTP/1.1   
  2. Accept: text/plain; text/html   
  3. Accept-Language: en-gb   
  4. Connection: Keep-Alive   
  5. Host: localhost   
  6. User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)   
  7. Content-Length: 33   
  8. Content-Type: application/x-www-form-urlencoded   
  9. Accept-Encoding: gzip, deflate   
  10.   
  11. lastName=Franks&firstName=Michael   
    POST /examples/default.jsp HTTP/1.1
    Accept: text/plain; text/html
    Accept-Language: en-gb
    Connection: Keep-Alive
    Host: localhost
    User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
    Content-Length: 33
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip, deflate
 
    lastName=Franks&firstName=Michael 

 

    Method-URI-Protocol/Version 是请求的第一行

Html代码 复制代码
  1. POST /examples/default.jsp HTTP/1.1   
POST /examples/default.jsp HTTP/1.1 

 

    POSTMethod/examples/default.jspURIHTTP/1.1 就是Protocol/Version

    每个HTTP 请求都可以使用HTTP 标准中众多Method 中的一个。HTTP/1.1 共支持7Method : GET , POST , HEAD , OPTIONS , PUT , DELETETRACEGETPOST 是互联网应用使用最普遍的Method

     URI 标识了互联网上的资源。URI 的解析通常都是相对与服务器根目录的。因此,URI 总是从正斜线/ 开始。统一资源定位器(Uniform Resource Locator , URL )实际上是一种URI (参见 http://www.ietf.org/rfc/rfc2396.txt )。Protocol version 表示使用了哪个版本的HTTP 协议。

    Request header 包含了关于客户端环境和entity body的有用信息 。例如,headers 可能包括浏览器的语言,entity body 的长度等等。Header 之间通过回车/换行符(CRLF )分隔。

    在headersentity body 之间,是一个空行(CRLF )。这个CRLF 对于HTTP 请求内容的格式是相当重要的,它告诉HTTP 服务器:entify body 从哪开始。在一些介绍互联网编程的书中,该CRLF 被认为是HTTP 请求的第4 个组成部分。

    在前面的HTTP 请求中,entify body 仅仅只有这一行:

Html代码 复制代码
  1. lastName=Franks&firstName=Michael   
    lastName=Franks&firstName=Michael 


    这里只是一个例子,实际的HTTP 请求中,entity body 当然可以更长一些。

HTTP 响应

    和HTTP请求一样,HTTP 响应也包含3 个组成部分:

  • Protocol—Status code—Description  
  • Response headers (响应头部)
  • Entity body (实体主体)

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

Html代码 复制代码
  1. HTTP/1.1 200 OK   
  2. Server: Microsoft-IIS/4.0   
  3. Date: Mon, 5 Jan 2004 13:13:33 GMT   
  4. Content-Type: text/html   
  5. Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT   
  6. Content-Length: 112   
  7.   
  8. <html>  
  9. <head>  
  10. <title>HTTP Response Example</title>  
  11. </head>  
  12. <body>  
  13. Welcome to Brainy Software   
  14. </body>  
  15. </html>   
    HTTP/1.1 200 OK
    Server: Microsoft-IIS/4.0
    Date: Mon, 5 Jan 2004 13:13:33 GMT
    Content-Type: text/html
    Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
    Content-Length: 112
 
    <html>
    <head>
    <title>HTTP Response Example</title>
    </head>
    <body>
    Welcome to Brainy Software
    </body>
    </html> 

 

    HTTP 响应的第一行类似于HTTP 请求的第一行。第一行告诉你:使用的HTTP 版本是HTTP/1.1 ,请求处理成功了(200 = 成功),一切运行正常。

    响应headers 和请求headers 类似,包含了很多有用的信息。响应数据中的entity body 是响应本身的HTML 内容。Headersentity body 之间通过CRLF 分隔。

Socket

    套接字是网络连接的一个端点(endpoint )。应用程序使用套接字从网络上读取数据、向网络写入数据。两台不同机器上的应用,在同一个连接上以字节流的格式向(从)对方发送(接收)数据。 为了向一个应用发送消息,你需要知道该应用的IP 地址和端口。在Java 中,套接字用 java.net.Socket 类来表示。

    你可以使用 Socket 类众多构造函数中的一个来创建 套接字 对象。其中一个构造函数接收主机名和端口作为参数:

Java代码 复制代码
  1. public Socket (java.lang.String host, int port)  
public Socket (java.lang.String host, int port)


    其中,host 是远程机器的名称或IP 地址,port 是远程应用的端口号。例如,要连接上 yahoo.com80 端口,你可以创建下面的 Socket 对象:

Java代码 复制代码
  1. new Socket("yahoo.com"80);   
new Socket("yahoo.com", 80); 


    只要你成功创建了 Socket 的一个实例,就可以使用它发送和读取字节流。如果要发送字节流,你可以调用 Socket 类的 getOutputStream 方法获取一个 java.io.OutputStream 对象。如果要发送文本信息,你可以将上述 java.io.OutputStream 对象包装成一个 java.io.PrintWriter 对象。如果要读取字节流,你可以调用Socket 类的 getInputStream 方法获取一个 java.io.InputStream 对象

    下面的代码片段创建了一个能够与本地HTTP 服务器(127.0.0.1 表示本地主机)通信的 Socket 对象,发送了一个HTTP 请求,并 从服务器 接收了HTTP 响应。另外,这段代码还创建了一个 StringBuffer 对象来存储响应数据,并将其打印在控制台上。

Java代码 复制代码
  1. Socket socket = new Socket("127.0.0.1""8080");   
  2. OutputStream os = socket.getOutputStream();   
  3. boolean autoflush = true;   
  4. PrintWriter out = new PrintWriter(socket.getOutputStream(), autoflush);   
  5. BufferedReader in = new BufferedReader(new InputStreamReader( socket.getInputstream() ));   
  6.   
  7. // send an HTTP request to the web server   
  8. out.println("GET /index.jsp HTTP/1.1");   
  9. out.println("Host: localhost:8080");   
  10.   
  11. out.println("Connection: Close");   
  12. out.println();   
  13.   
  14. // read the response   
  15. boolean loop = true;   
  16. StringBuffer sb = new StringBuffer(8096);   
  17. while (loop) {   
  18.   if ( in.ready() ) {   
  19.     int i=0;   
  20.     while (i!=-1) {   
  21.       i = in.read();   
  22.       sb.append((char) i);   
  23.     }   
  24.     loop = false;   
  25.   }   
  26.   Thread.currentThread().sleep(50);   
  27. }   
  28.   
  29. // display the response to the out console   
  30. System.out.println(sb.toString());   
  31. socket.close();  
    Socket socket = new Socket("127.0.0.1", "8080");
    OutputStream os = socket.getOutputStream();
    boolean autoflush = true;
    PrintWriter out = new PrintWriter(socket.getOutputStream(), autoflush);
    BufferedReader in = new BufferedReader(new InputStreamReader( socket.getInputstream() ));
 
    // send an HTTP request to the web server
    out.println("GET /index.jsp HTTP/1.1");
    out.println("Host: localhost:8080");
 
    out.println("Connection: Close");
    out.println();
 
    // read the response
    boolean loop = true;
    StringBuffer sb = new StringBuffer(8096);
    while (loop) {
      if ( in.ready() ) {
        int i=0;
        while (i!=-1) {
          i = in.read();
          sb.append((char) i);
        }
        loop = false;
      }
      Thread.currentThread().sleep(50);
    }
 
    // display the response to the out console
    System.out.println(sb.toString());
    socket.close();


    需要注意的是,为了从Web 服务器得到恰当的响应,你发送的HTTP 请求必须遵守HTTP 协议。如果你读了 前一节,超文本传输协议(HTTP ),应该就会理解上面代码中的HTTP 请求。

    提示:你可以使用本书源代码中的com.brainysoftware.pyrmont.util.HttpSniffer 类发送HTTP 请求和显示HTTP 响应。为了使用这个Java 程序,你必须连接到Internet 。不过,提醒一句,如果你在防火墙后面,那么这个类可能不能正常工作。

ServerSocket

    前面介绍的Socket 类,代表的是客户端套接字,即当你为了连接到远程服务程序而创建的 套接字 对象。现在,如果你想要实现一个服务器程序,比如HTTP 服务器或FTP 服务器,你需要一种不同的做法。因为,服务器程序必须一直驻守,它不知道客户端何时会连接过来。为了使你的程序能够驻守,你需要使用 java.net.ServerSocket 类。这是服务器端socket 的一个实现类。

    ServerSocket 类和 Socket 类并不相同。服务器套接字的职责是等待来自客户端的连接请求。当服务器套接字收到一个连接请求后,创建一个 Socket 对象来与客户端通信。 为了创建服务器套接字,你需要使用 ServerSocket 类提供的4 个构造函数之一。你需要指定服务器套接字将要监听的IP 地址和端口。通常,IP 地址是127.0.0.1 ,表示服务器套接字将监听本地机器。服务器套接字监听的IP 地址被称为绑定地址(binding address )。服务器套接字的另一个重要属性是backlog ,服务器套接字有一个保存尚未处理的连接请求的队列,backlog 就是该队列的的最大长度。如果达到最大长度,服务器套接字将拒绝新的连接请求。

    下面是 ServerSocket 类的一个构造函数原型:

Java代码 复制代码
  1. public ServerSocket(int port, int backLog, InetAddress bindingAddress);   
public ServerSocket(int port, int backLog, InetAddress bindingAddress); 


    注意这个构造函数,binding address 必须是一个 java.net.InetAddress 对象。创建 InetAddress 对象的一个简单方法就是调用该类的静态方法 getByName ,并把主机名作为 String 对象传给该方法,就像下面这样:

Java代码 复制代码
  1. InetAddress.getByName("127.0.0.1");   
InetAddress.getByName("127.0.0.1"); 


    下面这行代码创建了一个监听本地8080 端口的、backlog1ServerSocket 对象。

Java代码 复制代码
  1. new ServerSocket(80801, InetAddress.getByName("127.0.0.1"));  
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));

 

    有了ServerSocket 对象后,你就可以告诉它:在指定的端口上监听绑定地址的连接请求吧。告诉的办法就是调用 ServerSocketaccept 方法。当有一个连接请求到达时,该方法就会返回,返回值是一个 Socket 对象。这个Socket 对象就像 前一节, “Socket 类”,描述的那样,可以用来向(从)客户端发送(读取)数据。实际上,accept 方法也是本章应用程序唯一使用的( ServerSocket 类的)方法。

应用程序

    本章的应用程序是一个Web服务器程序,放在 ex01.pyrmont 包中,由3 个类组成:

  • HttpServer
  • Request
  • Response  

    本章应用程序的入口(静态的 main 方法)在 HttpServer 类中。 main 方法创建了一个 HttpServer 对象,并调用了它的 await 方法。人如其名, await 方法在指定端口上等待HTTP 请求,然后处理HTTP 请求,最后将HTTP 响应发送回客户端。而且, await 方法保持等待,只有接收到 shutdown 命令,才退出运行。该应用只能发送静态资源,诸如特性目录下的HTTP 文件和图像文件。同时,还在控制台上显示HTTP 请求的字节流。然而,该应用不向浏览器发送任何headerdatecookies 等)。

    下面各小节,我们将会看一看这3 个类。

HttpServer

    HttpServer 类表示了一个Web 服务器,代码在 Listing 1.1 中。需要注意的是,为了节省篇幅,await 方法没被列在 Listing 1.1 中,可以在 Listing 1.2 中找到。

Listing 1.1: The HttpServer class    

Java代码 复制代码
  1. package ex01.pyrmont;   
  2.     
  3. import java.net.Socket;   
  4. import java.net.ServerSocket;   
  5. import java.net.InetAddress;   
  6. import java.io.InputStream;   
  7. import java.io.OutputStream;   
  8. import java.io.IOException;   
  9. import java.io.File;   
  10.     
  11. public class HttpServer {   
  12.     
  13.   /** WEB_ROOT is the directory where our HTML and other files reside.  
  14.    *  For this package, WEB_ROOT is the "webroot" directory under the  
  15.    *  working directory.  
  16.    *  The working directory is the location in the file system  
  17.    *  from where the java command was invoked.  
  18.    */  
  19.   public static final String WEB_ROOT =   
  20.     System.getProperty("user.dir") + File.separator  + "webroot";   
  21.     
  22.   // shutdown command   
  23.   private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";   
  24.     
  25.   // the shutdown command received   
  26.   private boolean shutdown = false;   
  27.     
  28.   public static void main(String[] args) {   
  29.     HttpServer server = new HttpServer();     
  30.     server.await();   
  31.   }   
  32.     
  33.   public void await() {   
  34.     ...   
  35.   }   
  36. }   
package ex01.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;
import java.io.File;
 
public class HttpServer {
 
  /** 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.
   */
  public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator  + "webroot";
 
  // shutdown command
  private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
 
  // the shutdown command received
  private boolean shutdown = false;
 
  public static void main(String[] args) {
    HttpServer server = new HttpServer();  
    server.await();
  }
 
  public void await() {
    ...
  }
} 

 
Listing 1.2: The HttpServer class's await method

Java代码 复制代码
  1. public void await() {   
  2.   ServerSocket serverSocket = null;   
  3.   int port = 8080;   
  4.   try {   
  5.     serverSocket =  new ServerSocket(port, 1,   
  6.       InetAddress.getByName("127.0.0.1"));   
  7.   }   
  8.   catch (IOException e) {   
  9.     e.printStackTrace();   
  10.     System.exit(1);   
  11.   }   
  12.   // Loop waiting for a request   
  13.   while (!shutdown) {   
  14.     Socket socket = null;   
  15.     InputStream input = null;   
  16.     OutputStream output = null;   
  17.     
  18.     try {   
  19.       socket = serverSocket.accept();   
  20.       input = socket.getInputStream();   
  21.       output = socket.getOutputStream();   
  22.       // create Request object and parse   
  23.       Request request = new Request(input);   
  24.       request.parse();   
  25.     
  26.       // create Response object   
  27.       Response response = new Response(output);   
  28.       response.setRequest(request);   
  29.       response.sendStaticResource();   
  30.     
  31.       // Close the socket   
  32.       socket.close();     
  33.       //check if the previous URI is a shutdown command   
  34.       shutdown = request.getUri().equals(SHUTDOWN_COMMAND);   
  35.     }   
  36.     catch (Exception e) {   
  37.       e.printStackTrace ();   
  38.       continue;   
  39.     }   
  40.   }   
  41. }  
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);
      response.sendStaticResource();
 
      // 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 ();
      continue;
    }
  }
}

 

    这个Web 服务器可以提供静态资源服务,可访问的资源位于 public static final WEB_ROOT 表示的 目录及子目录下。WEB_ROOT 是这样初始化的:

Java代码 复制代码
  1. public static final String WEB_ROOT =  System.getProperty("user.dir") + File.separator + "webroot";  
public static final String WEB_ROOT =  System.getProperty("user.dir") + File.separator + "webroot";


    这段代码中有一个名为webroot 的目录,该目录包含了可以用于测试该应用的的静态资源。你还可以从该目录下找到几个用于测试下一章应用的servlet

    如果要请求一个静态资源,你可以在浏览器的地址栏中敲入以下URL

Html代码 复制代码
  1. http://machineName:port/staticResource   
    http://machineName:port/staticResource 


    如果你从另一台机器上发送请求, machineName 应该是该应用所在机器的主机名或IP 地址。如果你的浏览器运行在同一台机器上,可以使用 localhost 作为 machineName 。端口是8080staticResource 是被请求的文件(静态资源)名,该文件必须位于 WEB_ROOT 下。

    例如,你在同一台机器上测试该应用,想让HttpServer 发送文件index.html ,你可以使用下面的URL :  

    如果要停止服务器,你可以通过特定的URL从浏览器发送shutdown命令:host:port的后面加上预先定义的、表示shutdown的字符串即可。 HttpServer 类的静态常量 SHUTDOWN 定义了 shutdown命令:

Java代码 复制代码
  1. private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";  
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";


    因此,如果要停止服务器,你可以使用下面的URL

Html代码 复制代码
  1. http://localhost:8080/SHUTDOWN   
    http://localhost:8080/SHUTDOWN 


    现在,我们看一看 Listing 1.2 中的await 方法。
   
    方法名使用await 而不用wait ,是因为waitjava.lang.Object 类中一个重要的、与多线程紧密相关的方法。

    await 方法首先创建了一个ServerSocket 对象,然后进入一个 while 循环。

Java代码 复制代码
  1. serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));   
  2. ...   
  3. // Loop waiting for a request   
  4.   
  5. while (!shutdown) {   
  6.   ...   
  7. }  
    serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
    ...
    // Loop waiting for a request
 
    while (!shutdown) {
      ...
    }


    while 循环中的代码运行到 ServerSocketaccept 方法就停止下来,直到在8080 端口上收到HTTP 请求才返回:

Java代码 复制代码
  1. socket = serverSocket.accept();  
socket = serverSocket.accept();

 

    收到一个请求后, await 方法从 accept 方法返回的 Socket 对象中获取了 java.io.InputStream 对象和 java.io.OutputStream 对象。

Java代码 复制代码
  1. input = socket.getInputStream();   
  2. output = socket.getOutputStream();   
input = socket.getInputStream();
output = socket.getOutputStream(); 

 

    await 方法然后创建了一个 ex01.pyrmont.Request 对象,并调用它的 parse 方法来解析HTTP请求的原始数据(raw data )。

Java代码 复制代码
  1. // create Request object and parse   
  2. Request request = new Request(input);   
  3. request.parse ();   
    // create Request object and parse
    Request request = new Request(input);
    request.parse (); 


    之后, await 方法创建了一个 Response 对象,将上面的 Request 对象设置成 Response 对象的成员,并调用 Response 对象的 sendStaticResponse 方法。

Java代码 复制代码
  1. // create Response object   
  2.  Response response = new Response(output);   
  3.  response.setRequest(request);   
  4.  response.sendStaticResource();  
   // create Response object
    Response response = new Response(output);
    response.setRequest(request);
    response.sendStaticResource();

 

    最后, await 方法关闭了 Socket 对象,并调用了 Request 对象的 getUri 方法,检查本次HTTP 请求的URI 似乎否是shutdown 命令。如果是(shutdown 命令)的话, shutdown 变量会被设置成 true ,从而程序将退出 while 循环。

Java代码 复制代码
  1. // Close the socket   
  2. socket.close ();   
  3.   
  4. //check if the previous URI is a shutdown command   
  5. shutdown = request.getUri().equals(SHUTDOWN_COMMAND);   
    // Close the socket
    socket
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值