How Java Web Servers Work

来源:互联网 发布:悟空软件 编辑:程序博客网 时间:2024/05/29 13:25

编辑批注: 本文改编自 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 实例获取一个 java.io.InputStream java.io.OutputStream 对象.

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

await 方法接着会创建 Request 对象并调用 parse 方法解析原始的 HTTP 请求.

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

再往下, await 方法创建了一个 Response 对象, Request 对象给它, 然后调用 sendStaticResource 方法.

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

最后, await 方法关闭了 Socket 并调用 Request getUri 方法检查是否这个 HTTP 请求是一个关闭命令. 如果是, shutdown 变量被设置为 true, 程序退出 while 循环.

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



The Request Class

Request 类代表一个 HTTP 请求. 这个类的实例通过传递一个 InputStream 对象而构建, InputStream 对象正是从处理与客户端通信的 Socket 处得到的. 调用 InputStream 对象的一个 read 方法获取 HTTP 请求的原始数据.

Request 类有2public 方法: parse getUri. parse 方法解析 HTTP 请求的原始数据. 除此以外它不会做得更多--它的工作成果中唯一有用的是 HTTP 请求的 URI, 它也是通过调用 private 方法 parseUri 而获得这个 URI. parseUri 方法使用 uri 变量存储 URI. 调用 public getUri 方法以返回得到 HTTP 请求的 URI.

要想搞明白 parse parseUri 方法是怎样工作的, 你需要知道 HTTP 请求的结构, 这是由 RFC 2616(.pdf) 定义的.

一个 HTTP 请求包含3部分:

  • Request line

  • Headers

  • Message body

现在, 我们只对 HTTP 请求的第一部分感兴趣, request line. request line 以请求方式开头, 跟着是请求 URI 和协议版本, (CRLF) 结束. request line 中的各部分以空格分隔. 例如, 对于请求 index.html 文件的请求的 request line 使用 GET 方式:

GET /index.html HTTP/1.1

parse 方法从传递给 Request 对象的得自socket InputStream 读取全部字节流, 保存这些字节数组到 buffer. 然后将 buffer 字节数组中的 bytes 移到一个叫 request StringBuffer 对象中, 接着把这个 StringBuffer 对象的 String 形式传递给 parseUri 方法.

Listing 1.2 给出了 parse 方法.

Listing 1.2. The Request class' parse method

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

parseUri 方法随后从 request line 获得 URI. Listing 1.3 展示了 parseUri 方法. parseUri 方法搜索 request 中的第一个和第二个空白, 2者之间就是 URI.

Listing 1.3. Request 类的 parseUri 方法

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

The Response Class

Response 类表示 HTTP 响应. 它的构造器接收一个 OutputStream 对象, 如下:

public Response(OutputStream output) {
this.output = output;
}

Response 对象在 HttpServer 类的 await 方法中通过传递 OutputStream 对象(从 socket 获得)来构造.

Response 类有2个公共方法: setRequest sendStaticResource. setRequest 方法用于传递一个 Request 对象给 Response 对象. 就象 Listing 1.4 中的代码那样简单.

Listing 1.4. The Response class' setRequest method

public void setRequest(Request request) {
this.request = request;
}

sendStaticResource 方法用于发送静态资源, 象一个 HTML 文件. Listing 1.5 给出了它的实现.

Listing 1.5. The Response class' sendStaticResource method

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" +
"<h1>File Not Found</h1>";
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();
}
}

sendStaticResource 方法非常简单. 首先传递父和孩子路径给 File 类的构造器实例化一个 java.io.File .

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

然后检查文件是否存在. 如果存在, sendStaticResource 方法通过传递 File 对象构造一个 java.io.FileInputStream 对象. 接着调用 FileInputStream read 方法, OutputStream 输出写入 byte 数组. 注意, 在这种情况下, 静态资源的内容将作为原始数据发送给浏览器.

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" +
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());

Compiling and Running the Application

要想编译和运行程序, 首先需要解压包含本文示例程序的 .zip 文件. 解压 .zip 文件所在的目录叫做工作目录, 而且会有3个子目录: src/, classes/, lib/. 在工作路径下敲入如下命令编译程序:

javac -d . src/ex01/pyrmont/*.java

-d 选项将结果写到当前而非 src/ 目录下.

在工作目录敲下如下输入运行程序:

java ex01.pyrmont.HttpServer

测试程序时, 打开你的浏览器并在地址栏中输入如下 URL :

http://localhost:8080/index.html

你会看到 index.html 页面展示于你的浏览器中, Figure 1.


Figure 1. The output from the web server

在控制台上, 你会看见如下输出:

GET /index.html HTTP/1.1
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
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 4.01; Windows 98)
Host: localhost:8080
Connection: Keep-Alive

Summary

在这篇文章里, 你已经看到了一个简单的web server是如何工作. 本文所带的程序只由3个类组成, 功能并不完善. 不管怎样, 它是好的学习工具.

Budi Kurniawan is a senior J2EE architect and author.

 
 
原创粉丝点击