分享

浏览器工作原理 3

 nbxming 2011-09-09

WWW 的工作基于客户机/服务器计算模型,由Web 浏览器(客户机)和Web服务器(服务 器)构成,两者之间采用超文本传送协议(HTTP)进行通信, HTTP协议的作用原理包括四 个步骤:连接,请求,应答。根据上述HTTP协议的作用原理,本文实现了GET请求的Web服 务器程序的方法,通过创建 TcpListener类对象,监听端口8080; 等待、接受客户机连 接到端口8080; 创建与socket字相关联的输入流和输出流;然后,读取客户机的请求信 息,若请求类型是GET,则从请求信息中获取所访问的HTML文件名,如果HTML文件存在, 则打开HTML文件,把HTTP头信息和 HTML文件内容通过socket传回给Web浏览器,然后关闭 文件。否则发送错误信息给Web浏览器。最后,关闭与相应Web浏览器连接的socket 字。 一、HTTP协议的作用原理 WWW是以Internet作为传输媒介的一个应用系统,WWW网上最基本的传输单位是 Web网 页。WWW的工作基于客户机/服务器计算模型,由Web 浏览器(客户机)和Web服务器(服务 器)构成,两者之间采用超文本传送协议(HTTP)进行通信。HTTP协议是基于TCP/IP协议 之上的协议,是Web浏览器和Web服务器之间的应用层协议,是通用的、无状态的、面向对 象的协议。HTTP协议的作用原理包括四个步骤: 连接:Web浏览器与Web服务器建立连接,打开一个称为socket(套接字)的虚拟文 件,此文件的建立标志着连接建立成功。 请求:Web浏览器通过socket向Web服务器提交请求。HTTP的请求一般是GET或POST命 令(POST用于FORM参数的传递)。GET命令的格式为: GET 路径/文件名 HTTP/1.0 文件名指出所访问的文件,HTTP/1.0指出Web浏览器使用的HTTP版本。 应答:Web浏览器提交请求后,通过HTTP协议传送给Web服务器。Web服务器接到后, 进行事务处理,处理结果又通过HTTP传回给Web浏览器,从而在Web浏览器上显示出所请求 的页面。 例:假设客户机与www.:8080/mydir/index.html建立了连接,就会发 送GET命令: GET /mydir/index.html HTTP/1.0。主机名为www.的Web服 务器从它的文档空间中搜索子目录mydir的文件index.html。如果找到该文件,Web服务器 把该文件内容传送给相应的Web浏览器。 为了告知 Web浏览器传送内容的类型,Web服务器首先传送一些HTTP头信息,然后传 送具体内容(即HTTP体信息),HTTP头信息和HTTP体信息之间用一个空行分开。 常用的HTTP头信息有: ① HTTP 1.0 200 OK 这是Web服务器应答的第一行,列出服务器正在运行的HTTP版本号和应答代码。代码 “200 OK”表示请求完成。 ② MIME_Version:1.0 它指示MIME类型的版本。 ③ content_type:类型 这个头信息非常重要,它指示HTTP体信息的MIME类型。如:content_type:text/html 指示传送的数据是HTML文档。 ④ content_length:长度值 它指示HTTP体信息的长度(字节)。 关闭连接:当应答结束后,Web浏览器与Web服务器必须断开,以保证其它Web浏览器 能够与Web服务器建立连接。

Java WEB服务器 工作原理

一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器。一个基于JavaWeb服务器用到二个重要的类,java.net.Socketjava.net.ServerSocket,并通过HTTP消息通信。因此,本文从讨论HTTP与这二个类开始,然后我将解释一个与本文相关的简单的Web应用。

The Hypertext Transfer Protocol(HTTP)

HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议。它是一个请求、响应协议--客户端发出一个请求,服务器响应这个请求。HTTP运用可靠的TCP连接,通常用的TCP80端口。它的第一个版本是HTTP/0.9 ,然后被HTTP/1.0取代。当前的版本是HTTP/1.1,由RFC2616(.pdf)定义。

