基于JAVA的WEB服务器工作机制
一个WEB服务器也被称为一个HTTP服务器,因为它使用HTTP协议和它的客户进行通讯,而这些客户通常是浏览器。 一个基于JAVA的WEB服务器使用了两个重要的类:java.net.Socket和
java.net.ServerSocket
,并且是通过HTTP消息进行通讯的。本文开头将讨论HTTP和这两个类,后面,将解释一个简单WEB服务器应用程序的工作机制。
HTTP协议允许服务器和客户机通过INTERNET接收和发送数据。它是个请求和回应协议----客户机发送请求,服务器对请求给出回应。HTTP 使用可靠的TCP 连接,默认TCP端口是80。HTTP的第一版是HTTP/0.9,随后被 HTTP/1.0所取代。当前最新的版本是HTTP/1.1,这个在RPC2616规范文档中给出了定义。
这一章节简单讲叙了HTTP 1.1, 对于你理解WEB服务器应用程序发送的消息还是足够的。如果你很感兴趣,可以参考RFC 2616文档。
使用HTTP,客户端通过建立一个连接和发送一个HTTP请求来初始化事务会话,服务器联系客户端或者回应一个callback连接给客户端。 它们都可以中断连接。比如,在使用WEB浏览器时,你可以点击浏览器上的STOP按钮来停止文件下载进程,就有效的关闭了和这个WEB服务器的HTTP连接。
一个HTTP request包含三个部分:
下面给出一个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。
POST /servlet/default.jsp HTTP/1.1
请求的是POST方法,后面的 /servlet/default.jsp
表示一个URL地址,HTTP/1.1
表示协议的版本。
HTTP标准规范定义了一些请求方法,用来给每个HTTP请求所使用。HTTP 1.1支持7中请求方法: GET
, POST
, HEAD
, OPTIONS
, PUT
, DELETE
, 和 TRACE。
GET
和POST
在INTERNET的应用程序中是使用最普遍的两个方法。
URI完整的指明了一个INTERNET资源。一个URI通常是相对于服务器的根目录被解释的。 因此,它总是使用符号(/
)开头。一个URL实际是一个URI类型。协议版本表示当前正在使用的HTTP协议的版本。
请求包头(request header)包含了一些有用的客户机环境的信息和请求的实体(entity body)信息。比如,它可以包含浏览器使用的语言和实体的长度等等。每个请求包头都被CRLF(回车换行)序列所分离。
在先前的HTTP请求中,实体是下面简单的一行:
LastName=Franks&FirstName=Michael
在一个典型的HTTP请求中,这个实体能够很容易地变得更长。
和请求类似,一个HTTP响应也包含三个部分:
下面是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
HTTP Response Example
Welcome to Brainy Software
第一行的响应包头和上面的请求包头很相似。 第一行告诉我们,协议是使用的HTTP1.1,响应请求已成功(200表示成功),一切已OK。
响应包头和请求包头相似,也包含一些有用的信息。响应的实体是HTML那一部分的内容。包头和实体也都是被CRLF序列分离开的。
套接字(socket)是网络连接的一个端点。它使得应用程序能够通过网络进行读和写的操作。 通过在连接上发送和接受字节流,两个位于不同计算机的软件程序能够彼此相互通讯。为了发送一个消息到另一个程序,你需要知道对方机器的IP地址和socket端口号。在JAVA中,一个socket是由java.net.Socket类所表示的。
为了创建一个套接字,你可以使用Socket类的构造函数来完成。
这些构造函数接受主机名和端口:
public Socket(String host, int port)
host表示远程计算机名或者IP地址,
port
表示该远程应用的端口号。比如,要在80端口连接到yahoo.com,你需要构造下面的socket:
new Socket("yahoo.com", 80);
一旦你成功创建了一个Socket
类的实例,就可以使用它来发送和接受字节流了。 要发送字节流,必须首先调用Socket
类的getOutputStream
方法来获得一个java.io.OutputStream
对象。要发送一个文本到远程应用程序,经常要构造一个从OutputStream
对象返回的java.io.PrintWriter
对象。要接收连接另一端的字节流,要调用Socket
类的getInputStream
方法,该方法是从 java.io.InputStream
返回的。
下面的程序段创建了一个socket,和本地HTTP服务器(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();
要从服务器得到一个确切的响应,你需要发送一个遵循HTTP协议规则的HTTP请求。如果你阅读了上面的那段"超文本传输协议(HTTP)" ,那么你就应该能够理解刚才上面建立socket的代码。
ServerSocket
类Socket
类表示的是客户端的socket。无论什么时候,只要你想连接到一个远程服务器的应用,你都要构建一个socket。如果你想执行一个服务器应用程序,比如HTTP服务或者FTP服务的程序,那么你需要使用不同的途径。因为你的服务器必须一直是开机闲置,所以它不知道什么时候客户机试图来连接它。
这个时候,需要使用java.net.ServerSocket
类。它会实现一个服务器socket。一个服务器socket会等待来自客户端的连接。一旦它接收到一个连接请求,它就会创建一个 Socket
实例来处理和客户端通讯的问题。
要创建一个服务器socket,可以使用四种ServerSocket类构造方法中的一种来实现。你需要制定服务器socket监听的IP地址和端口。 典型的,IP地址如果是127.0.0.1,意味着服务器socket将监听本地机器。这个被监听的IP地址被认为是一种绑定地址。server socket的另一个重要属性是它的 backlog属性,它是在server socket拒绝连接请求前,能够接受的连接请求的最大队列长度。
ServerSocket类的构造函数之一如下:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
对于这个构造函数而言,绑定地址必须是java.net.InetAddress
的一个实例。一个简单的办法是通过调用它的静态方法getByName来构造一个InetAddres
对象。该方法来一个包含主机名的字符串参数:
InetAddress.getByName("127.0.0.1");
下面一行代码构造一个ServerSocket
,它监听本地机器的8080端口,backlog设置为1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦有了一个 ServerSocket
实例,可以通过调用accept方法来告诉它等待进来的连接请求。这个方法只有在有一个连接请求时才返回。它返回的是Socket
类的实例。这个Socket
对象能够发送和接受来自客户端应用的字节流,就是第一节所讲到的socket类。实际上,accept
是本文提及的唯一一个在应用中使用的方法。
我们的web服务器应用是ex01.pyrmont
包的一部分,包含三个类:
HttpServer
Request
Response
这个应用的入口(静态main
方法)是HttpServer
类。它创建了一个HttpServer
实例来调用它的await
方法。 就象这个方法名所暗示的,await
方法在一个指定的端口等待一个HTTP请求,并处理它们,然后发送回应给客户端。它保持等待状态,直到收到一个shutdown命令。 (命令名await
来代替wait
的原因是wait是
System.Object
类中的一个用于线程方面的重要方法)
应用仅仅只发送静态资源,比如来自特定目录的HTML和图片文件。不支持动态包头 (比如日期或者cookie) 。
在下面的段落中,让我们来看看这三个类吧。
HttpServer
类HttpServer
类表示一个web服务器,且在公共静态目录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
作为机器名。端口是8080。staticResource
是请求的文件夹名,它必须位于WEB-ROOT目录中。
必然,如果你使用同一个计算机来测试应用,你想向HttpServer请求发送一个index.html 文件,那么使用如下URL:
http://localhost:8080/index.html
想要停止服务器,可以通过发送一个shutdown命令。该命令是被HttpServer
类中的静态SHUTDOWN_COMMAND变量所定义:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
因此,要停止服务,你可以使用命令:
http://localhost:8080/SHUTDOWN
现在让我们来看看前面提到的await
方法。下面一个程序清单给出了解释。
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) {
...
}
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
对象,使用setRequest
方法并调用它的sendStaticResource
方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
最后,await
关闭该Socket
。调用Request的getUri
方法来检查HTTP请求的URI是否是一个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
类
Request类代表一个HTTP请求。Socket处理客户端的通讯,将返回一个InputStream对象,通过传递该对象,可以构造一个Request类的实例。通过调用
InputStream
对象的read方法来获得这个HTTP请求的原始数据(raw data)。
Request
有两个公共方法:parse
和 getUri。
parse方法解释HTTP请求的原始数据。它不做很多事情----它能够利用的唯一信息只是HTTP请求的URI ,这个URI是从私有方法 parseUri
.得到的。parseUri
方法保存URI 到uri
变量中,然后调用公共方法getUri
来返回一个HTTP请求的URI。
为了理解parse
和 parseUri
方法是如何工作的,需要知道HTTP请求的内部结构。这个结构是在RFC2616文档中定义的。
一个HTTP请求包含三个部分:
- 请求行(Request line)
- 请求包头(Headers)
- 消息体(Message body)
现在,我们仅仅只对HTTP请求的第一部分请求行(Request line)感兴趣。一个请求行由方法标记开始,后面根请求的URI和协议版本,最后由CRLF字符结束。请求行中的元素被空格字符分开。比如,使用GET方法请求的index.html文件的请求行如下:
GET /index.html HTTP/1.1 //这是一个请求行
方法parse
从socket的InputStream
中读取整个字节流,该字节流是 Request
对象传递进来的,然后parse将这些字节流存储在一个缓冲区里, 在缓冲区中组装一个称为request的StringBuffer
对象。
下面的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
parseUri
方法从请求行那里得到URI。Listing 1.3 展示了parseUri
方法的用途。 parseUri
减缩请求中的第一个和第二个空格来获得URI。
Listing 1.3. The Request
class' 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;
}
Response
类
Response
表示一个HTTP响应。它的构造函数接受一个OutputStream
对象,比如下面的:
public Response(OutputStream output) {
this.output = output;
}
Response
对象被HttpServer
类的await
方法构造,该方法被传递的参数是从socket那里得到的OutputStream
对象。
Response
类有两个公共方法: 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" +
"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();
}
}
sendStaticResource
方法是非常简单的。它首先传递父路径和子路径给File类的构造器,从而对java.io.File
类进行了实例化。
File file = new File(HttpServer.WEB_ROOT, request.getUri());
然后它检查文件是否存在。如果存在,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());
编译和运行应用程序
为了编译和运行应用,你首先需要解压包含本文应用程序的.zip文件。你解压的目录成为工作目录(working directory),它有三个子目录: 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
在控制台(Console),你能看到如下内容:
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
概要总结
在本文中,你了解了一个简单的WEB服务器的工作机制。本文附带的应用程序源代码只包含三个类,但并不是所有的都有用。尽管如此,它还是能被作为一种很好的学习工具为我们服务。
Translated by Willpower,2003.11.24