|
第一章一个简单的web server
这章分析java web servers怎么工作。一个web 服务器也被称作一个超文本传输协议(HTTP)服务器,因为它使用HTTP来与客户端通信,客户端通常是web浏览器。一个基于java的web服务器使用2个重要的类:java.net.Socket和java.net.ServerSocket.它们之间的通信是通过HTTP消息完成的。很自然我们这章由讨论HTTP和这两个类为开始。最后,继续分析这个简单web服务器应用结束本章内容。
The Hypertext Transfer Protocol (HTTP)
HTTP是种允许web服务器和浏览器之间通过Internet互相发送和接收数据的协议。是一种基于请求/相应的协议。客户端向服务器发出请求来请求一个文件,服务器相应地对这个请求作出回应。HTTP使用可靠地TCP连接—其默认为TCP80端口号。第一个HTTP版本号是HTTP/0.9,然后被HTTP/1.0重新修改。代替HTTP/1.0版本的是现在的HTTP/1.1版本,1.1版本被定义为Request for Comments(RFC)2616 。
提示:这包含HTTP 1.1的部分只是简单的帮助你理解web服务器应用传递消息。如果想了解跟多,可以阅读 RFC 2016。
HTTP总是客户端通过建立连接和发送一个HTTP请求来初始化一个事务。Web服务器是不能够去连接一个客户端或者发出一个(callback connection)回调连接给客户端。客户端和服务器都可以提前结束一个连接。例如,当使用一个web浏览器你可以按浏览器上的stop按钮来停止下载一个文件,有效地关闭与web服务器的HTTP连接。
HTTP Requests
一个HTTP请求包含3个组成部分:
方法-统一资源定位符(uri)-协议/版本Method-Uniform ResourceIdentifer-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
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
//这行是CRLF
lastName=Franks&firstName=Michael
POST是请求方法,/example/defaul.jsp 是请求的URI HTTP/1.1是协议的和版本部分。
每一个HTTP请求可以使用许多请求方法的一种作为HTTP标准。在HTTP1.1支持7中类型的请求:GET.POST,HEAD,OPTIONS,PUT,DELETE和TRACE。
GET和POST是在Internet应用中使用最普遍的。
URI完整地指定了Intetnet上的资源。一个URI通常被解析为服务器相关的根目录。这样,它总是可以使用一个前斜杠‘/’开始。一个URL(Uniform Resource Locator)本质上是URI的一种类型。
协议版本表示被使用的HTTP的协议版本号。
请求头部包含关于客户端环境和实体正文的有用的信息。例如,它包含浏览器设置的语言,正文的长度等等。每一个头部都被一个回车换行符(CRLF)序列分开。
在头部和实体正文有一个空行(CRLF),这个空行对HTTP求个格式尤为重要。CRLF告诉HTTP服务器正文从哪里开始。在一些Internet编程书籍中介绍这个换行符(CRLF)是HTTP请求的第四组成部分。
在前面的HTTP请求中,实体正文只是简单的一行内容:
lastName=Franks&firstName=Michael
在一个电影的HTTP请求中,实体内容可以很容易地变得更长
HTTP Responses
与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
//(CRLF)
<html>
<head>
<title>HTTP Response Example</title>
</head>
<body> Welcome to Brainy Software </body>
</html>
响应头的第一行与请求头的第一行很相似。第一行告诉我们协议是使用的HTTP1.1版本,请求成功(200表示请求成功),所有的请求都完成(ok)。
响应头报班的有用信息和请求头相似。在响应的实体正文是响应自己的HTML内容。头部和正文实体同样被一个(CRLF)序列隔开。
The Socket Class
一个socket是一个网络连接的终端。一个socket运行一个应用在网络上能够读和写。两个软件应用安装在2台不同的计算机上能够通过一个连接发送和接收字节流来相互通信。从你的应用发送一条消息到另一个应用,你需要了解另一个应用的IP地址和socket端口号。在java中,一个socket用java.net.Socket类表示。
你可以使用Socket类提供的诸多构造方法中的一种来创建一个socket。
其中一个构造方法接收一个主机名和端口号:
Public Socket (java.lang.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对象可以接收响应并把它打印在控制台(console)上。
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,例如:一个你想要在任何时候连接到一个远程服务器应用的socket。现在,如果你想要事项一个服务器应用,就像HTTP服务器或FTP服务器,你需要一种不同的途径来实现。这是因为你的服务器必须在任何时刻都处于工作(监听)中,而且它不知道什么时候会有客户端应用来连接它。为了是你的应用能够处于时刻地工作(监听状态)中,你需要使用java.net.ServerSocket类。这是一个服务器socket的实现。
ServerSocket是与Socket有区别的。服务器socket的作用是等待客户端的连接请求。一旦服务器socket得到一个连接请求,它就会创建一个Socket实例来处理与客户端之间的通信。
你需要使用ServerSocket类提供的4个构造函数的其中一个来创建一个服务器Socket。你需要明确IP地址和服务器socket将要监听的端口号。典型地,IP地址为127.0.0.1,意味着服务器socket将要监听本地机器。IP地址是服务器socket正在监听的作为绑定地址。作为服务器的另一重要特性是它的backlog。在服务器socket开始拒绝接收客户端到来的请求的最大连接请求队列的长度。
ServerSocket类的一个构造函数:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
注意这个构造函数,它的绑定地址必须是一个java.net.InetAddress的实例。一个简单的方法来创建一个InetAddress对象是通过调用它的静态方法 getByName,通过一个包含主机名的字符串来构建,例如:
InetAddress.getByName("127.0.0.1");
下面一行代码是构建一个在8080端口监听的ServerSocket。它的backlog为1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦你拥有一个ServerSocket实例,你就可以通知它在绑定的地址和服务器socket正在监听的端口号等待客户端的链接请求。即调用ServerSocket类的accept方法。这个方法将只在当有一个连接请求和它的返回值是一个Socket类的实例的时返回。这个Socket对象就可以用来发送和接收客户端的字节流。
The Application
我的web服务器应用包括下面3个类:
HttpServer
Request
Response
应用的入口(静态主方法 main )可以再HttpServlet类中找到。主方法创建一个HttpServer的实例然后调用它的等待方法。顾名思义,就是在指定的端口等待HTTP请求、处理请求和发送响应给客户端。一直到它接收到shutdown命名后才停止等待。
这个应用仅仅能发送像HTML文件和图像文件类似的位于某个目录下的静态资源。它也把收到HTTP请求字节流显示在控制台(console)上。不管怎样,它不能发送任何像日期或cookies这样的头信息到浏览器。
下面让我们来看看这3个类
The HttpServer Class
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 System.getProperty("user.dir") +File.separator + "webroot";
代码中有一个叫做webroot的一个目录。这个目录里面包含一些静态资源你可以使用它们来测试这个应用。
获取静态资源。你可以在浏览器URL中输入:
http://machineName:port/staticResource
如果你发送一个请求道另一台运行这个应用的计算机上,machineName是主机名或IP地址。如果是本机运行,你可以使用localhost在当做machineName使用。Port端口号是8080,staticResource是你请求的文件名,它必须位于WEB_ROOT目录下。
例如:如果你你使用本机来测试应用程序,你想要HttpServer对象发送index.html文件给你,你使用下面的地址:
http://localhost:8080/index.html
停止服务器,你从浏览器端URL地址上发送一个事先预定义好的字符串shutdown命令。这个命令位于host:port部分的后面。
例如:
http://localhost:8080/SHUTDOWN
await的方法是用来代替wait方法的,因为在java.lang.Object中有一个处理线程的wait方法。
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.OututStream对象。
input = socket.getInputStream();
output = socket.getOutputStream();
然后await方法会创建一个Request对象调用它的parse方法来解析HTTP请求未处理的数据。
// create Request object and parse Request request = new Request(input);
request.parse ();
然后,await方法创建一个Response对象,把Requset对象传给Response。随之调用sendStaticResource方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
最后,await方法关闭Socket和调用Request的getUri方法来检查HTTP请求是否一个shutdown命令。如果是,shutdown变量被设置成为true,程序退出while循环。
// Close the socket
socket.close ();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
The Request
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;
}
parse方法是解析HTTP请求的未处理的的数据。这个方法并没有做太多事情。HTTP请求的URI通过调用private的parseUri方法。ParseUri方法在uri变量里保存了URI。Public的getUri方法被调用用来返回HTTP请求的URI。
The Response Class
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方法,这个await方法传递了一个从socket里获得的OutputStream对象来创建Resqonse对象。
Response类有2个public方法:setRequest和sendStaticResource方法。setRequest方法用来传递一个Request对象到Response对象中。
SendStaticResource方法用来发送像HTML文件一样的静态资源。java.io.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());
Running the Application
从工作目录下运行这个程序,输入下面:
java ex01.pyrmont.HttpServer
测试程序,打开浏览器输入URL:
http://localhost:8080/index.html
在控制台会得到像下面的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
第一章 完 |
|