How tomcat works——1 一个简单的Web Server

本章讲解了Java Web Server是如果工作的。一个Web Server也被叫着HTTP(Hypertext Transfer Protocol) Server,因为它使用HTTP协议和客户端通讯——客户端通常是WEB浏览器。一个最基本的Java Web Server会用到二个重要的Class:java.net.Socket和java.net.ServerSocket,通过HTTP信息通信。因此,本章以讨论HTTP协议和二个Class开始。然后,继续讲解本章简单的Web Server应用Demo。

1.1 Hypertext Transfer Protocol (HTTP)协议

HTTP是一种允许Web Server和浏览器在互联网间发送和接收数据的协议。它是请求和应答协议。客户端请求一个文件,服务器响应该请求。HTTP默认连接使用可靠的TCP协议80端口。HTTP第一个版本是HTTP/0.9,续而被HTTP/1.0重写。替换HTTP/1.0的是当前版本HTTP/1.1,它被规定为RFC(Request for Comments)2616,可以从这里下载:http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf

注意:本章节只是简要的介绍HTTP 1.1,目的是帮助你理解Web Server上的传信息。如果对更多细节感兴趣,你可以阅读RFC2616。

在HTTP中,通常是一个客户端通过界定的事务连接发送一HTTP请求。Web服务器别无选择的和客户端接头,或者为客户端创建一回调连接。客户端和服务器均可以贸然地中断连接。比如:你点击浏览器上关闭按钮来中断一个文件的下载,实际上有效地中断了和Web服务器间的HTTP连接。

1.1.1 HTTP请求

一HTTP请求,有3部分组成:

1>请求方法—请求地址—协议/版本(Method—Uniform Resource Identifier (URI)—Protocol/Version) 
2>.请求头部(Request headers) 
3>请求实体内容(Entity body)

如下是一个HTTP请求例子:

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的版本:POST /examples/default.jsp HTTP/1.1。其中POST是请求方法;/examples/default.jsp是请求URI;HTTP/1.1是协议版本。

每一个HTTP请求,均可以使用HTTP协议标准中多个请求方法之一。HTTP 1.1支持7种:GET、POST、HEAD、OPTIONS、PUT、DELETE和TRACE。其中,GET和POST在Internet应用中使用最普遍。

URI指定一Internet中资源完整路径。URI通常代表中服务的Root根目录,因此它总是以/开始。URL实际上是URI的一种类型(参阅:http://www.ietf.org/rfc/rfc2396.txt )。协议版本表示当前使用的HTTP协议版本。

请求头部包含些有用的信息:客户端环境和请求实体(entity body)。比如:浏览器设置的语言、请求实体的长度,等等。每一请求头部都被换行符隔离开(Each header is separated by a carriage return/linefeed (CRLF) sequence)。

在请求头部和请求实体之间,有一空白行(a blank line (CRLF)),这是HTTP请求的重要格式。CRLF已在告诉HTTP服务器,这是请求体的开始。在一些Internet编程书籍中,把CRLF当做HTTP请求的第四部分。

在上面的请求中,请求体是很简单的:lastName=Franks&firstName=Michael。通常典型的HTTP请求中请求体还是比较长的。

1.1.2 HTTP响应

和HTTP请求类似,HTTP响应也包含3部分:

1> 响应状态(Protocol—Status code—Description) 
2> 响应头部(Response headers) 
3> 响应实体(Entity body)

下面是一HTTP响应例子:

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 Exampletitle> 
head> 
<body> 
Welcome to Brainy Software 
body> 
html>

响应头部第一行和请求头部第一行是相似的。第一行告诉你:用的是HTTP 1.1,请求成功(200表示成功)。

响应头部像请求头部一样包含中有用的信息。响应实体中是HTML内容。响应实体和响应头部之间是被换行符分割的。

1.2 Socket类

“套接字”是一种端点式网络连接。“套接字”在网络中可以提供读和写的功能。在2台计算机之间2个应用通过连接可以彼此发送和接收字节流进行通信。从一个应用发送信息给另一应用,你需要知道另应用所在机器的IP和端口(Port)。在java中,“套接字”是通过java.net.Socket展现的。

可以使用Socket类中其中一个构造方法来创建Socket。构造函数接收主机名称和端口号: 
public Socket (java.lang.String host, int port)

其中,host是远程机器的名称或IP地址,port指远程应用的端口号。例如:要连接yahoo.com的80端口,你可以创建如下的Socket对象: 
new Socket (“yahoo.com”, 80);

当你成功创建了Socket类实例后,你可以使用它发送和接收字节流。发送字节流,你必须先得通过调用Socke类的getOutputStream()来获得一个java.io.OutputStream对象。发送文本信息给远程应用,你通常得通过OutputStream对象返回构造一java.io.PrintWriter对象。从连接的另一端接收字节流,你得调用Socket类的getInputStream()方法来获得一java.io.InputStream.对象。

如下代码片段构造的Socket,可以和本地的HTTP服务通讯:发送一HTTP请求,并从服务器获得应答响应。创建了一StringBuffer对象来组装响应信息,并把它打印到控制台:

  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 Server获得正确的响应,你得先发送一遵循HTTP协议的HTTP请求。如果你阅读了前面一节“HTTP协议”,你应该可以理解上面代码中的HTTP请求。

现在,你可以使用本书中的com.brainysoftware.pyrmont.util.HttpSniffer发送HTTP请求,并显示请求应答信息。使用这个java程序,你必须得联网。另外,还有可能不可以运行成功运行此程序,如果你开启了防火墙。

1.3 ServerSocket类

Socket类代表着客户端套接字。例如,每当你想连接远程服务应用时创建的Socket。现在如果你想实现一类似HTTP Server或FTP Server的服务器应用,你需要使用不同于创建Socket的方式。这是因为服务应用不知道客户端什么时候会连接它,所以它不得不一直保持运行状态(不断的监听)。介于此,你需要使用java.net.ServerSocket类。这代表着服务端“套接字”。

ServerSocket不同于Socket。ServerSocket扮演着等待客户端发起请求连接的角色。一旦ServerSocket获得一连接请求,它就创建一Socket实例去处理和该客户端的交互。

创建一ServerSocket,你需要使用ServerSocket类提供的4个构造方法中任何一个。你需要指定要监听的IP地址和端口号。典型的,IP地址是127.0.0.1,意味着此服务应用监听着本机器。The IP address the server socket is listening on is referred to as the binding address. ServerSocket另一重要属性“backlog”,它代表着可支持的最大请求数,当超过时,ServerSocket将拒绝接收请求。

ServerSocket类的一构造方法如下:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);

