现在的位置: 首页 > 综合 > 正文

How Java Web Servers Work

2013年01月07日 ⁄ 综合 ⁄ 共 7067字 ⁄ 字号 评论关闭

编辑批注: 本文改编自 Budi 个人出版的关于TOMCAT内部实现原理的书.你可以到他的网站上找到更多这方面的信息.

web server 也叫 Hypertext Transfer Protocol (HTTP) server, 因为它使用的是HTTP协议与客户端通信, 通常是指那些 web 浏览器. 基于 Java web server 使用2个重要的类, java.net.Socket java.net.ServerSocket, 进行HTTP消息通信. 因此,本文先讨论HTTP协议和这2个类. 之后, 我会讲解本书附带的一个简单的 web server 程序.

The Hypertext Transfer Protocol (HTTP)

HTTP 协议能够让 web servers 和浏览器通过 Internet 发送和接收数据. 它是一种请求-响应式的协议—客户端发起一个请求,服务器响应这个请求. HTTP 使用可信赖的 TCP 连接, 默认使用的是 TCP 80 端口. HTTP协议的第一个版本是 HTTP/0.9, 然后是 HTTP/1.0. 当前版本是由 RFC 2616(.pdf) 定义的 HTTP/1.1.

这部分简短的介绍了一下 HTTP 1.1; 足可以让你理解 web server 应用程序发出的消息. 如果你有兴趣了解更多的细节,请阅读 RFC 2616.

使用 HTTP 的时候, 客户端总是通过建立一个连接来开始一个事务,然后发送一个 HTTP 请求. server 不会主动联系客户端,也不会建立一个回调连接到客户端(is in no position to contact a client or to make a callback connection to the client). 客户端和服务器都可以提前中止一个连接. 例如, 当使用 web 浏览器的时候, 你可以点击浏览器上的 停止 按钮结束文件的下载过程, 有效关闭与 web 服务器的 HTTP 连接.

HTTP Requests

一个 HTTP 请求由3部分组成:

  • Method-URI-Protocol/Version

  • Request headers

  • Entity body

一个 HTTP 请求的例子如下:

POST /servlet/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
Referer: http://localhost/ch8/SendDetails.htm
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 出现在请求的第1.

POST /servlet/default.jsp HTTP/1.1

POST 是请求的方式, /servlet/default.jsp 表示 URI, HTTP/1.1 Protocol/Version 这段的值.

每个 HTTP 请求都可以使用诸多请求方式中的任何一种, 这些请求方式都由HTTP 标准制定. HTTP 1.1 支持7种请求: GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE. GET POST 是在 Internet 程序中最常使用的2种请求.

URI 完整的指定了一个 Internet 资源. URI 通常以相对于服务器的根目录来进行解析. 因此, 应该总是以 / 开始. URL 实际是 URI 的一种. 协议版本表示正在使用的 HTTP 协议的版本号.

请求头包含了客户端环境和请求主体的有用信息. 例如, 包含浏览器的语言设置,请求主体的长度, . 每个请求头被 CRLF 序列分隔开.

在头和主体之间有一个非常重要的空白行 (CRLF sequence) . 这一行标志着请求主体的开始. 一些 Internet 编程书认为这个 CRLF HTTP 请求的第4个组成部分.

在前面的 HTTP 请求, 请求主体只是简单的如下这么一行:

LastName=Franks&FirstName=Michael

在一个更典型的 HTTP 请求里, 请求主体可以比这更长.

HTTP Responses

类似于请求, HTTP 响应也由3部分组成:

  • Protocol-Status code-Description

  • Response headers

  • Entity body

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

HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 3 Jan 1998 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 11 Jan 1998 13:23:42 GMT
Content-Length: 112

<html>
<head>
<title>HTTP Response Example</title></head><body>
Welcome to Brainy Software
</body>
</html>

响应头的第一行很类似于请求头的第一行. 第一行告诉你使用的协议是 HTTP version 1.1, 请求成功 (200 = success), 一切都完全正确.

类似于请求头, 响应头也包含了有用的信息. 响应主体是 HTML 内容. 头和主体也以 CRLFs 序列分隔.

The Socket Class

一个socket是一次网络连接的一个端点. socket 能够使程序从网络中读取数据, 向网络中发送数据. 处于2台不同机器上的2个程序能够通过一次连接发送和接收字节流而进行通信. 要想发送一条消息给另一个程序, 你必须知道它的 IP 地址, 和它的 socket 端口. Java 中的 socket 使用 java.net.Socket 类来表示.

要想创建一个 socket, 你可以使用 Socket 类的诸多构造器中任一种. 其中一个构造器接受主机名和端口号:

public Socket(String host, int port)

host 是远程机器名或者 IP 地址, port 是远程程序的端口号. 例如, 要连接 yahoo.com 80 端口, 你应当如下构建 socket:

new Socket("yahoo.com", 80);

一旦你成功创建了 Socket 类的一个实例, 你就可以使用它发送和接收字节流. 发送字节流的时候, 你必须首先调用 Socket 类的 getOutputStream 方法获得 java.io.OutputStream 对象. 当向远程程序发送文本的时候, 通常要用返回得到的 OutputStream 构造一个 java.io.PrintWriter 对象. 当要接收来自连接另一端的字节流的时候, 需调用 Socket 类的 getInputStream 方法得到一个 java.io.InputStream 对象.

