本文先讲解一下Java web server都是怎么工作的。web server也叫HTTP server——顾名思义它是用HTTP协议和客户端交互的。客户端一般就是各种各样的浏览器了。相信所有朋友都清楚这个基本事实,否则你也不会看到这个系列文章了。
基于Java的web server必然用到两个极其重要的类:java.net.Socket和java.net.ServerSocket,然后以HTTP消息进行交互。
1. HTTP协议简介(The Hypertext Transfer Protocol)
HTTP是用于web server和浏览器之间发送、接收数据的基础核心协议——客户端发起请求然后服务端进行响应。它使用应答式TCP连接,默认情况下监听在80端口上。第一版协议是HTTP/0.9,然后又被HTTP/1.0重写了,随后HTTP/1.1又替换掉了HTTP/1.0——当前我们使用的正是HTTP/1.1,它的协议文件叫RFC2616,有兴趣的可以去w3网站上下载回来研究一下,对你理解和掌握HTTP以及整个互联网的核心有着无可替代的作用。接地气的说法就是:明白了RFC2616,你就明白了易筋经和九阳神功,自此之后横行天下无所顾忌。。。
HTTP里,永远都是客户端主动发起请求,然后服务端才有可能和它建立连接。web server永远不会主动连接或者回调客户端,但是两边都可以直接断开连接。
总结成一句话就是:服务端永远处于绝对优势地位,客户端你不连我我就绝对不会连你,只有你客户端发起请求了,我服务端才会和你连接,当然,心情不好时我也照样可以不对你的请求做出任何响应。像极了男人追女人的恋爱过程吧。。。
1.1 HTTP请求
它由以下部分组成:
第一部分:方式 — URI — 协议/版本号
第二部分:请求头
第三部分:实体数据
典型例如如下:
1:
POST /baidu.com/小苹果歌词.txt HTTP/1.1
2:
3:
Accept: text/plain; text/html
4:
Accept-Language: en-gb
5:
Connection: Keep-Alive
6:
Host: localhost
7:
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
8:
Content-Length: 33
9:
Content-Type: application/x-www-form-urlencoded
10:
Accept-Encoding: gzip, deflate
11:
12:
lastName=Franks&firstName=Michael
对应的第一部分就是这段请求信息的第一行,下面再详细讲解下:
POST /baidu.com/小苹果歌词.txt HTTP/1.1
这一行的“POST”是请求方式,“/baidu.com/小苹果歌词.txt”是对应的URI,而“HTTP/1.1”就是对应的协议/版本号了。
HTTP协议定义了很多请求方式,每一个HTTP请求都可以使用其中的一种。HTTP 1.1 支持7种请求类型:GET,POST,HEAD,OPTIONS,PUT,DELETE以及TRACE。一般情况下,我们只用到GET和POST就足够了。
URI完整的指定了一个网络资源,一般情况下它都是相对于服务器的根目录进行资源定位,你看到的URI才经常以斜杠“/”开头,当然,通常我们只知道URL,URL实际上只是URI的一种而已(细节可研究RFC2396协议)。第一行的协议版本号,顾名思义就是当前使用的是哪版HTTP协议了。
请求头包含了一些关于客户端环境和请求体的有用信息。例如,它可以指示浏览器使用的语言、请求体的数据长度等等。每一个请求头和请求体之间都通过回车换行符(CRLF)分隔。
请求头和请求体之间的空白行(CRLF)是HTTP请求格式中不可或缺的一部分,它用于指明请求体数据开始的w位置。甚至在一些网络编程书中,这个空白行(CRLF)直接被当作了HTTP请求标准格式的第四个组成部分。
在上面那个例子中,请求体的实体数据只有简单的一行,不过实际应用中实体数据往往比较多:
lastName=Franks&firstName=Michael
1.2 HTTP响应
和HTTP请求相似,HTTP响应也由三部分组成:
第一部分:协议 -- 状态码 --描述
第二部分:响应头
第三部分:响应体
举个例子:
1:
HTTP/1.1 200 OK
2:
Server: Microsoft-IIS/4.0
3:
Date: Mon, 5 Jan 2004 13:13:33 GMT
4:
Content-Type: text/html
5:
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
6:
Content-Length: 112
7:
8:
<
html
>
9:
<
head
>
10:
<
title
>
HTTP Response Example
</
title
>
11:
</
head
>
12:
<
body
>
13:
Welcome to Brainy Software
14:
</
body
>
15:
</
html
>
响应头第一行和请求头极为相似,它指示了当前使用的协议是HTTP/1.1版本,而且请求成功了(200=成功),一切顺利。
响应头包含的有用信息类似于请求头的。响应体是一段HTML内容,响应头和响应体之间以CRLF分隔。
2. Socket类
socket是网络连接的一个端点,它赋予应用程序读写网络流的能力。两台电脑通过发送和接收基于连接的字节流来进行交流沟通。若要发消息给另一个程序,你需要知道这个程序socket的ip地址和端口号。在java里,socket指的是java.net.Socket类。
你可以使用Socket类的诸多构造器中任意一个来创建socket,下面这个构造器接收主机名和端口号作为参数:
public Socket (java.lang.String host, int port)
在此,host可以是主机名或者ip地址,端口号就是对应的程序占用的端口。例如,要想连接80端口上的yahoo.com,你需要如下构造方式:
new Socket("yahoo.com", 80);
一旦成功创建Socket实例,你就可以用它来发送接收字节流了。要发送字节流,你必须首先调用Socket类的getOutputStream方法获取java.io.OutputStream对象,要发送纯文本的话,我们通常构造一个OutputStream对象返回的java.io.PrintWriter对象。要接收字节流,你就应该调用Socket类的getInputStream方法来获取 java.io.InputStream。
下面就是代码展示了,各位看官请好:
1:
Socket socket =
new
Socket(
"127.0.0.1"
,
"8080"
);
2:
OutputStream os = socket.getOutputStream();
3:
boolean autoflush =
true
;
4:
PrintWriter
out
=
new
PrintWriter( socket.getOutputStream(), autoflush);
5:
BufferedReader
in
=
new
BufferedReader(
new
InputStreamReader( socket.getInputstream() ));
6:
7:
// 向web server发送HTTP请求
8:
out
.println(
"GET /index.jsp HTTP/1.1"
);
9:
out
.println(
"Host: localhost:8080"
);
10:
out
.println(
"Connection: Close"
);
11:
out
.println();
12:
13:
// 读取响应
14:
boolean loop =
true
;
15:
StringBuffer sb =
new
StringBuffer(8096);
16:
while
(loop) {
17:
if
(
in
.ready() ) {
18:
int
i=0;
19:
while
(i!=-1) {
20:
i =
in
.read();
21:
sb.append((
char
) i);
22:
}
23:
loop =
false
;
24:
}
25:
Thread.currentThread().sleep(50);
26:
}
27:
28:
// 输出响应内容
29:
System.
out
.println(sb.toString());
30:
socket.close();
3. ServerSocket类
Socket类代表的是客户端Socket,例如IE浏览器、chrome、火狐、safari等发起的连接。如果你想实现一个服务器应用程序,像HTTP server或者FTP server的话,你就必须使用不同的方法了。这是因为服务端根本不知道客户端会发起请求建立连接,它必须永不停歇的等待客户端请求。为此,你必须使用java.net.ServerSocket类,它是服务端socket的实现。
ServerSocket不同于Socket,服务端的ServerSocket必须一直等着客户端请求的到来。一旦server socket接到连接请求,它必须创建一个Socket实例来处理和客户端的交互。
要创建server socket,你得用ServerSocket类提供的四个构造器之一。它需要你指明IP地址和server socket要监听的端口号。经典的127.0.0.1意味着server socket将监听本机。server socket监听的IP地址通常也叫绑定地址。另一个重要的属性是backlog,它意味着接入的连接请求超过此数值之后server socket就会拒绝后续请求。
public ServerSocket(int port, int backlog, InetAddress bindingAddress);
值得注意的是,这个构造器的绑定地址必须是java.net.InetAddress类的实例。构造InetAddress对象的简易方法就是调用它的静态方法getByname,并传一个主机名字符创参数,如下所示:
InetAddress.getByName("127.0.0.1");
下面这行代码构造了一个ServerSocket,监听本机8080端口,同时backlog为1:
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦ServerSocket实例构造完成,它就可以一直监听在绑定地址的对应端口上等待请求的到来,你需要做的就是调用ServerSocket类的accept方法来启动这个监听过程。这个方法只返回何时产生了连接请求并且返回值是一个Sokcet类的实例。然后通过这个Socket可以发送和接收字节流。
4. 动手实现自己的山寨版web server
这个山寨web server由三个类组成:HttpServer、Request、Response。
HttpServer的main方法创建一个HttpServer实例并调用它的await方法,顾名思义,这个await方法一直等着请求到来,然后处理请求、发送响应信息到客户端。它会一直等,直到程序终止或停机。
这个山寨版的server目前只能发送静态资源,它会在控制台显示HTTP请求的字节流,但不能发送任何响应头,比如data、cookie之类的。
4.1 HTTPServer.java
1:
import java.io.File;
2:
import java.io.IOException;
3:
import java.io.InputStream;
4:
import java.io.OutputStream;
5:
import java.net.InetAddress;
6:
import java.net.ServerSocket;
7:
import java.net.Socket;
8:
9:
public
class
HttpServer {
10:
11:
/**
12:
* WEB_ROOT is the directory where our HTML and other files reside. For this
13:
* package, WEB_ROOT is the "webroot" directory under the working directory.
14:
* The working directory is the location in the file system from where the
15:
* java command was invoked.
16:
*/
17:
public
static
final String WEB_ROOT = System.getProperty(
"user.dir"
) + File.separator +
"webroot"
;
18:
19:
// 关机命令
20:
private
static
final String SHUTDOWN_COMMAND =
"/SHUTDOWN"
;
21:
22:
// the shutdown command received
23:
private
boolean shutdown =
false
;
24:
25:
public
static
void
main(String[] args) {
26:
HttpServer server =
new
HttpServer();
27:
server.await();
28:
}
29:
30:
public
void
await() {
31:
ServerSocket serverSocket =
null
;
32:
int
port = 8080;
33:
try
{
34:
serverSocket =
new
ServerSocket(port, 1, InetAddress.getByName(
"127.0.0.1"
));
35:
}
catch
(IOException e) {
36:
e.printStackTrace();
37:
System.exit(1);
38:
}
39:
// 轮询是否有请求进来
40:
while
(!shutdown) {
41:
Socket socket =
null
;
42:
InputStream input =
null
;
43:
OutputStream output =
null
;
44:
try
{
45:
socket = serverSocket.accept();
46:
input = socket.getInputStream();
47:
output = socket.getOutputStream();
48:
// create Request object and parse
49:
Request request =
new
Request(input);
50:
request.parse();
51:
// create Response object
52:
Response response =
new
Response(output);
53:
response.setRequest(request);
54:
response.sendStaticResource();
55:
// Close the socket
56:
socket.close();
57:
// check if the previous URI is a shutdown command
58:
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
59:
}
catch
(Exception e) {
60:
e.printStackTrace();
61:
continue
;
62:
}
63:
}
64:
}
65:
}
4.2 Request.java
1:
package ex01.pyrmont;
2:
3:
import java.io.IOException;
4:
import java.io.InputStream;
5:
6:
public
class
Request {
7:
8:
private
InputStream input;
9:
10:
private
String uri;
11:
12:
public
Request(InputStream input) {
13:
this
.input = input;
14:
}
15:
16:
public
void
parse() {
17:
// Read a set of characters from the socket
18:
StringBuffer request =
new
StringBuffer(2048);
19:
int
i;
20:
byte
[] buffer =
new
byte
[2048];
21:
try
{
22:
i = input.read(buffer);
23:
}
catch
(IOException e) {
24:
e.printStackTrace();
25:
i = -1;
26:
}
27:
for
(
int
j = 0; j < i; j++) {
28:
request.append((
char
)buffer[j]);
29:
}
30:
System.
out
.print(request.toString());
31:
uri = parseUri(request.toString());
32:
}
33:
34:
private
String parseUri(String requestString) {
35:
int
index1, index2;
36:
index1 = requestString.indexOf(
' '
);
37:
if
(index1 != -1) {
38:
index2 = requestString.indexOf(
' '
, index1 + 1);
39:
if
(index2 > index1)
40:
return
requestString.substring(index1 + 1, index2);
41:
}
42:
return
null
;
43:
}
44:
45:
public
String getUri() {
46:
return
uri;
47:
}
48:
}
49:
4.3 Response.java
1:
package ex01.pyrmont;
2:
3:
import java.io.File;
4:
import java.io.FileInputStream;
5:
import java.io.IOException;
6:
import java.io.OutputStream;
7:
8:
/*
9:
* HTTP Response = Status-Line (( general-header | response-header |
10:
* entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP
11:
* Status-Code SP Reason-Phrase CRLF
12:
*/
13:
public
class
Response {
14:
15:
private
static
final
int
BUFFER_SIZE = 1024;
16:
17:
Request request;
18:
19:
OutputStream output;
20:
21:
public
Response(OutputStream output) {
22:
this
.output = output;
23:
}
24:
25:
public
void
setRequest(Request request) {
26:
this
.request = request;
27:
}
28:
29:
public
void
sendStaticResource() throws IOException {
30:
byte
[] bytes =
new
byte
[BUFFER_SIZE];
31:
FileInputStream fis =
null
;
32:
try
{
33:
File file =
new
File(HttpServer.WEB_ROOT, request.getUri());
34:
if
(file.exists()) {
35:
fis =
new
FileInputStream(file);
36:
int
ch = fis.read(bytes, 0, BUFFER_SIZE);
37:
while
(ch != -1) {
38:
output.write(bytes, 0, ch);
39:
ch = fis.read(bytes, 0, BUFFER_SIZE);
40:
}
41:
}
else
{
42:
// file not found
43:
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>"
;
44:
output.write(errorMessage.getBytes());
45:
}
46:
}
catch
(Exception e) {
47:
// thrown if cannot instantiate a File object
48:
System.
out
.println(e.toString());
49:
}
finally
{
50:
if
(fis !=
null
)
51:
fis.close();
52:
}
53:
}
54:
}
55:
5. 总结
本文讲解了web server的基本原理,同时代码贴出来了一个粗糙山寨的web server。它只有三个类构成,当然不是全功能的,不过呢,毕竟刚开始,我们会不断的逐步完善这个web server,到本系列结束时,基本上就有一个完整的web server了。
文档信息
- 译者:张大爷
- 原文网址: http://www.cnblogs.com/flance/
- 版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