注意这个构造函数,bindingAddress必须是java.net.InetAddress的实例。通过调用getByName静态方法,很容易创建InetAddress对象,如下代码:

InetAddress.getByName("127.0.0.1");
  • 1

如下代码中构造的ServerSocket监听着本地机器80端口,属性backlog等于1:

new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));

当你拥有ServerSocket实例时,你可以让它等待连接请求——来自指定IP地址和端口号监听处。你只要调用ServerSocket类的accept()方法即可。此方法只会在有连接请求时才会返回,返回一Socket实例。此Socket对象实例可用来发送和接收来自客户端应用的字节流,前一节“Socket类”已介绍过。实际上,本章应用Demo也仅使用了accept()方法。

1.4 应用Demo

本章web Sever应用位于ex01.pyrmont package下,它包含3个类: 
1》 HttpServer 
2》 Request 
3》 Response

本应用的入口处是HttpSever类的main()方法。此main()方法创建了一HttpServer实例,并调用了它的await()方法。从字面意思看,await()方法在指定端口处等待HTTP请求,处理它们,返回给客户端一个请求响应。它一直不断地监听等待直到收到shutdown命令。

本应用不可以处理除指定文件路径目录下HTML文件、图片文件之外的静态资源。它在控制台上显示了来自HTTP字节流信息请求。然而,它没有返回诸如日期或cookie给浏览器。

现在,我们在如下的小节中学习这3个类。

1.4.1 HttpServer类

HttpServer类表示一个Web Server。如下代码介绍:

package ex01.pyrmont;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author : Ares
 * @createTime : Aug 21, 2012 9:45:01 PM
 * @version : 1.0
 * @description :
 */
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() {
        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 Server可以服务于public static final WEB_ROOT指定目录或其子目录路径下静态资源请求。WEB_ROOT初始化如下:

public static final String WEB_ROOT = System.getProperty("user.dir")
            + File.separator + "webroot";
  • 1
  • 2

代码中包含一叫webroot目录,它里面含有一些可以用来测试本应用的静态资源。在后续章节中,你也会发现此目录还含有一些servlet——用来测试各个应用Demo。

请求一静态资源,在你的浏览器上输入如下类型URL:http://machineName:port/staticResource

如果,你是在运行此应用之外的一台机器上发送请求,machineName是运行此应用机器的名称或IP地址。如果是在同一台机器上,你可以使用localhost表示machineName,port是8080,staticResource是请求资源的名称,它必须在WEB_ROOT目录中。

例如,你想在同一台机器上测试此应用,你想像HttpServer请求index.html文件,使用如下URL:http://localhost:8080/index.html

停止server,你在浏览器上发送shutdown命令即可。shutdown命令在HttpServer中通过SHUTDOWN静态常量来定义的:

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
  • 1

所以,停止server,你可以使用如下URL:http://localhost:8080/SHUTDOWN

现在,我们看看await()方法。

使用await方法名称代替wait,因为wait()是java.lang.Object类中重要的方法——线程应用中使用。

await()开始创建一ServerSocket实例,然后进入一while循环。

serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
...
// Loop waiting for a request
while (!shutdown) {...}

在while循环中,ServerSocket的accept(),只有在8080端口接收到HTTP请求时才会返回:socket = serverSocket.accept();

当接收到HTTP请求时,await()方法中包含的java.io.InputStream和java.io.OutputStream对象,将会从accept()方法返回的Socket实例中得到:

input = socket.getInputStream();
output = socket.getOutputStream();

await()方法创建ex01.pyrmont.Request一对象,并调用它的parse()方法去解析HTTP请求数据:

// create Request object and parse
Request request = new Request(input);
request.parse();

然后,await()方法创建Response对象,并调用它sendStaticResource()方法:

// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();

最后,await()方法关闭Socket,并且通过Request对象的getUri()检查HTTP请求URL是否是shutdown命令。如果是,shutdown变量赋值true,并且程序退出while循环:

// Close the socket
socket.close();
// check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);

1.4.2 Request类

应用Demo中ex01.pyrmont.Request表示一HTTP请求。此对象实例是通过传入在Socket中获得InputStream对象创建的,它负责处理与客户端通讯。通过调用InputStream对象的read()方法获取HTTP请求数据。Request代码如下:

package ex01.pyrmont;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author : Ares
 * @createTime : Aug 21, 2012 9:48:45 PM
 * @version : 1.0
 * @description :
 */
public class Request {
    private InputStream input;
    private String uri;

    public Request(InputStream input) {
        this.input = input;
    }

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

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

其中parse()方法解析HTTP请求数据。在此方法中,其他的就没做什么了。唯一信息,通过私有方法parseUri()获取到HTTP请求的URI。parseUri()方法通过uri变量储存URI。共有方法getUri()被用来调用,以便获取到此HTTP请求URI。

有关HTTP请求数据处理将在第三章应用Demo中出现。

理解parse()和parseUri()方法是如何工作的,你需要知道HTTP请求的结果,这个已经在上面小节介绍过了——“Hypertext Transfer Protocol (HTTP)协议”。此章节,我们只关心HTTP请求的第一部分——请求行。一请求行,开始与请求方法,随后是请求URI和协议版本,结束与CRLF换行符。请求行中每一元素被空格符分开。例如,一请求行,以get方式请求index.html,格式是:GET /index.html HTTP/1.1

1.4.3 Response类

应用Demo中ex01.pyrmont.Response类代表中HTTP请求。代码如下:

package ex01.pyrmont;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @author : Ares
 * @createTime : Aug 21, 2012 9:51:37 PM
 * @version : 1.0
 * @description :
 * 
 * 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
 */
public class Response {
    private static final int BUFFER_SIZE = 1024;
    Request request;
    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];
        FileInputStream fis = null;
        try {
            File file = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                while (ch != -1) {
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            } else {
                // file not found
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n"
                        + "Content-Type: text/html\r\n"
                        + "Content-Length: 23\r\n" + "\r\n" 
                        + "

File Not Found

"
; output.write(errorMessage.getBytes()); } } catch (Exception e) { // thrown if cannot instantiate a File object System.out.println(e.toString()); } finally { if (fis != null){ fis.close(); } } } }

首先注意构造函数接收java.io.OutputStream对象,如下:

    public Response(OutputStream output) {
        this.output = output;
    }
  • 1
  • 2
  • 3

一个Response对象,是通过HttpServer类的await(),通过传人从socket中获得的OutputStream对象。

Response对象包含2个共有方法:setRequest()和sendStaticResource() 方法。其中setRequest()方法被用来传送一个Request对象给Response对象。

sendStaticResource()方法被用来发送静态资源,如HTML文件。它首先通过传入父目录和子目录给java.io.File类的构造方法:

File file = new File(HttpServer.WEB_ROOT, request.getUri());
  • 1

然后检查文件是否存在。如果存在,sendStaticResource()方法通过传入File对象构造java.io.FileInputStream对象。然后,它调用FileInputStream的read()方法,并向OutputStream对象写入字节数组输出。注意这里向浏览器输出的静态数据是未做处理的原始数据。

if (file.exists()) {
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                while (ch != -1) {
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
}

如果,文件不存在,sendStaticResource()方法发送一错误信息给浏览器。

String errorMessage = "HTTP/1.1 404 File Not Found\r\n"
                        + "Content-Type: text/html\r\n"
                        + "Content-Length: 23\r\n" + "\r\n" 
                        + "

File Not Found

"; output.write(errorMessage.getBytes());

1.4.4 运行Demo

运行HttpServer。打开浏览器,输入http://localhost:8080/index.html

在你的浏览器上,你将看到index.html页面显示如下: 
这里写图片描述

在控制台上,你可以看到类似如下的HTTP请求信息:

GET /index.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
application/vnd.ms-excel, application/msword, application/vnd.ms-
powerpoint, application/x-shockwave-flash, application/pdf, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR1.1.4322)
Host: localhost:8080
Connection: Keep-Alive

GET /images/logo.gif HTTP/1.1
Accept: */*
Referer: http://localhost:8080/index.html
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR1.1.4322)
Host: localhost:8080
Connection: Keep-Alive

1.5 小结

在本章中你已经看到了一个简单的Web Server是如何工作的。本章中应用Demo只包含3个类,并且不足实用。尽管如此,它依然是很好的学习工具。下一章将讨论如何处理动态内容。


Reference:https://blog.csdn.net/LoveJavaYDJ/article/details/54019164

你可能感兴趣的:(tomcat)