下面代码片段建立了一个 socket 与本地 HTTP server (127.0.0.1 意味着一个本地主机) 通信, 发送 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 服务器获得正确的响应, 你必须发送一个遵从 HTTP 协议的 HTTP 请求. 如果你已经阅读过了前面章节, "The Hypertext Transfer Protocol (HTTP)," 你就会理解上面代码里的 HTTP 请求.

The ServerSocket Class

Socket 类代表一个 “客户端“("client"socket; 就是当你想连接到一个远端服务器程序时所构建的 socket. 如果你想实现一个服务器程序, 例如 HTTP server 或者 FTP server, 你需要一种不同的方法. 这是因为你的服务器必须持久存在随时准备接收请求, 因为它根本不知道什么时候一个客户端程序要连接它.

因为这个原因, 你必须使用 java.net.ServerSocket . 这是服务器端 socket 的一个实现. 服务器端 socket 等待来自客户端的连接请求. 每当它接受了连接请求, 它就会创建一个 Socket 实例去处理和客户端的通信.

为创建一个服务器端 socket, 你必须使用 ServerSocket 类提供的四个构造器中的一个. 你需要指定 IP 地址和这个服务器端 socket 监听的端口号码. 典型地, IP 地址会是 127.0.0.1, 意思是这个服务器端 socket 将在本地机器上监听. 服务器端 socket 所在的机器的 IP 地址也被称做绑定地址. 服务器端 socket 的另一个重要属性是它的 backlog, 它是指在服务器端 socket 开始拒绝新来的请求前所能忍受的连接请求队列的最大长度.

ServerSocket 类的其中一个构造器有如下方法声明:

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

对于这个构造器, 绑定地址必须是 java.net.InetAddress 的一个实例. 一种简单的构建 InetAddress 对象的方式是通过调用它的静态方法 getByName, 传递一个包含主机名称的字符串给它:

InetAddress.getByName("127.0.0.1");

下面这行代码构造了一个 ServerSocket, 监听本地机器的 8080 端口, backlog值设置为1.

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

一旦你已经有了一个 ServerSocket 实例, 你就可以调用它的 accept 方法告诉它等待进入的连接请求. 这个方法只有在有连接请求的时候才会有返回. 它返回一个 Socket 类的实例. 这个 Socket 对象随后就可以用来发送和接收来自客户端程序的字节流, 就象在 The Socket Class 里解释的那样. 实际上, 在本文所附带的这个程序里 accept 方法是唯一用到的方法.

The Application

我们的 web 服务器程序位于包 ex01.pyrmont, 3个类组成:

  • HttpServer

  • Request

  • Response

这个程序的入口点 (the static main method) HttpServer 类里. 它创建一个 HttpServer 实例并调用它的 await 方法. 就象名字所隐含的意思那样, 在指定的端口上 await 等待 HTTP 请求, 处理请求, 然后向这些客户端发回响应. 它会一直保持等待直到接收到一个关闭命令为止. (方法名称使用 await 而不是 wait 是因为 wait System.Object 类中一个非常重要的用于处理多线程的方法.)

这个程序只发送来自指定目录下的静态资源, HTML 和图片文件. 它不支持头 (像时间或者 cookies).

现在, 在下面子章节里, 我们来看看这3个类.

The HttpServer Class

HttpServer 类代表一个 web server, 能提供在由public static final 变量 WEB_ROOT 表示的目录下所找到的静态资源以及这个目录下的所有子目录下的静态资源. WEB_ROOT 按如下方式初始化:

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

上面代码包含一个叫 webroot 的目录, 包含一些静态资源用于测试这个程序. 你也会发现一个 servlet, 在我的下一篇文章里将会用到, "How Servlet Containers Work."

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

http://machineName:port/staticResource

如果你在另一台不同的机器上跑着你的程序并发送请求, 那么 machineName 是运行这个程序的计算机名称或者 IP 地址. 如果你使用的浏览器是在同一台机器上, 你可以使用 localhost 作为 machineName. 端口是 8080, staticResource 是要请求的文件名, 并且该文件必须位于 WEB_ROOT.

例如, 如果你使用同一台计算机测试这个程序, 并且你想要求 HttpServer 返回 index.html 文件, 使用如下 URL:

http://localhost:8080/index.html

要想停止服务器, 在地址栏中敲入预先定义好的关闭字符串发送关闭命令. 关闭命令在 HttpServer 类中定义为 SHUTDOWN static final 变量:

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

因此, 要关闭服务器, 你可以使用:

http://localhost:8080/SHUTDOWN

现在, 我们来看看 Listing 1.1 中给出的 await 方法. 对代码的解释就在这个 listing 的后面.

Listing 1.1. The HttpServer class' await method

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

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 对象:

socket = serverSocket.accept();

接收到一个请求后, await 方法从 accept 方法返回的 Socket 实例获取一个

抱歉!评论已关闭.