译者 jarfield
博客 http://jarfield.javaeye.com ? ???
- 超文本传输协议(HTTP )
- HTTP 请求
- HTTP 响应
- Socket 类
- ServerSocket 类
-
应用程序
- HttpServer 类
- Request 类
- Response 类
- 运行应用程序
- 总结
??? 本章解释了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 使用可靠的TCP 连接――默认端口是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???
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 /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???
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/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)??
public Socket (java.lang.String host, int port)
??? 其中,host 是远程机器的名称或IP 地址,port 是远程应用的端口号。例如,要连接上 yahoo.com 的80 端口,你可以创建下面的 Socket 对象:
- new?Socket("yahoo.com",?80);???
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 响应。另外,这段代码还创建了一个 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();??
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.HttpSniffer 类发送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);???
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
??? 注意这个构造函数,binding address 必须是一个 java.net.InetAddress 对象。创建 InetAddress 对象的一个简单方法就是调用该类的静态方法 getByName ,并把主机名作为 String 对象传给该方法,就像下面这样:
- InetAddress.getByName("127.0.0.1");???
InetAddress.getByName("127.0.0.1");
??? 下面这行代码创建了一个监听本地8080 端口的、backlog 为1 的ServerSocket 对象。
- new?ServerSocket(8080,?1,?InetAddress.getByName("127.0.0.1"));??
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()?{ ??
- ????... ??
- ??} ??
- }???
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; ??
- ????} ??
- ??} ??
- }??
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";??
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
??? 这段代码中有一个名为webroot 的目录,该目录包含了可以用于测试该应用的的静态资源。你还可以从该目录下找到几个用于测试下一章应用的servlet 。
??? 如果要请求一个静态资源,你可以在浏览器的地址栏中敲入以下URL :
- http://machineName:port/staticResource???
http://machineName:port/staticResource
??? 如果你从另一台机器上发送请求, machineName 应该是该应用所在机器的主机名或IP 地址。如果你的浏览器运行在同一台机器上,可以使用 localhost 作为 machineName 。端口是8080 , staticResource 是被请求的文件(静态资源)名,该文件必须位于 WEB_ROOT 下。
??? 例如,你在同一台机器上测试该应用,想让HttpServer 发送文件index.html ,你可以使用下面的URL : ?
??? 如果要停止服务器,你可以通过特定的URL从浏览器发送shutdown命令:host:port的后面加上预先定义的、表示shutdown的字符串即可。 HttpServer 类的静态常量 SHUTDOWN 定义了 shutdown命令:
- private?static?final?String?SHUTDOWN_COMMAND?=?"/SHUTDOWN";??
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
??? 因此,如果要停止服务器,你可以使用下面的URL :
- http://localhost:8080/SHUTDOWN???
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)?{ ??
- ??... ??
- }??
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();??
socket = serverSocket.accept();
?
??? 收到一个请求后, await 方法从 accept 方法返回的 Socket 对象中获取了 java.io.InputStream 对象和 java.io.OutputStream 对象。
- input?=?socket.getInputStream(); ??
- output?=?socket.getOutputStream();???
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?();???
// 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();??
// 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);???
// Close the socket socket