您的位置:首页 > 编程语言 > Java开发

Java Web服务器都是如何工作的

2007-11-17 15:15 309 查看
 

How Java Web Servers Work

by Budi Kurniawan04/23/2003编辑批注: [b]本文改编自 Budi [b]个人出版的关于TOMCAT[b]内部实现原理的书.[b]你可以到他的网站上找到更多这方面的信息.[/b][/b][/b][/b]web server 也叫 Hypertext Transfer Protocol (HTTP) server, 因为它使用的是HTTP协议与客户端通信, 通常是指那些 web 浏览器. 基于 Java 的web server 使用2个重要的类,
java.net.Socket
java.net.ServerSocket
, 进行HTTP消息通信. 因此,本文先讨论HTTP协议和这2个类. 之后, 我会讲解本书附带的一个简单的web server 程序.

The Hypertext Transfer Protocol (HTTP)

HTTP 协议能够让 web servers 和浏览器通过 Internet 发送和接收数据. 它是一种请求-响应式的协议—客户端发起一个请求,服务器响应这个请求. HTTP 使用可信赖的TCP 连接, 默认使用的是 TCP 的80 端口. HTTP协议的第一个版本是 HTTP/0.9, 然后是 HTTP/1.0. 当前版本是由 RFC 2616(.pdf) 定义的HTTP/1.1.这部分简短的介绍了一下 HTTP 1.1; 足可以让你理解 web server 应用程序发出的消息. 如果你有兴趣了解更多的细节,请阅读 RFC 2616.使用 HTTP 的时候, 客户端总是通过建立一个连接来开始一个事务,然后发送一个 HTTP 请求. server 不会主动联系客户端,也不会建立一个回调连接到客户端(is in no position to contact a client or to make a callback connection to the client). 客户端和服务器都可以提前中止一个连接. 例如, 当使用 web 浏览器的时候, 你可以点击浏览器上的停止 按钮结束文件的下载过程, 有效关闭与 web 服务器的HTTP 连接.

HTTP Requests

一个 HTTP 请求由3部分组成:Method-URI-Protocol/VersionRequest headersEntity body一个 HTTP 请求的例子如下:
method-URI-Protocol/Version 出现在请求的第1行.
POST
是请求的方式,
/servlet/default.jsp
表示 URI,
HTTP/1.1
是 Protocol/Version 这段的值.每个 HTTP 请求都可以使用诸多请求方式中的任何一种, 这些请求方式都由HTTP 标准制定. HTTP 1.1 支持7种请求:
GET
,
POST
,
HEAD
,
OPTIONS
,
PUT
,
DELETE
, 和
TRACE
.
GET
POST
是在 Internet 程序中最常使用的2种请求.URI 完整的指定了一个 Internet 资源. URI 通常以相对于服务器的根目录来进行解析. 因此, 应该总是以
/
开始
. URL 实际是 URI 的一种. 协议版本表示正在使用的HTTP 协议的版本号.请求头包含了客户端环境和请求主体的有用信息. 例如, 包含浏览器的语言设置,请求主体的长度, 等. 每个请求头被 CRLF 序列分隔开.在头和主体之间有一个非常重要的空白行 (CRLF sequence) . 这一行标志着请求主体的开始. 一些 Internet 编程书认为这个 CRLF 是 HTTP 请求的第4个组成部分.在前面的HTTP 请求, 请求主体只是简单的如下这么一行:
在一个更典型的HTTP 请求里, 请求主体可以比这更长.

HTTP Responses

类似于请求, HTTP 响应也由3部分组成:Protocol-Status code-DescriptionResponse headersEntity body下面是一个 HTTP 响应的例子:
响应头的第一行很类似于请求头的第一行. 第一行告诉你使用的协议是 HTTP version 1.1, 请求成功 (200 = success), 一切都完全正确.类似于请求头, 响应头也包含了有用的信息. 响应主体是 HTML 内容. 头和主体也以 CRLFs 序列分隔.

The Socket Class

一个socket是一次网络连接的一个端点. socket 能够使程序从网络中读取数据, 向网络中发送数据. 处于2台不同机器上的2个程序能够通过一次连接发送和接收字节流而进行通信. 要想发送一条消息给另一个程序, 你必须知道它的IP 地址, 和它的socket 端口. Java 中的socket 使用
java.net.Socket
类来表示.要想创建一个 socket, 你可以使用
Socket
类的诸多构造器中任一种. 其中一个构造器接受主机名和端口号:
host
是远程机器名或者 IP 地址,
port
是远程程序的端口号. 例如, 要连接 yahoo.com 的80 端口, 你应当如下构建 socket:
一旦你成功创建了 
Socket
类的一个实例, 你就可以使用它发送和接收字节流. 发送字节流的时候, 你必须首先调用
Socket
类的
getOutputStream
方法获得
java.io.OutputStream
对象. 当向远程程序发送文本的时候, 通常要用返回得到的
OutputStream
构造一个
java.io.PrintWriter
对象. 当要接收来自连接另一端的字节流的时候, 需调用
Socket
类的
getInputStream
方法得到一个
java.io.InputStream
对象
.下面代码片段建立了一个 socket 与本地 HTTP server (127.0.0.1 意味着一个本地主机) 通信, 发送 HTTP 请求, 从服务器接收响应. 它创建了一个
StringBuffer
对象容纳响应内容, 然后打印到控制台.
注意要想从 web 服务器获得正确的响应, 你必须发送一个遵从 HTTP 协议的HTTP 请求. 如果你已经阅读过了前面章节, "The Hypertext Transfer Protocol (HTTP)," 你就会理解上面代码里的HTTP 请求.

