一个WEB服务器也被称为一个HTTP服务器,因为它使用HTTP协议和它的客户进行通讯,而这些客户通常是浏览器。 一个基于JAVA的WEB服务器使用了两个重要的类:java.net.Socket和java.net.ServerSocket,并且是通过HTTP消息进行通讯的。本文开头将讨论HTTP和这两个类,后面,将解释一个简单WEB服务器应用程序的工作机制。
超文本传输协议 (HTTP)
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 请求(Requests)
一个HTTP request包含三个部分:
方法,URL,协议/版本(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。
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响应(Responses)
和请求类似,一个HTTP响应也包含三个部分:
协议状态 代码描叙(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>
第一行的响应包头和上面的请求包头很相似。 第一行告诉我们,协议是使用的HTTP1.1,响应请求已成功(200表示成功),一切已OK。
响应包头和请求包头相似,也包含一些有用的信息。响应的实体是HTML那一部分的内容。包头和实体也都是被CRLF序列分离开的。
Socket类
套接字(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 是本文提及的唯一一个在应用中使用的方法。
Application应用
我们的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<i; j++) {
request.append((char) buffer[j]);
}
System.out.print(request.toString());
uri = parseUri(request.toString());
}
||||||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" +
"<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输出。注意这种情况下, 静态资源的内容也被作为原始数据被发送给了浏览器。
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());
编译和运行应用程序
为了编译和运行应用,你首先需要解压包含本文应用程序的.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. 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服务器的工作机制。本文附带的应用程序源代码只包含三个类,但并不是所有的都有用。尽管如此,它还是能被作为一种很好的学习工具为我们服务。