1.A Simple Web Server
1.1The HypertextTransfer Protocol
HTTP是一个协议,允许浏览器和服务端进行数据交互。是一份关于request和response的协议。客户端发送请求文件,然后服务端响应它。HTTP协议用可靠的TCP连接,默认端口号是80。HTTP协议的第一个版本是HTTP/0.9,后来是HTTP/1.0,现最新的版本是HTTP/1.1,是RFC2616定义的。
注意:这章只会简短地涉及HTTP/1.1,如果你想要了解更多,去读RFC2616。
在HTTP协议中,永远是客户端开启一个事务,通过建立连接并发送HTTP请求。服务端没有办法联系到客户端或者是回联客户端。同时,服务端和客户端必有一方提前终止连接。比如当你下载文件的时候,可以点击停止按键马上终止与服务端的连接。
HTTP请求
一个HTTP请求由三个部分组成:
[if !supportLists]1. [endif]方法——URI——协议版本
[if !supportLists]2. [endif]请求头
[if !supportLists]3. [endif]请求主体
一个标准的HTTP请求看起来是这样子的:
[if !vml]
[endif]其中POST是请求的方法,/example/default.jsp是URI,HTTP/1.1则是版本号。
每一个HTTP请求可以使用HTTP标准方法其中的一种,一共有七种:GET、POST、HEAD、OPTIONS、PUT、DELETE、TRACE。GET和POST是最为常用的。
URI完全定位了资源所在的位置,URI通常是指相对于服务器根目录的定位。所以它必须是以/开头,URL则是URI的一种。协议版本号则指明了当前用的HTTP协议版本。
请求头则是关于客户端和请求主体的一些有用信息,比如,它包含了客户端所用的语言,请求体的长度等等。每个请求头之间以回车分隔。
在请求头和请求体之间必须有一个回车,这是HTTP请求的重要规范。它告诉了服务器请求体从哪儿开始。甚至在有些书中,把这个回车当作了HTTP请求的第四个组成部分。
上面这个请求的请求体十分简单:lastName=Franks&firstName=Michael
在典型的HTTP请求头请求体往往会比这个长的多。
HTTP响应
与HTTP请求相似,响应也由三个部分组成:
[if !supportLists]1. [endif]协议——状态码——描述
[if !supportLists]2. [endif]响应头
[if !supportLists]3. [endif]响应体
以下是一个标准的HTTP响应:
[if !vml]
[endif]
状态码200代表一切都非常顺利。
同样,响应头与响应体之前也必须有一个回车符。
1.2The Socket Class
Socket是网络连接的一个端点。Socket使得应用可以向网络读写数据。两台计算机上的两个应用之间可以通过收发网络连接字节流来交互。当你发消息的时候,你不仅要知道对方的IP地址,还要知道对方app的socket的端口号。在JAVA中,socket用java.net.Socket来表示。
创建一个Socket,你可以用Socket类提供的许多构造方法。其中有一个方法:[if !vml]
[endif]
参数是host的名字和端口号,host可以是远程机器名字也可以是IP地址。端口号是远程app的端口号。比如连接yahoo.com的80端口,如上。
一旦你实例化出来一个Socket对象,你可以用它来发送或者接收字节流了。发送字节流,你要先用getOutputStream方法得到一个OutputStream对象。为了向远程app发送文本,你还需要一个PrintWriter对象。接收字节流的时候,你要用getInputStream得到一个InputStream对象。
下面的代码创建了一个可以与本地HTTP服务交互的socket,用了一个StringBuffer对象去接收响应并打印到控制台:
[if !vml]
[endif]
注意,为了得到正确的响应,你必须发送符合规范的HTTP请求。如果你前半部分好好学了,你肯定能看懂上面的请求是如何发送出去的。
1.3The ServerSocketClass
客户端socket用Socket类来表示,那么服务端呢,当你想要连接远程网络服务的时候,你需要一个Socket类的实例。这时,如果你想要实现服务端呢,比如一个HTTP服务或者一个FTP服务,你需要不同的方法了。你的服务端必须要一直待命,因为它不知道客户端什么时候会连接它,为了让你的服务能够一直待命,你需要java.net.ServerSocket类,这是服务端socket的一种实现。
ServerSocket和Socket不同的是,服务端总是等待着连接的那个。一旦服务端socket收到连接请求,马上会创建一个Socket实例去负责与客户端的通信。
创建一个服务端socket,你可以用ServerSocket提供的四种构造方法之一。你需要确定IP地址和服务监听的端口号。通常来说,IP地址是127.0.0.1,意味着这个服务端socket监听本地这台机器。这个IP地址也被称为绑定地址。服务端socket还有一个重要属性叫backlog,它是服务端能接收请求队列的最大长度,超过这个长度,服务端会拒绝连接。
[if !vml]
[endif]
第一个参数是端口号,第二个是backlog,第三个参数地址必须是InetAddress类,这个getByName是个静态方法,用来得到InetAddress。
一旦你有了一个ServerSocket实例,你可以让它监听并等待请求了。通过ServerSocket类的accept方法。这个方法只有在连接请求的时候会返回一个Socket类的实例。这个Socket类的实例可以用来收发字节流,从我们的客户端。
1.4The Application
我们的web应用由三个类组成:
HttpServer、Request、Response
我们应用的主方法在HttpServer类中。主方法创建一个HttpServer实例,并调用它的await方法,正如其名,这个方法会监听指定端口的HTTP请求,并且处理它们,并把响应发回客户端。它会一直工作直到下达关闭命令。
这个应用只能够发送静态资源,如存在于特定目录的HTML文件和图片文件,它同样在控制台打印每个HTTP请求的字节流,然而它并不会返回任何cookie给浏览器。
我们在下面的章节详细介绍这三个类。
1.5The HttpServerClass
HttpServer类是我们写的一个表示web服务的类。
[if !vml]
[endif] [if !vml]
[endif]
这个web服务可以为WEB_ROOT目录下的静态资源文件提供访问服务,你可以用这些静态资源文件来测试这个web服务。目录下还有几个servlet也可以供你测试,为请求一个静态资源,你可以在浏览器输入以下地址访问:[if !vml]
[endif]
你可以输入特定地址来关闭这个web服务,这个特定地址就是SHOWDOWN_COMMAND,这个await方法之所以叫await,是因为wait名字被占了,wait是java中多线程编程的一个重要方法。
await方法创建了一个ServerSocket类实例,然后进入一个while循环。While循环中的代码会阻塞在ServerSocket类的accept方法,直到有人请求时才会返回值,程序才会继续往下执行。当收到HTTP请求的时候,accept方法会返回一个Socket对象,这个Socket对象会提供OutputStream对象和InputStream对象。然后await方法会构造一个Request对象,然后调用Request对象的parse方法格式化请求数据。
之后,await方法会构造一个Response对象,把Request对象放进去,然后调用其sendStaticResource方法。
最后,调用Socket的close()方法关闭Socket对象,同时检查是否是关闭命令,如果是,把shutdown变量变成true,终止while循环。
1.6The Request Class
Request类表示一个HTTP请求。这个类的一个实例可以通过一个InputStream对象创建,InputStream对象是从Socket对象中拿过来的,Socket对象维持着与客户端的通信。你可以调用InputStream的read方法的一种,来拿到HTTP请求的原始数据。
以下是代码:
[if !vml]
[endif][if !vml]
[endif][if !vml]
[endif]
Parse方法负责解析HTTP请求中的原始数据,其实也没做太多事情。它提供的唯一信息就是HTTP请求的URI地址,还是通过调用parseUri方法获得的。ParseUri方法将URI地址存入uri变量。这样,我们就可以通过调用public方法getUri来得到HTTP请求的URI地址。
注意:第三章我们将讨论如何详细处理HTTP请求的原始数据。
要明白parse和parseUri方法是如何工作的,你必须对HTTP请求的结构了然于心,就像我们上一节讲的那样。在这里的话,我们将只讨论HTTP请求的第一部分:请求行,请求行由方法名开头,然后是URI地址,然后是协议版本号,然后是回车符。请求行的每个部分之间以空格分隔。举个例子,GET方法请求index.html文件的HTTP请求如下:
[if !vml]
[endif]
parse方法读取Socket的InputStream对象的所有字节流,并且将它缓存进一个byte数组里面。然后生成一个StringBuffer,去使用这个byte数组里面的byte,然后把StringBuffer转成String。
parseUri方法从请求行拿到时URI地址,它会搜索第一个空格和第二个空格之间的字符串,然后拿到它。
1.7TheResponse Class
[if !vml]
[endif][if !vml]
[endif]
Response类的构造方法接收一个OutputStream类型的参数。一个Response对象会在HttpServer类的await方法中被实例化,Socket将为它提供OutputStream。
Response类有两个public方法:setRequest和sendStaticResource。setRequest用于向Response对象传递Request对象。
sendStaticResource方法用来发送静态资源,比如HTML文件。它通过全路径(根目录+相对目录)实例化了一个java的File类。
然后它会检查文件是否真的存在,如果存在的话,通过刚才的java.io.File对象实例化一个FileInputStream对象。然后调用FileInputStream的read方法,调用OutputStream的write方法。需要注意的是,在此情形下,发送给浏览器的静态资源文件将是原始数据。
如果文件不存在的话,sendStaticResource方法向浏览器发送一个错误消息。
1.8Runningthe Application
在当前目录,输入以下命令来运行app.
[if !vml]
[endif]
在浏览器访问以下地址来测试app.
[if !vml]
[endif]
你会看到index.html在你的浏览器成功显示。
总结
在这一章你看到了一个web服务是如何工作的。这一章做的app也是个非常好的学习工具。下一章,我们将讨论如何处理动态内容。