The
ServerSocket
Class

Socket
类代表一个 “客户端“("client")socket; 就是当你想连接到一个远端服务器程序时所构建的socket. 如果你想实现一个服务器程序, 例如 HTTP server 或者 FTP server, 你需要一种不同的方法. 这是因为你的服务器必须持久存在随时准备接收请求, 因为它根本不知道什么时候一个客户端程序要连接它.因为这个原因, 你必须使用
java.net.ServerSocket
类. 这是服务器端 socket 的一个实现. 服务器端 socket 等待来自客户端的连接请求. 每当它接受了连接请求, 它就会创建一个
Socket
实例去处理和客户端的通信.为创建一个服务器端 socket, 你必须使用
ServerSocket
类提供的四个构造器中的一个. 你需要指定 IP 地址和这个服务器端 socket 监听的端口号码. 典型地, IP 地址会是 127.0.0.1, 意思是这个服务器端 socket 将在本地机器上监听. 服务器端 socket 所在的机器的IP 地址也被称做绑定地址. 服务器端 socket 的另一个重要属性是它的backlog, 它是指在服务器端 socket 开始拒绝新来的请求前所能忍受的连接请求队列的最大长度.
ServerSocket
类的其中一个构造器有如下方法声明:
对于这个构造器, 绑定地址必须是 
java.net.InetAddress
的一个实例
. 一种简单的构建
InetAddress
对象的方式是通过调用它的静态方法
getByName
, 传递一个包含主机名称的字符串给它:
下面这行代码构造了一个 
ServerSocket,
监听本地机器的
8080 端口, backlog值设置为1.
一旦你已经有了一个 
ServerSocket
实例, 你就可以调用它的
accept
方法告诉它等待进入的连接请求. 这个方法只有在有连接请求的时候才会有返回. 它返回一个
Socket
类的实例. 这个
Socket
对象随后就可以用来发送和接收来自客户端程序的字节流, 就象在 The Socket Class 里解释的那样. 实际上, 在本文所附带的这个程序里
accept
方法是唯一用到的方法.

The Application

我们的web 服务器程序位于包
ex01.pyrmont,
3
个类组成
:
HttpServer
Request
Response
这个程序的入口点 (the static
main
method) 在
HttpServer
类里. 它创建一个
HttpServer
实例并调用它的
await
方法. 就象名字所隐含的意思那样, 在指定的端口上
await
等待 HTTP 请求, 处理请求, 然后向这些客户端发回响应. 它会一直保持等待直到接收到一个关闭命令为止. (方法名称使用
await
而不是
wait
是因为
wait
System.Object
类中一个非常重要的用于处理多线程的方法.)这个程序只发送来自指定目录下的静态资源, 象 HTML 和图片文件. 它不支持头 (像时间或者 cookies).现在, 在下面子章节里, 我们来看看这3个类.

The
HttpServer
Class

HttpServer
类代表一个 web server, 能提供在由public static final 变量
WEB_ROOT
表示的目录下所找到的静态资源以及这个目录下的所有子目录下的静态资源.
WEB_ROOT
按如下方式初始化:
上面代码包含一个叫 webroot 的目录, 包含一些静态资源用于测试这个程序. 你也会发现一个 servlet, 在我的下一篇文章里将会用到, "How Servlet Containers Work."要想请求一个静态资源, 请在你的浏览器地址栏中敲入如下 URL:
如果你在另一台不同的机器上跑着你的程序并发送请求, 那么 
machineName
是运行这个程序的计算机名称或者 IP 地址. 如果你使用的浏览器是在同一台机器上, 你可以使用
localhost
作为
machineName
. 端口是 8080,
staticResource
是要请求的文件名, 并且该文件必须位于
WEB_ROOT
.例如, 如果你使用同一台计算机测试这个程序, 并且你想要求
HttpServer
返回 index.html 文件, 使用如下 URL:
要想停止服务器, 在地址栏中敲入预先定义好的关闭字符串发送关闭命令. 关闭命令在 
HttpServer
类中定义为
SHUTDOWN
static final 变量:
因此, 要关闭服务器, 你可以使用:
现在, 我们来看看 Listing 1.1 中给出的
await
方法. 对代码的解释就在这个 listing 的后面.Listing 1.1. The
HttpServer
class'
await
method
await
开始先创建一个
ServerSocket
实例, 然后进入一个
while
循环.
while 循环在 
ServerSocket
accept
方法处停止
, 这个方法只有当在 8080 端口接收到了一个 HTTP请求时才会返回 Socket 对象:
接收到一个请求后, 
await
方法从
accept
方法返回的
Socket
实例获取一个
java.io.InputStream
java.io.OutputStream
对象.
await
方法接着会创建
Request
对象并调用
parse
方法解析原始的HTTP 请求.
再往下, 
await
方法创建了一个
Response
对象, 把
Request
对象给它, 然后调用
sendStaticResource
方法.
最后, 
await
方法关闭了
Socket
并调用 Request 的
getUri
方法检查是否这个 HTTP 请求是一个关闭命令. 如果是, shutdown 变量被设置为
true,
程序退出
while
循环.

