本章讲解了Java Web Server是如果工作的。一个Web Server也被叫着HTTP(Hypertext Transfer Protocol) Server,因为它使用HTTP协议和客户端通讯——客户端通常是WEB浏览器。一个最基本的Java Web Server会用到二个重要的Class:java.net.Socket和java.net.ServerSocket,通过HTTP信息通信。因此,本章以讨论HTTP协议和二个Class开始。然后,继续讲解本章简单的Web Server应用Demo。
HTTP是一种允许Web Server和浏览器在互联网间发送和接收数据的协议。它是请求和应答协议。客户端请求一个文件,服务器响应该请求。HTTP默认连接使用可靠的TCP协议80端口。HTTP第一个版本是HTTP/0.9,续而被HTTP/1.0重写。替换HTTP/1.0的是当前版本HTTP/1.1,它被规定为RFC(Request for Comments)2616,可以从这里下载:http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf
注意:本章节只是简要的介绍HTTP 1.1,目的是帮助你理解Web Server上的传信息。如果对更多细节感兴趣,你可以阅读RFC2616。
在HTTP中,通常是一个客户端通过界定的事务连接发送一HTTP请求。Web服务器别无选择的和客户端接头,或者为客户端创建一回调连接。客户端和服务器均可以贸然地中断连接。比如:你点击浏览器上关闭按钮来中断一个文件的下载,实际上有效地中断了和Web服务器间的HTTP连接。
1.1.1 HTTP请求
一HTTP请求,有3部分组成:
1>请求方法—请求地址—协议/版本(Method—Uniform Resource Identifier (URI)—Protocol/Version)
2>.请求头部(Request headers)
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
请求的第一行是method—URI—protocol的版本:POST /examples/default.jsp HTTP/1.1。其中POST是请求方法;/examples/default.jsp是请求URI;HTTP/1.1是协议版本。
每一个HTTP请求,均可以使用HTTP协议标准中多个请求方法之一。HTTP 1.1支持7种:GET、POST、HEAD、OPTIONS、PUT、DELETE和TRACE。其中,GET和POST在Internet应用中使用最普遍。
URI指定一Internet中资源完整路径。URI通常代表中服务的Root根目录,因此它总是以/开始。URL实际上是URI的一种类型(参阅:http://www.ietf.org/rfc/rfc2396.txt )。协议版本表示当前使用的HTTP协议版本。
请求头部包含些有用的信息:客户端环境和请求实体(entity body)。比如:浏览器设置的语言、请求实体的长度,等等。每一请求头部都被换行符隔离开(Each header is separated by a carriage return/linefeed (CRLF) sequence)。
在请求头部和请求实体之间,有一空白行(a blank line (CRLF)),这是HTTP请求的重要格式。CRLF已在告诉HTTP服务器,这是请求体的开始。在一些Internet编程书籍中,把CRLF当做HTTP请求的第四部分。
在上面的请求中,请求体是很简单的:lastName=Franks&firstName=Michael。通常典型的HTTP请求中请求体还是比较长的。
1.1.2 HTTP响应
和HTTP请求类似,HTTP响应也包含3部分:
1> 响应状态(Protocol—Status code—Description)
2> 响应头部(Response headers)
3> 响应实体(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 Exampletitle>
head>
<body>
Welcome to Brainy Software
body>
html>
响应头部第一行和请求头部第一行是相似的。第一行告诉你:用的是HTTP 1.1,请求成功(200表示成功)。
响应头部像请求头部一样包含中有用的信息。响应实体中是HTML内容。响应实体和响应头部之间是被换行符分割的。
1.2 Socket类
“套接字”是一种端点式网络连接。“套接字”在网络中可以提供读和写的功能。在2台计算机之间2个应用通过连接可以彼此发送和接收字节流进行通信。从一个应用发送信息给另一应用,你需要知道另应用所在机器的IP和端口(Port)。在java中,“套接字”是通过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类实例后,你可以使用它发送和接收字节流。发送字节流,你必须先得通过调用Socke类的getOutputStream()来获得一个java.io.OutputStream对象。发送文本信息给远程应用,你通常得通过OutputStream对象返回构造一java.io.PrintWriter对象。从连接的另一端接收字节流,你得调用Socket类的getInputStream()方法来获得一java.io.InputStream.对象。
如下代码片段构造的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();
注意,想从Web Server获得正确的响应,你得先发送一遵循HTTP协议的HTTP请求。如果你阅读了前面一节“HTTP协议”,你应该可以理解上面代码中的HTTP请求。
现在,你可以使用本书中的com.brainysoftware.pyrmont.util.HttpSniffer发送HTTP请求,并显示请求应答信息。使用这个java程序,你必须得联网。另外,还有可能不可以运行成功运行此程序,如果你开启了防火墙。
1.3 ServerSocket类
Socket类代表着客户端套接字。例如,每当你想连接远程服务应用时创建的Socket。现在如果你想实现一类似HTTP Server或FTP Server的服务器应用,你需要使用不同于创建Socket的方式。这是因为服务应用不知道客户端什么时候会连接它,所以它不得不一直保持运行状态(不断的监听)。介于此,你需要使用java.net.ServerSocket类。这代表着服务端“套接字”。
ServerSocket不同于Socket。ServerSocket扮演着等待客户端发起请求连接的角色。一旦ServerSocket获得一连接请求,它就创建一Socket实例去处理和该客户端的交互。
创建一ServerSocket,你需要使用ServerSocket类提供的4个构造方法中任何一个。你需要指定要监听的IP地址和端口号。典型的,IP地址是127.0.0.1,意味着此服务应用监听着本机器。The IP address the server socket is listening on is referred to as the binding address. ServerSocket另一重要属性“backlog”,它代表着可支持的最大请求数,当超过时,ServerSocket将拒绝接收请求。
ServerSocket类的一构造方法如下:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
注意这个构造函数,bindingAddress必须是java.net.InetAddress的实例。通过调用getByName静态方法,很容易创建InetAddress对象,如下代码:
InetAddress.getByName("127.0.0.1");
如下代码中构造的ServerSocket监听着本地机器80端口,属性backlog等于1:
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
当你拥有ServerSocket实例时,你可以让它等待连接请求——来自指定IP地址和端口号监听处。你只要调用ServerSocket类的accept()方法即可。此方法只会在有连接请求时才会返回,返回一Socket实例。此Socket对象实例可用来发送和接收来自客户端应用的字节流,前一节“Socket类”已介绍过。实际上,本章应用Demo也仅使用了accept()方法。
1.4 应用Demo
本章web Sever应用位于ex01.pyrmont package下,它包含3个类:
1》 HttpServer
2》 Request
3》 Response
本应用的入口处是HttpSever类的main()方法。此main()方法创建了一HttpServer实例,并调用了它的await()方法。从字面意思看,await()方法在指定端口处等待HTTP请求,处理它们,返回给客户端一个请求响应。它一直不断地监听等待直到收到shutdown命令。
本应用不可以处理除指定文件路径目录下HTML文件、图片文件之外的静态资源。它在控制台上显示了来自HTTP字节流信息请求。然而,它没有返回诸如日期或cookie给浏览器。
现在,我们在如下的小节中学习这3个类。
1.4.1 HttpServer类
HttpServer类表示一个Web Server。如下代码介绍:
package ex01.pyrmont;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author : Ares
* @createTime : Aug 21, 2012 9:45:01 PM
* @version : 1.0
* @description :
*/
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() {
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 Server可以服务于public static final WEB_ROOT指定目录或其子目录路径下静态资源请求。WEB_ROOT初始化如下:
public static final String WEB_ROOT = System.getProperty("user.dir")
+ File.separator + "webroot";
代码中包含一叫webroot目录,它里面含有一些可以用来测试本应用的静态资源。在后续章节中,你也会发现此目录还含有一些servlet——用来测试各个应用Demo。
请求一静态资源,在你的浏览器上输入如下类型URL:http://machineName:port/staticResource
如果,你是在运行此应用之外的一台机器上发送请求,machineName是运行此应用机器的名称或IP地址。如果是在同一台机器上,你可以使用localhost表示machineName,port是8080,staticResource是请求资源的名称,它必须在WEB_ROOT目录中。
例如,你想在同一台机器上测试此应用,你想像HttpServer请求index.html文件,使用如下URL:http://localhost:8080/index.html
停止server,你在浏览器上发送shutdown命令即可。shutdown命令在HttpServer中通过SHUTDOWN静态常量来定义的:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
所以,停止server,你可以使用如下URL:http://localhost:8080/SHUTDOWN
现在,我们看看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) {...}
在while循环中,ServerSocket的accept(),只有在8080端口接收到HTTP请求时才会返回:socket = serverSocket.accept();
当接收到HTTP请求时,await()方法中包含的java.io.InputStream和java.io.OutputStream对象,将会从accept()方法返回的Socket实例中得到:
input = socket.getInputStream();
output = socket.getOutputStream();
await()方法创建ex01.pyrmont.Request一对象,并调用它的parse()方法去解析HTTP请求数据:
// create Request object and parse
Request request = new Request(input);
request.parse();
然后,await()方法创建Response对象,并调用它sendStaticResource()方法:
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
最后,await()方法关闭Socket,并且通过Request对象的getUri()检查HTTP请求URL是否是shutdown命令。如果是,shutdown变量赋值true,并且程序退出while循环:
// Close the socket
socket.close();
// check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
1.4.2 Request类
应用Demo中ex01.pyrmont.Request表示一HTTP请求。此对象实例是通过传入在Socket中获得InputStream对象创建的,它负责处理与客户端通讯。通过调用InputStream对象的read()方法获取HTTP请求数据。Request代码如下:
package ex01.pyrmont;
import java.io.IOException;
import java.io.InputStream;
/**
* @author : Ares
* @createTime : Aug 21, 2012 9:48:45 PM
* @version : 1.0
* @description :
*/
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
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());
}
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;
}
public String getUri() {
return uri;
}
}
其中parse()方法解析HTTP请求数据。在此方法中,其他的就没做什么了。唯一信息,通过私有方法parseUri()获取到HTTP请求的URI。parseUri()方法通过uri变量储存URI。共有方法getUri()被用来调用,以便获取到此HTTP请求URI。
有关HTTP请求数据处理将在第三章应用Demo中出现。
理解parse()和parseUri()方法是如何工作的,你需要知道HTTP请求的结果,这个已经在上面小节介绍过了——“Hypertext Transfer Protocol (HTTP)协议”。此章节,我们只关心HTTP请求的第一部分——请求行。一请求行,开始与请求方法,随后是请求URI和协议版本,结束与CRLF换行符。请求行中每一元素被空格符分开。例如,一请求行,以get方式请求index.html,格式是:GET /index.html HTTP/1.1
1.4.3 Response类
应用Demo中ex01.pyrmont.Response类代表中HTTP请求。代码如下:
package ex01.pyrmont;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author : Ares
* @createTime : Aug 21, 2012 9:51:37 PM
* @version : 1.0
* @description :
*
* 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"
+ "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();
}
}
}
}
首先注意构造函数接收java.io.OutputStream对象,如下:
public Response(OutputStream output) {
this.output = output;
}
一个Response对象,是通过HttpServer类的await(),通过传人从socket中获得的OutputStream对象。
Response对象包含2个共有方法: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对象写入字节数组输出。注意这里向浏览器输出的静态数据是未做处理的原始数据。
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());
1.4.4 运行Demo
运行HttpServer。打开浏览器,输入http://localhost:8080/index.html
在你的浏览器上,你将看到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 CLR1.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 CLR1.1.4322)
Host: localhost:8080
Connection: Keep-Alive
1.5 小结
在本章中你已经看到了一个简单的Web Server是如何工作的。本章中应用Demo只包含3个类,并且不足实用。尽管如此,它依然是很好的学习工具。下一章将讨论如何处理动态内容。
Reference:https://blog.csdn.net/LoveJavaYDJ/article/details/54019164