本节主要对应HTTP1.1,足够使你充分理解由Web服务器程序发出的消息。如果你对更加详细的知识有兴趣,可以参考 RFC2616

HTTP中,客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系,也不能给客户端发出一个回叫连接。客户端与服务器端都可以提前中断一个连接。例如,当用一个浏览器下载一个文件时,你可以通过点击“停止”键来中断文件的下载,关闭与服务器的HTTP连接。

HTTP请求

一个HTTP请求包含三个部分:

Method-URI-Protocol/Version 方法-地址-版本

Request header 请求头

Entity body 请求实体

下面是一个 HTTP 请求实例:

POST /servlet/default.jsp HTTP/1.1

Accept: text/plain; text/html

Accept-Language: en-gb

Connection: Keep-Alive

Host: localhost

Referer: http://localhost/ch8/SendDetails.htm

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

The Method-URI-Protocol/Version 在这个请求的第一行:

POST /servlet/default.jsp HTTP/1.1

其中 POST 是请求的类型。每个客户端HTTP请求可以是HTTP规范中指定的许多请求类型中的一种。HTTP1.1支持七种类型的请求,它们是GETPOSTHEADOPTIONSPUTDELETETRACE。其中GETPOSTInternet 应用中经常用到的二种请求类型。

URI 完整地指定了 Internet 资源。一个URI通常被解析为相对服务器的根目录。这样,它应该总是以一个 '/'前缀开始。一个URL实际上是 URI 的一种类型。

Version 指的是该 HTTP 请求所用到的HTTP协议版本。

请求头包含了客户端环境与请求实体的一些有用的信息。例如它包含浏览器设定的语言、实体的长度等等。每条请求头用回车换行符(CRLF)分开。

一个非常重要的空行分开了请求头与实体,它标志着实体内容的开始。一些 Internet 开发书籍认为这个 CRLF空行是 HTTP 请求的第四个部分。

在上面的 HTTP 请求中,实体只是简单以下的一行:

LastName=Franks&FirstName=Michael

在一个典型的 HTTP 请求中,请求实体内容会长得多。

HTTP 响应

与请求相似,HTTP 响应也由三部分组成:

Protocol-Status code-Description 协议状态 描述代码

Response headers 响应头

Entity body 响应实体

以下是一个 HTTP 响应的实例:

HTTP/1.1 200 OK

Server: Microsoft-IIS/4.0

Date: Mon, 3 Jan 1998 13:13:33 GMT

Content-Type: text/html

Last-Modified: Mon, 11 Jan 1998 13:23:42 GMT

Content-Length: 112

Welcome to Brainy Software

响应头的第一行类似请求头的第一行,告诉你所用的协议是 HTTP 1.1 ,请求成功(200=success),以及没有任何问题。

响应头类似请求头也包含了一些有用的信息。响应的实体响应本身的 HTML 内容。头与实体之间由回车换行的空行(CRLF)分开。

Socket 

一个 socket 是一个网络连接的端点,它使得一个应用可以从网络读与写。在不同电脑上的二个应用软件能够通过收发字节流而彼此通信。要发一个信息到另一个应用程序,你需要知道它的IP地址,以及它的 socket 端口号。在 Java 中,一个 socket  java.net.Socket 来实现。

要创建一个 socket ,你可以用 Socket 类中几个构建方法中的一个。其中一个接受主机名与端口号作为参数:

new Socket("yahoo.com", 80);

一旦你成功地创建了一个 Socket 类的实例,你就可以用它去发送与接收字节流了。要发送字节流,你需要呼叫Socket 类的 getOutputStream 方法来得到一个 java.io.OutputSteam 对象。要发送文本到远程的程序,你通常需要从返回的 OutputStream 创建一个 java.io.PrintWriter 对象。要从连接的另一端接收字节流,你需要呼叫Socket 类的 getInputStream 方法,它返回一个 java.io.InputStream 对象。

以下代码创建一个可以与本地 HTTP 服务器通信的 socket (127.0.0.1 表示一个本地的主机),发送一个 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;

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请求。

ServerSocket

