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浏览器。一个基于Java的Web服务器用到二个重要的类,java.net.Socket与java.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支持七种类型的请求,它们是GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE。其中GET与POST是Internet 应用中经常用到的二种请求类型。 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地址,port是8080,staticResources是被请求的文件名称,它必须包含在 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 与 getUri。parse 方法解析 HTTP 请求的原始数据。它做的事情不多--唯一它使之有效的信息是 HTTP 请求的 URI ,这个通过呼叫私有方法 parseUri 来获得。parseUri 方法把URI 作为一个变量。调用 getUri 方法可以得到 HTTP 请求的 URI 。 要明白 parse 与 parseUri 的工作原理,你需要知道 HTTP 请求的结构,由 RFC2616 定义。 一个 HTTP 请求包括三个部分:Request line;Headers;Message 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 服务器的工作原理。本文相关的应用只包括了三个类,功能是不全面的。然而,它仍不失为一个好的学习工具。 |
|