The
Request
Class

Request
类代表一个 HTTP 请求. 这个类的实例通过传递一个
InputStream
对象而构建,
InputStream
对象正是从处理与客户端通信的Socket 处得到的. 调用
InputStream
对象的一个
read
方法获取 HTTP 请求的原始数据.
Request
类有2个 public 方法:
parse
getUri
.
parse
方法解析 HTTP 请求的原始数据. 除此以外它不会做得更多--它的工作成果中唯一有用的是 HTTP 请求的URI, 它也是通过调用 private 方法
parseUri
而获得这个
URI
.
parseUri
方法使用
uri
变量存储 URI. 调用 public
getUri
方法以返回得到 HTTP 请求的URI.要想搞明白
parse
parseUri
方法是怎样工作的, 你需要知道 HTTP 请求的结构, 这是由 RFC 2616(.pdf) 定义的.一个 HTTP 请求包含3部分:Request lineHeadersMessage body现在, 我们只对 HTTP 请求的第一部分感兴趣, request line. request line 以请求方式开头, 跟着是请求 URI 和协议版本, 以 (CRLF) 结束. request line 中的各部分以空格分隔. 例如, 对于请求 index.html 文件的请求的request line 使用
GET
方式:
parse
方法从传递给
Request
对象的得自socket 的InputStream 读取全部字节流, 保存这些字节数组到 buffer. 然后将 buffer 字节数组中的bytes 移到一个叫 request 的
StringBuffer
对象中, 接着把这个
StringBuffer
对象的
String
形式传递给
parseUri
方法.Listing 1.2 给出了
parse
方法.Listing 1.2. The
Request
class'
parse
method
parseUri
方法随后从 request line 获得 URI. Listing 1.3 展示了
parseUri
方法.
parseUri
方法搜索 request 中的第一个和第二个空白, 2者之间就是 URI.Listing 1.3.
Request
类的
parseUri
方法

The
Response
Class

Response
类表示 HTTP 响应. 它的构造器接收一个
OutputStream
对象, 如下:
Response
对象在
HttpServer
类的
await
方法中通过传递
OutputStream
对象(从 socket 获得)来构造.
Response
类有2个公共方法:
setRequest
sendStaticResource
.
setRequest
方法用于传递一个
Request
对象给
Response
对象. 就象 Listing 1.4 中的代码那样简单.Listing 1.4. The
Response
class'
setRequest
method
sendStaticResource
方法用于发送静态资源, 象一个 HTML 文件. Listing 1.5 给出了它的实现.Listing 1.5. The
Response
class'
sendStaticResource
method
sendStaticResource
方法非常简单. 首先传递父和孩子路径给 File 类的构造器实例化一个
java.io.File
类.
然后检查文件是否存在. 如果存在, 
sendStaticResource
方法通过传递
File
对象构造一个
java.io.FileInputStream
对象. 接着调用
FileInputStream
read
方法, 向
OutputStream
输出写入 byte 数组. 注意, 在这种情况下, 静态资源的内容将作为原始数据发送给浏览器.
如果文件不存在, 
sendStaticResource
方法发送一个错误消息给浏览器.

Compiling and Running the Application

要想编译和运行程序, 首先需要解压包含本文示例程序的.zip 文件. 解压 .zip 文件所在的目录叫做工作目录, 而且会有3个子目录: src/, classes/, 和 lib/. 在工作路径下敲入如下命令编译程序:
-d
选项将结果写到当前而非 src/目录下.在工作目录敲下如下输入运行程序:
测试程序时, 打开你的浏览器并在地址栏中输入如下 URL :
你会看到 index.html 页面展示于你的浏览器中, 如 Figure 1.Figure 1. The output from the web server在控制台上, 你会看见如下输出:

Summary

在这篇文章里, 你已经看到了一个简单的web server是如何工作. 本文所带的程序只由3个类组成, 功能并不完善. 不管怎样, 它是好的学习工具.Budi Kurniawan is a senior J2EE architect and author. 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息