Socket类描述的是“客户端”socket,当你需要创建与远程服务程序连接时需要用到它。如果你想实现一个服务程序,如HTTP服务器或者FTP服务器,则需要另外不同的方法。这是因为你的服务器必须随时服务,它不知道什么时候会有一个客户端程序需要连接它。

因为这个目的,你需要用到java.net.ServerSocket这个类,它是服务器端socket的一个实现。服务器端socket等待来自客户端的连接请求。一旦它收到一个连接请求,它创建一个socket实例来与客户端进行通信。

要创建服务器端socket,需要用到ServerSocket类提供的四个构建方法中的一个。你需要指定服务器端socket侦听的IP地址与端口号。比较典型地,这个IP地址可以是127.0.0.1,意思是该服务器端socket侦听的是本地机器。服务器端socket侦听的IP地址指的是绑定地址。服务器端socket另一个重要的属性是队列长度,即它拒绝请求前所接受的最大请求排队长度。

ServerSocket类的构建方法之一如下:

public ServerSocket(int port,int backLog,InetAddress bindingAddress);

对于这个构建方法,绑定地址必须是 java.net.InetAddress 类的实例。创建一个 InetAddress类的对象的简单方法是呼叫其静态方法 getByName,传递一个包含主机名的字符串。

InetAddress.getByName("127.0.0.1");

以下行的代码创建了一个服务器端socket ,它侦听本地机器的 8080 端口,限制队列长度为 1 

new ServerSocket(8080,1,InetAddress.getByName("127.0.0.1"));

一旦有了一个 ServerSocket 实例,就可以通过呼叫其 accept 方法来让它等待进来的链接请求。这个方法只有当接收到请求时才返回,它返回的是 Socket 类的实例。这个 Socket 对象就可以用来从客户端应用程序发送与接收字节流,正如上节据说的那样。实际上,accept 方法是本文例子中用到的唯一方法。

应用实例

我们的web服务器程序是 ex01.pyrmont 包的一部分,它包含三个类:HttpServer;Request;Response

整个程序的入口(静态main方法)是HttpServer类。它创建一个HttpServer的实例,并呼叫其await方法。正如名字表达的,await在一个特定的端口等待HTTP请求,处理它们,并返回响应给客户端。它保持等待状态,直到收到停止命令。(用方法名await代替wait,是因为System中有一个重要的与线程相关的方法)

这个程序只从一个特定的目录发送静态资源,如 HTML 与图像文件。它只支持没有文件头(如日期与 cookie)的情况。现在我们将在如下的几节中看一下这三个类。

HttpServer

HttpServer 实现了一个 web 服务器,它可以提供(serve)特定目录及其子目录下的静态资源。这个特定的目录由 public static final WEB_ROOT 指定。

WEB_ROOT 初始化如下:

public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

代码列表中包含了一具叫做 webroot 的目录,里面有一些静态的资源,你可以用来测试本应用。

为了请求一个静态的资源,在浏览器的地址栏输入如是地址:http://machinename:port/staticResources

如果你从不同的机器上发送请求到运行本应用的机器,则machinename是运行应用机器的机器名或IP地址,port8080staticResources是被请求的文件名称,它必须包含在 WEB_ROOT目录内。

例如,如果你用同一台电脑来测试这个应用,你想要HttpServer发送index.html这个文件,用以下的地址:http://localhost:8080/index.html

要停止服务,只需要从浏览器发送一个停止(shutdown)命令,即在浏览器的地址栏输入 host:port字段后,加上预先定义好的字符串。在我们的HttpServer类中,停止命令被定义为SHUTDOWN,一个 static final变量。

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

因此,要停止服务,你可以这样:http://localhost:8080/SHUTDOWN

现在,让我们看一下列表 1.1 中给出的 await 方法。代码列表后面将对这段代码做一些解释。

Listing 1.1. The HttpServer class' 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;     } }

} await 方法以创建一个 ServerSocket 实例开始,然后进入一个 while 的循环。 erverSocket = 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.OutputStream input = socket.getInputStream(); output = socket.getOutputStream();然后await 方法创建一个 Request 对象,呼叫它的 parse 方法来解析这个原始的 HTTP 请求: // create Request object and parse Request request = new Request(input);

