译者 jarfield
博客 http://blog.csdn.net/jarfield
本章解释了Java Web服务器是如何工作的。Web服务器又被称为超文本传输协议(Hypertext Transport Protocol, HTTP)服务器,因为它和客户端(通常是浏览器)使用HTTP协议进行通信。基于Java开发的Web服务器都使用到两个重要的类:java.net.Socket 和java.net.ServerSocket , 并通过HTTP消息完成通信。因此,本章的开头就开始讨论HTTP和这两个类。然后,继续介绍本章附带的应用程序。
超 文本传输协议(HTTP)
HTTP协议,允许Web服务器和浏览器在Internet上发送和接受数据。HTTP是一种基于“请求-响应”模式的协议。客户端请求一个文件 (file),服务器针对该请求给出响应。HTTP使用可靠的TPC连接——默认端口是80。HTTP的最初版本是HTTP/0.9,后来被HTTP /1.0重写。HTTP/1.0的替代者是当前的HTTP/1.1。HTTP/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请求的一个例子:
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是请求的第一行:
POST /examples/default.jsp HTTP/1.1
POST是Method,/examples/default.jsp是URI,HTTP/1.1就是Protocol/Version。
每个HTTP请求都可以使用HTTP标准中众多Method中的一个。HTTP/1.1共支持7种Method: GET, POST, HEAD, OPTIONS, PUT, DELETE和TRACE。GET和POST是互联网应用使用最普遍的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)分隔。
在headers和entity body之间,是一个空行(CRLF)。这个CRLF对于HTTP请求内容的格式是相当重要的,它告诉HTTP服务器:entify body从哪开始。在一些介绍互联网编程的书中,该CRLF被认为是HTTP请求的第4个组成部分。
在前面的HTTP请求中,entify body仅仅只有这一行:
lastName=Franks&firstName=Michael
这里只是一个例子,实际的HTTP请求中,Entity body当然可以更长一些。
HTTP响应
和HTTP请求一样,HTTP响应也包含3个组成部分:
- Protocol—Status code—Description
- Response headers(响应头部)
- 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 Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>
HTTP响应的第一行类似于HTTP请求的第一行。第一行告诉你:使用的HTTP版本是HTTP/1.1,请求处理成功了(200 = 成功),一切运行正常。
响应headers和请求headers类似,包含了很多有用的信息。响应数据中的entity body是响应本身的HTML内容。Headers和entity body之间通过CRLF分隔。
Socket类
套接字是网络连接的一个端点(endpoint)。应用程序使用套接字从网络上读取数据、向网络写入数据。两台不同机器上的应用,在同一个连接上以字节流 的格式向(从)对方发送(接收)数据。 为了向一个应用发送消息,你需要知道该应用的IP地址和端口。在Java中,套接字用java.net.Socket 类来表示。
你可以使用Socket 类众多构造函数中的一个来创建套接字 对 象。其中一个构造函数接收主机名和端口作为参数:
public Socket (java.lang.String host, int port)
其中,host是远程机器的名称或IP地址,port是远程应用的端口号。例如,要连接上yahoo.com 的80端口,你可以创建下面的Socket 对象:
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响应。另外i,这段代码还创建了一个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协议。如果你读了前一节 ,超文本传输协议(HTTP),应该就会理解上面代码中的 HTTP请求。
提示:你可以使用本书源代码中的com.brainysoftware.pyrmont.util.HttpSniffe r 类发送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 类 的一个构造函数原型:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
注意这个构造函数,binding address必须是一个java.net.InetAddress 对 象。创建InetAddress 对象的一个简单方法就是调用该类的静态方法getByName ,并把主机名作为String 对 象传给该方法,就像下面这样:
InetAddress.getByName("127.0.0.1");
下面这行代码创建了一个监听本地8080端口的、backlog为1的ServerSocket对象。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
有了ServerSocket对象后,你就可以告诉它:在指定的端口上监听绑定地址的连接请求吧。告诉的办法就是调用ServerSocket 的accept 方 法。当有一个连接请求到达时,该方法就会返回,返回值是一个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请求的字节流。然而,该应用不向浏览器发送任何header(date、 cookies等)。
下面各小节,我们将会看一看这3个类。
HttpServer类
HttpServer类表示了一个Web服务器,代码在Listing 1.1 中。需要注意的是,为了 节省篇幅,await方法没被列在Listing 1.1 中,可以在Listing 1.2 中找到。
Listing 1.1: The HttpServer class
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
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是这样初始化的:
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
这段代码中有一个名为webroot的目录,该目录包含了可以用于测试该应用的的静态资源。你还可以从该目录下找到几个用于测试下一章应用的 servlet。
如果要请求一个静态资源,你可以在浏览器的地址栏中敲入以下URL:
http://machineName:port/staticResource
如果你从另一台机器上发送请求,machineName 应该是该应用所在机器的主机 名或IP地址。如果你的浏览器运行在同一台机器上,可以使用localhost 作为machineName 。端口是8080,staticResource 是 被请求的文件(静态资源)名,该文件必须位于WEB_ROOT 下。
例如,你在同一台机器上测试该应用,想让HttpServer发送文件index.html,你可以使用下面的URL:
http://localhost:8080/index.html
如果要停止服务器,你可以通过特定的URL从浏览器发送shutdown命令:host:port的后面加上预先定义的、表示shutdown的字符串即 可。HttpServer 类的静态常量SHUTDOWN 定义了shutdown命令:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
因此,如果要停止服务器,你可以使用下面的URL:
http://localhost:8080/SHUTDOWN
现在,我们看一看Listing 1.2 中的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();
收到一个请求后,await 方法从accept 方 法返回的Socket 对象中获取了java.io.InputStream 对 象和java.io.OutputStream 对象。
input = socket.getInputStream();
output = socket.getOutputStream();
await 方法然后创建了一个ex01.pyrmont.Request 对 象,并调用它的parse 方法来解析HTTP请求的原始数据(raw data)。
// create Request object and parse
Request request = new Request(input);
request.parse ();
之后,await 方法创建了一个Response 对象,将上面的Request 对象设置成Response 对 象的成员,并调用Response 对象的sendStaticResponse 方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
最后,await 方法关闭了Socket 对 象,并调用了Request 对象的getUri 方 法,检查本次HTTP请求的URI似乎否是shutdown命令。如果是(shutdown命令)的话,shutdown 变量会被设置成true ,从而程序将退出while 循环。
// Close the socket
socket.close ();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
Request类
ex01.pyrmont.Request 类代表了HTTP请求。要创建Reuqest 对象,我们可以先从处理客户端通信的Socket 对象中获得的InputStream 对象,然后 将其作为参数调用Request 类的构造函数。通过调用InputStream 对象的read 方 法簇之一,就可以获取HTTP请求的原始数据。
Listing 1.3 列出了Request 类的代码。Request 类 有两个public方法,parse 和getUri ,Listing 1.4 和Listing 1.5 分别列出了这两个方法的 代码。
Listing 1.3: The Request class
package ex01.pyrmont;
import java.io.InputStream;
import java.io.IOException;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
public void parse() {
...
}
private String parseUri(String requestString) { ...
}
public String getUri() {
return uri;
}
}
Listing 1.4: The Request class's 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());
}
Listing 1.5: the Request class's parseUri method
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;
}
除了解析HTTP请求的原始数据,parse 方法也没做更多的事情。该方法从HTTP 请求中获取的唯一信息,就是通过调用private方法parseUri 解析出的 URI。parseUri 方法将HTTP请求的URI存储在成员变量uri 中。调用public方法getUri 可 以返回HTTP请求的URI。
提示:第3章 及后续章节将对HTTP请求原始数据进行更多的处理。
为了理解parse 和parseUri 方 法是如何工作的,你需要知道HTTP请求的协议格式,这在前面小节 ,“超文本传输协议(HTTP)”,已经讨论过。本 章,我们只对HTTP请求的第一部分,请求行(request line),刚兴趣。请求行以method标记开头,后面是请求URI和协议版本,最后以回车换行符(CRLF)结束。请求行中的元素是以空格分隔的。例 如,使用GET方法请求index.html的请求行如下所示:
GET /index.html HTTP/1.1
parse 方 法从传给Request 对象的InputStream 对 象中读取整个字节流,并保存在字节数组buffer 中。然后,使用buffer 中的字节数据创建一个名为request 的StringBuffer 对象,并将StringBuffer 对 象的String 表示(representation)传给parseUri 方法。
parse方法的代码列在Listing 1.4 中。
parseUri 方法负责从请求行中获取URI。Listing 1.5 列出了parseUri 方法的代码。parseUri 方 法在请求行中搜索第一个和第二个空格,获取(两个空格)之间的URI。
Response类
ex01.pyrmont.Response 类 代表了HTTP响应数据,Listing 1.6列出了其代码。
Listing 1.6: The Response class
package ex01.pyrmont;
import java.io.OutputStream; import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;
/*
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" +
"<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();
}
}
}
我们首先注意到,该类的构造函数接收一个java.io.OutputStream 对象作为参数,如下所示。
public Response(OutputStream output) {
this.output = output;
}
HttpServer 类的await 方 法从Socket 对象中获取OutputStream 对 象,将其作为参数构造了一个Response 对象。Response 类有两个public 方 法:setRequest 和sendStaticResource 。setRequest 方法用来将Request 对 象设置成Response 对象的成员变量。
sendStaticResource 方法用来发送静态资源,比如HTML文件。该方法首先将父路 径和子路径传递给java.io.File 的构造函数,创建一个File对象。
File file = new File(HttpServer.WEB_ROOT, request.getUri());
然后检查该文件是否存在。如果存在,那么sendStaticResource 方法以File对象为参数构造一个java.io.FileInputStream 对象。接着,调用FileInputStream 对象的read 方 法,并向OutputStream 对象output 写入字节数组。请注意,这种情况下,静态资源的内容是作为原始数据发送给浏览器的。
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());
运 行应用程序
要从工作目录运行该应用,需要敲入下面的命令:
java ex01.pyrmont.HttpServer
要测试该应用,可以打开浏览器,在地址栏敲入下面的URL:
http://localhost:8080/index.html
正如Figure 1.1所示,你会看到index.html显示在浏览器(原图是IE6,这里是译者的IE8)中。
Figure 1.1: The output from the web server
在控制台上,你可以看到类似于下面的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 CLR
1.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 CLR
1.1.4322)
Host: localhost:8080
Connection: Keep-Alive
总结
从本章你已经看到了简单的Web服务器是如何工作的。本章附带的应用只包括3个类,功能还不完整。无论如何,该应用仍是一个很好的学习工具。下一章,我们 将讨论对动态内容(dynamic content)的处理。