本章主要解释Java Web服务器是如何工作的。由于Web服务器通常使用HTTP与客户端(同时为Web浏览器)进行通信,所以Web服务器也经常被称为HTTP服务器。基于Java的Web服务器通常使用两个重要的Java类:java.net.socket和java.net.serversocket,并且服务器与客户端之间的通信通过HTTP信息完成,因此本章的内容从讨论HTTP及这两个类开始。最后,将介绍一个简单的Web服务器应用。
一、 超文本传输协议(Hypertext Transfer Protocol)
HTTP是一种允许Web服务器和Web浏览器在Internet上相互发送和接受数据的协议。这是一种请求响应协议。客户端向服务器请求一个文件然后服务器将对客户端进行响应。HTTP使用可靠的TCP连接——在默认情况下使用TCP的80端口。HTTP的第一个版本是HTTP/0.9,后来这个版本被HTTP/1.0替代。当前所使用的版本HTTP/1.1在RFC2616中定义,该文档的下载地址为:http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf
在HTTP中,总是由客户端来发起一个事务,即由客户端建立一个连接并发送一个HTTP请求。服务器没有资格主动联系客户端或与客户端建立一个反馈连接。无论是客户端还是服务器端都可以提前终止连接。例如,当我们在使用Web浏览器时,我们可以点击浏览器上的关闭按钮来阻止文件的下载,同时也有效的关闭了与Web服务器之间的连接.
1 HTTP请求
一个HTTP请求通常由三部分组成:
(1) 方法-URI-协议/版本
(2) 请求头(Request Header)
(3) 实体主体(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 |
方法-URI-协议/版本行出现在请求的第一行:
POST /examples/default.jsp HTTP/1.1 |
其中POST是请求方法,/examples/default.jsp代表URI,HTTP/1.1是协议/版本部分。
每个HTTP请求使用HTTP标准规定的众多方法中的一种。HTTP1.1支持起七种请求类型:GET,POST,HEAD,OPTIONS,PUT,DELETE和TRACE。在Internet应用中方法GET和POST是最常使用的。
URI完整规定了Internet上的资源。一个URI通常被解析为针对服务器根目录的相对路径,因此该部分通常以“/”开始。URL实际上是URI的一种类型(具体说明可以参见http://www.ietf.org/rfc/rfc2396.txt)。协议版本代表所使用HTTP的版本。
请求头中包含了客户端环境及请求实体主体的重要信息。例如,它包含了浏览器所设定的语言,实体主体的长度等等。每个请求头由回车换行进行分割。
在请求头和实体主体之间有一个空行,这个空行对HTTP请求的格式非常重要。这个回车换行告诉服务器主体实体从哪里开始。在某些Internet编程书中,空格换行被认为是HTTP请求的第四部分。
在前面的HTTP请求中,实体主体是下面的部分:
lastName=Franks&firstName=Michael |
在典型的HTTP请求中实体主体可以更长。
2 HTTP响应
和HTTP请求类似,HTTP响应同样由三部分组成:
(1) 协议-状态码-描述
(2) 响应头
(3) 实体主体
下面是一个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,版本1.1,请求成功(200=success),并且一切运行顺利。
响应头中包含的有用信息也和请求头中的类似。响应中的实体主体是响应的HTTP内容。响应头和实体主体由回车换行分割。
3 Socket类
Socket是网络连接中的一个端点。Socket允许一个应用程序在网络上进行读写操作。驻留在两台不同电脑上的两个软件应用可以通过在一个连接上发送和接收字节流进行通信。为了从你的应用中发送一个消息到另外一个应用,你需要知道另一个应用所使用Socket的IP地址和端口号。在Java中,socket由类java.net.Socket表示。
为了创建一个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对象。为了发送文本到远程应用,可用使用返回得到的OutputStream对象来构造java.io.PrintWriter对象。为了从连接的另一端接收字节流,可以调用Socket类中getInputStream方法来获得java.io.InputStream对象。
下面的代码片段创建了一个与本地HTTP服务器(127.0.0.1代表本地主机)通信的socket,它发送一个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请求。
4 ServerSocket类
Socket类代表了“客户端”的socket,即任何时候你想连接到一个远程服务器应用所构建的socket。现在如果你想实现一个服务器应用,例如HTTP服务器或者FTP服务器,你需要一种不同的方法。这是因为服务器必须随时待命,它并不知道客户端应用何时会尝试连接它。为了让你的应用能够随时待命,你需要使用java.net.ServerSocket类。这是服务器Socket的一种实现。
ServerSocket不同于Socket。ServerSocket的作用是随时等待来自客户端的连接请求。一旦服务器socket接收到一个连接请求,它会创建一个socket对象来处理和客户端的通信。
为了创建一个服务器socket,你需要使用ServerSocket类中所提供的四个构造函数中的一个。你需要规定服务器socket所监听的IP地址及端口号。通常情况下,IP地址可以是127.0.0.1,这意味着服务器socket将监听本地机器。服务器socket监听的IP地址叫做绑定地址。服务器socket另一个重要的属性是backlog,它是入连接请求的最大队列长度,当超过这个数字时,服务器socket将开始拒绝入连接请求。
ServerSocket类中的一个构造方法的签名如下:
public ServerSocket(int port, int backLog, InetAddress bindingAddress); |
注意,对于这个构造函数,绑定地址一定是java.net.InetAddress类的实例。构造InetAddress对象的一种简单方法就是调用其静态方法 getName,并传递一个String类型的参数(该参数包含了主机的信息),例如下面的代码是个小例子:
InetAddress.getName(“127.0.0.1”); |
下面一行代码构造了一个监听本地机器端口8080的服务器Socket。ServerSocket中的参数backLog设定为1:
new ServerSocket(8080, 1, InetAddress.getName(“127.0.0.1”)); |
一旦你拥有了一个ServerSocket的实例,你可以让它在绑定地址和服务器Socket正在监听的端口上等待入连接请求。你可以通过调用ServerSocket类中的方法accept实现这个过程。这个方法只会在出现连接请求时返回结果且返回值是Socket类的实例。这个Socket对象可用来与客户端发送和接收字节流。事实上,accept方法是唯一用在本章实例中的方法。
二、 简单实例
该Web服务器应用是包ex01.pyrmont中的一部分,由三部分组成:
n HttpServer
n Request
n Response
这个应用程序的入口(静态main方法)可以在HttpServer类中找到。main方法创建了一个HttpServer实例并调用了其await方法。这个await方法,正如它名字所暗示的,在一个指定的端口等待HTTP请求,处理它们,同时给客户端发送响应。当接收到shutdown指令时,await方法将停止等待。
该应用程序只能发送静态资源,例如驻留在某个目录下的HTML文件和image文件等。它同样能将入HTTP请求的字节流显示在控制台中。然而,它不能发送任何请求头到浏览器,例如时间或Cookies等。
我们将在下面的部分中依次解释该应用中的三个类。
1 HttpServer类
HttpServer类代表一个Web服务器,其代码参见列表1.1。注意,为了节省列表1.1的空间,await方法将在列表1.2中给出。
列表1.1:HttpServer类
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. */ // System.getProperty("user.dir")用于获得工作空间地址 public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
//服务器关闭命令 private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
//是否接收到关闭命令 private boolean shutdown = false;
public static void main(String[] args) { HttpServer server = new HttpServer(); server.await(); }
public void await() { … } } |
这个Web服务器可以为静态变量WEB_ROOT指定的目录及其子目录下发现的静态资源服务。WEB_ROOT的初始化如下:
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot"; |
该WEB_ROOT指定的目录包含了一些静态资源,你可以使用这些资源对本应用进行测试。同时你也能在相同的目录下发现一些servlet,可以使用这些servlet来测试下一张的应用。
为了请求一个静态资源,你可以在浏览器的地址栏中输入如下的URL:
http://machinename:port/staticResource |
如果你从另一台电脑上运行的应用程序发送一个请求,machineName是运行该程序电脑的名称或IP地址。如果你的浏览器在相同的电脑上,你可以使用localhost作为machineName,port为8080,staticResource是你请求文件的名称,且该文件必须驻留在WEB_ROOT下。
例如,如果你使用相同的电脑来测试应用,并且你想让HttpServer对象发送index.html文件,你可以使用如下的URL地址:
http://localhost:8080/index.html |
为了终止服务器的运行,你需要在Web浏览器的地址栏中输入预先定义的关闭命令(位于URL中host:port部分的后面)。关闭命令在HttpServer类中的静态变量SHUTDOWN中定义:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; |
因此,为了终止服务器的运行,你可以输入如下的URL:
http://localhost:8080/SHUTDOWN |
接下来解释一下列表1.2中的await方法:
列表1.2:HttpServer类中的await方法
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); } // 循环等待请求,当收到SHUTDOWN命令时结束循环 while (!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream();
// 创建请求对象并解析 Request request = new Request(input); request.parse();
// 创建响应对象 Response response = new Response(output); response.setRequest(request); response.sendStaticResource();
// 关闭socket socket.close();
//检查URL是否为关闭服务器的命令 shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); continue; } } } |
由于wait方法是java.lang.Object中的中线程处理的一个重要方法,所以使用await方法来命名来替代wait方法。
await方法从创建一个ServerSocket实例开始,紧接着进入while循环。
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); … 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方法创建了一个ex01.pyrmont.Request的对象并调用其parse方法来解析HTTP请求中的原始数据。
// 创建请求对象并解析 Request request = new Request(input); request.parse(); |
紧接着,await方法创建了一个响应对象,将参数Request对象设定入响应对象中,并调用其方法sendStaticResource。
// 创建响应对象 Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); |
最后,await方法关闭socket并调用Request对象的getUri方法来检查HTTP请求的URI是否为服务器终止命令。如果变量shutdown设定为true,则程序将跳出while循环。
// 关闭socket socket.close();
//检查URL是否为关闭服务器的命令 shutdown = request.getUri().equals(SHUTDOWN_COMMAND); |
2 Request类
ex01.pyrmont.Request类代表一个HTTP请求。该类的实例通过传入InputStream对象(从处理与客户端通信的socket对象中获得)构造得到。你可以调用InputStream对象中的某个read方法来获得HTTP请求中的原始数据。
Request类的实现在列表1.3中显示。该类中有两个公共方法parse和getUri,它们将分别在列表1.4和1.5中给出。
列表1.3:
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; } } |
parse方法用于解析HTTP请求中的原始数据。这个方法并没有完成很多功能。唯一有用的信息是HTTP请求中的URI,它可以通过调用似私有方法parseUri获得。parseUri方法将URI存储到变量uri中。调用公共方法getUri可以获得HTTP请求中的URI。
为了理解parse和parseUri方法的工作原理,你需要了解HTTP请求的结构,这部分内容在前面的章节“HTTP”中已经讨论过。在本章中,我们只对HTTP请求的第一部分感兴趣,即请求行。请求行以方法开始,随后紧跟着请求URI和协议版本,最后以回车换行结束。请求行中的各个元素通过空格来分隔。例如,某个使用GET方法请求index.html的请求行如下所示:
GET /index.html HTTP/1.1 |
parse方法从socket的InputStream对象(在构造Request对象中传递进来的)中读取全部的字节流,并将这些字节存放到缓冲器数组中。然后使用存放在缓冲字节数组的字节填入称为request的StringBuffer对象中,并将StringBuffer对象的String表示传递给方法parseUri。
列表1.4:Request类中的parse方法
public void parse() { // 从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。代码列表1.5显示了parseUri方法。该方法在请求中搜索第一个与第二个空行并从中获得URI。
列表1.5:Request类中的parseUri方法
private String parseUri(String requestString) { //URI是第index+1个字符到第index2-1个字符 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; } |
3 Response类
Ex01.pyrmont.Response类代表了一个HTTP响应,并在列表1.6中给出:
列表1.6:Response类
package ex01.pyrmont.Response;
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; } |
Response对象是通过传入从socket中获得的OutputStream由HttpServer类中的await方法构造的。
Response类有两个公共方法:setRequest和sendStaticResource。方法setRequest用于将Request对象传入Response对象中。
方法sendStaticResource用来发送一个静态资源,例如HTML文件。它首先调用java.io.File类的构造函数,同时传入代表父路径及子路径的参数。
File file = new File(HttpServer.WEB_ROOT, request.getUri()); |
然后检查该路径上的文件是否存在。如果存在,方法sendStaticResource通过传入的File对象创建一个java.io.InputStream对象。然后它调用FileInputStream中的read方法并将字节数组写入到输出流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()); |
4 运行程序
为了运行程序,在工作空间目录下输入如下:
java ex01.pyrmont.Response |
为了测试该程序,打开浏览器并在地址栏中输入:
http://localhost:8080/index.html |
你会看到浏览器中显示的网页index.html,如图1.1。
在控制台中,你可以看到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.mspowerpoint, 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服务器是如何工作的。本章中附带的例子包含了三个部分,但是这三个部分功能都是不完整的。然而这是一个很好的学习工具。下一章我们将讨论动态内容的处理。
本文出自邱栋的博客,转载请注明出处!