request.parse();下一步,await 方法创建一个 Response 对象并把 Request 对象设置给它,呼叫它的sendStaticResource 方法: // create Response object Response response = new Response(output); response.setRequest(request); response.sendStaticResource();最后,await 方法关闭 Socket ,呼叫Request  getUri 方法来检查 HTTP 请求的地址是否是一个停止命令。如果是,则 shutdown 变量被设置为true ,程序退出 while 循环: // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND);

Request 

Request 类对应 HTTP 请求。创建这个类的实例,并传给它从 Socket 获得的 InputStream 对象,从而捕获与客户端的通信。呼叫 InputStream 对象的 read 方法中的一个就可以得到 HTTP 请求的原始数据。

Request 类有二个 public 方法 parse  getUriparse 方法解析 HTTP 请求的原始数据。它做的事情不多--唯一它使之有效的信息是 HTTP 请求的 URI ,这个通过呼叫私有方法 parseUri 来获得。parseUri 方法把URI 作为一个变量。调用 getUri 方法可以得到 HTTP 请求的 URI 

要明白 parse  parseUri 的工作原理,你需要知道 HTTP 请求的结构,由 RFC2616 定义。

一个 HTTP 请求包括三个部分:Request lineHeadersMessage body 

现在,我们只需要关注 HTTP 请求的第一部分--请求行。请求行以方法记号开始,接着是请求的 URI 与协议版本,以回车换行符结束。请求行的元素之间以空格分开。例如,一个用 GET 方法的 index.html 文件的请求行如下:

GET /index.html HTTP/1.1

parse 方法从 socket  InputStream 传递给 Request 对象中读取字节流,把这个字节数组存在缓冲里。然后,它把 buffer 字节数组里的字节放入叫做 request  StringBuffer 对象中,再把 StringBuffer 替换成String 传递给 parseUri 方法。parse 方法的代码如列表 1.2 Listing

1.2. The Request class' 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< buffer.length;j++)

request.append((char) buffer[j]);

}System.out.print(request.toString());

uri = parseUri(request.toString());

}

parseUri 方法查找请求行的第一个与第二个空格,从而从请求行获得了 URI 。列表 1.3 展示了 parseUri 方法的代码。

Listing 1.3. The Request class' 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;

}

 

Response 

Response 类描述 HTTP 响应。它的构建方法接受 OutputStream 对象,如下:

public Response(OutputStream output) {

this.output = output;

}

Response 对象通过传递从 socket 获得的 OutputStream 对象到 HttpServer 类的 await 方法而创建。

Response 类有二个公共方法 setRequest  setStaticResource setRequest 用来传递 Request 对象到Response 对象。它比较简单,代码如列表 1.4 所示:

Listing 1.4. The Response class' setRequest method

public void setRequest(Request request) {

this.request = request;

}

sendStaticResource 方法用来发送静态的资源,例如 HTML 文件。它的实现如列表 1.5 所示:

Listing 1.5. The Response class' sendStaticResource method

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(); } } SendStaticResource 方法非常简单。它首先通过传递父与子目录到 File 类的构建方法从而实例化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" +"File Not Found"; output.write(errorMessage.getBytes());

编译与运行应用程序 要编辑与运行本文的应用,首先你需要解压源码 zip 文件。直接解压出来的目录被称为工作目录,它有三个子目录:src/classes/lib/。要编译应用,从工作目录输入如下命令: javac -d . src/ex01/pyrmont/*.java -d 选项把结果写到当前目录,而不是 src/ 目录。 要运行应用,在当前工作目录输入如下命令: java ex01.pyrmont.HttpServer测试这个应用,打开你的浏览器,在地址栏输入如下地址:http://localhost:8080/index.html

你将在你的浏览器看到 MSIE 4.01; MSIE 4.01; Windows 98) Host: localhost:8080 Connection: Keep-Alive总结 在这篇文章中,你看到了一个简单的 web 服务器的工作原理。本文相关的应用只包括了三个类,功能是不全面的。然而,它仍不失为一个好的学习工具。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多