HTTP(Hypertext Transfer Protocol,超级文本传输协议)是网络应用层的协议,建立在TCP/IP基础上。HTTP使用可靠的TCP连接,默认端口是80端口。HTTP的第1个版本是HTTP/0.9,后来发展到了HTTP/1,现在最新的版本是HTTP/2。值得注意的是,在目前的实际运用中,HTTP/2并没有完全取代HTTP/1,而是这两种协议在网络上并存,也就是说,许多Web服务器和浏览器之间既可以通过HTTP/1通信,也可以通过HTTP/2通信。
HTTP/1.1对应的RFC文档为RFC2616,它对HTTP/1.1做了详细的阐述。HTTP/2对应的RFC文档为RFC7540,它对HTTP/2协议做了详细的阐述。
HTTP基于客户/服务器模式,客户端主动发出HTTP请求,服务器接收HTTP请求,返回HTTP响应结果。HTTP对HTTP请求以及响应的格式做了明确的规定。
1、Htpp简介
当用户打开浏览器,输入一个URL地址,就能接收到远程HTTP服务器发送过来的网页。浏览器就是最常见的HTTP客户程序。如下图所示,HTTP客户程序必须先发出一个HTTP请求,然后才能接收到来自HTTP服务器的响应。
HTTP客户程序和HTTP服务器分别由不同的软件开发商提供,它们都可以用任意的编程语言编写。用VC编写的HTTP客户程序能否与用Java编写的HTTP服务器顺利通信呢?答案是肯定的。HTTP严格规定了HTTP请求和HTTP响应的数据格式,只要HTTP服务器与客户程序都遵守HTTP,就能彼此看得懂对方发送的消息。
1.1、HTTP请求格式
HTTP规定,HTTP请求由3部分构成,分别是:
- 请求方法、URI、HTTP的版本
- 请求头(Request Header)
- 请求正文(Request Content)
下面是一个HTTP请求的例子:
1.请求方式、URI、HTTP的版本
HTTP请求的第1行包括请求方式、URI和协议版本这3项内容,以空格分开:
在以上代码中,“POST” 表示请求方式,“/hello”表示URI,“HTTP/1.1”表示HTTP的版本。
根据HTTP,HTTP请求可以使用多种请求方式,主要包括以下几种:
- GET:这种请求方式最为常见,客户程序通过这种请求方式访问服务器上的一个文档,服务器把文档发送给客户程序。
- POST:客户程序可通过这种方式发送大量信息给服务器。在HTTP请求中除了包含要访问的文档的URI,还包括大量的请求正文,这些请求正文中通常会包含大量HTML表单数据。
- HEAD:客户程序和服务器之间交流一些内部数据,服务器不会返回具体的文档。当使用GET和POST方法时,服务器最后都将特定的文档返回给客户程序。而HEAD请求方式则不同,它仅仅交流一些内部数据,这些数据不会影响用户浏览网页的过程,可以说对用户是透明的。HEAD请求方式通常不单独使用,而是为其他请求方式起辅助作用。一些搜索引擎使用HEAD请求方式来获得网页的标志信息,还有一些HTTP服务器在进行安全认证时,用这个方式来传递认证信息。
- PUT:客户程序通过这种方式把文档上传给服务器。
- DELETE:客户程序通过这种方式来删除远程服务器上的某个文档。客户程序可以利用PUT和DELETE请求方式来管理远程服务器上的文档。
GET和POST请求方式最常用,而PUT和DELETE请求方式并不常用,因而不少HTTP服务器并不支持PUT和DELETE请求方式。
统一资源定位符(Universal Resource Identifier,URI)用于标识要访问的网络资源。在HTTP请求中,通常只要给出相对于服务器的根目录的相对目录即可,因此以“/”开头。
HTTP请求的第1行的最后一部分内容为客户程序使用的HTTP的版本。
2.请求头(Request Header)
请求头包含许多有关客户端环境和请求正文的有用信息。例如,请求头可以声明浏览器的类型、所用的语言、请求正文的类型,以及请求正文的长度等。
3.请求正文(Request Content)
HTTP规定,请求头和请求正文之间必须以空行分割(即只有CRLF符号的行),这个空行非常重要,它表示请求头已经结束,接下来是请求正文。请求正文中可以包含客户以POST方式提交的表单数据。
在以上HTTP请求例子中,请求正文只有一行内容。在实际应用中,HTTP请求的正文可以包含更多的内容。
下面是一个简单的HTTP客户程序,它发送的HTTP请求信息就严格遵守上述规范。
1.2、HTTP响应格式
和HTTP请求相似,HTTP响应也由3部分构成,分别是:
- HTTP的版本、状态代码、描述
- 响应头(Response Header)
- 响应正文(Response Content)
下面是一个HTTP响应的例子:
1.HTTP的版本、状态代码、描述
HTTP响应的第1行包括服务器使用的HTTP的版本、状态代码,以及对状态代码的描述,这3项内容之间以空格分割。在本例中,使用HTTP1.1,状态代码为200,该状态代码表示服务器已经成功地处理了客户端发出的请求。
状态代码是一个3位整数,以1、2、3、4或5开头:
- 1xx:信息提示,表示临时的响应。
- 2xx:响应成功,表明服务器成功的接收了客户端请求。
- 3xx:重定向。
- 4xx:客户端错误,表明客户端可能有问题。
- 5xx:服务器错误,表明服务器由于遇到某种错误而不能响应客户请求。
以下是一些常见的状态代码:
- 200:响应成功。
- 400:错误的请求。客户发送的HTTP请求不正确。
- 404:文件不存在。在服务器上没有客户要求访问的文档。
- 405:服务器不支持客户的请求方式。
- 500:服务器内部错误。
2.响应头(Response Header)
响应头也和请求头一样包含许多有用的信息,例如服务器类型、正文类型和正文长度等。
3.响应正文(Response Content)
响应正文就是服务器返回的具体的文档,最常见的是HTML网页。
HTTP请求头与请求正文之间必须用空行分割,同样,HTTP响应头与响应正文之间也必须用空行分隔。
2、创建非阻塞的HTTP服务器
HTTP服务器的主要任务就是接收HTTP请求,然后发送HTTP响应。下图是我们要介绍的非阻塞的HTTP服务器范例的模型。
在这个对象模型中,HttpServer类是服务器主程序,由它启动服务器。AcceptHandler负责接收客户连接,RequestHandler负责接收客户的HTTP请求,对其解析,然后生成相应的HTTP响应,再把它发送给客户。Request类表示HTTP请求,Response类表示HTTP响应,Content类表示HTTP响应的正文。
2.1、服务器主程序:HttpServer类
HttpServer类是服务器的主程序,仅启用了单个主线程,采用非阻塞模式来接收客户连接,以及收发数据。下面是HttpServer类的源程序。
在HttpServer类的service()方法中,当ServerSocketChannel向Selector注册接收连接就绪事件时,设置了一个AcceptHandler附件:
AcceptHandler类的handle()方法负责处理接收连接就绪事件。当某种事件发生时,HttpServer类的service()方法从SelectionKey中获得Handler附件,然后调用它的handle()方法:
2.2、具有自动增长的缓冲区的ChannelIO类
自定义的ChannelIO类对SocketChannel进行了包装,增加了自动增长缓冲区容量的功能。当调用socketChannel.read(ByteBuffer buffer)方法时,如果buffer已满(即position=limit),那么即使通道中还有未接收的数据,read方法也不会读取任何数据,而是直接返回0,表示读到了零字节。
为了能读取通道中的所有数据,必须保证缓冲区的容量足够大。在ChannelIO类中,有一个requestBuffer变量,它用来存放客户的HTTP请求数据,当requestBuffer剩余容量已经不足5%,并且还有HTTP请求数据未接收时,ChannelIO会自动扩充requestBuffer的容量,该功能由resizeRequestBuffer()方法完成。
下面是ChannelIO类的源程序,它的read()方法和write()方法利用SocketChannel来接收和发送数据,并且它还提供了实用方法transferTo(),该方法能把文件中的数据发送到SocketChannel中。
2.3、负责处理各种事件的Handler接口
Handler接口负责处理各种事件,它的定义如下:
Handler接口有AcceptHandler和RequestHandler两个实现类。AcceptHandler负责处理接收连接就绪事件,RequestHandler负责处理读就绪和写就绪事件。更确切地说,RequestHandler负责接收客户的HTTP请求,以及发送HTTP响应。
2.4、负责处理接收连接就绪事件的AcceptHandler类
AcceptHandler负责处理接收连接就绪事件。它获得与客户连接的SocketChannel,然后向Selector注册读就绪事件,并且创建了一个RequestHandler,把它作为SelectionKey的附件。当读就绪事件发生时,将由这个RequestHandler来处理该事件。
在以上AcceptHandler的handle()方法中,还创建了一个ChannelIO,RequestHandler与它关联。RequestHandler会利用ChannelIO来接收和发送数据。
2.5、负责接收HTTP请求和发送HTTP响应的RequestHandler类
RequestHandler先通过ChannelIO来接收HTTP请求,当接收到了HTTP请求的所有数据后,就对HTTP请求数据进行解析,创建相应的Request对象,然后依据客户的请求内容,创建相应的Response对象,最后发送Response对象中包含的HTTP响应数据。为了简化程序,RequestHandler仅仅支持GET和HEAD这两种请求方式。
2.6、代表HTTP请求的Request类
RequestHandler通过ChannelIO读取HTTP请求数据时,这些数据被放在requestByteBuffer中。当HTTP请求的所有数据接收完毕,就要对requestByteBuffer中的数据进行解析,然后创建相应的Request对象。Request对象就表示特定的HTTP请求。
本范例仅支持GET和HEAD请求方式,在这两种方式下,HTTP请求没有正文部分,并且以“\r\n\r\n”结尾。Request类有3个成员变量:action、uri和version,它们分别表示HTTP请求中的请求方式、URI和HTTP的版本。下面是Request类的源程序。
2.7、代表HTTP响应的Response类
Response类表示HTTP响应。它有3个成员变量:code、headerBuffer和content,它们分别表示HTTP响应中的状态代码、响应头和正文。Response类的prepare()方法负责准备HTTP响应的响应头和正文内容,send()方法负责发送HTTP响应的所有数据。下面是Response类的源程序:
2.8、代表响应正文的Content接口及其实现类
Response类有一个成员变量content,表示响应正文,它被定义为Content类型。
Content接口表示响应正文,它的定义如下:
Content接口继承了Sendable接口,Sendable接口表示服务器端可发送给客户的内容,它的定义如下:
Content接口有StringContent和FileContent两个实现类。StringContent表示字符串形式的正文,FileContent表示文件形式的正文。例如在RequestHandler类的build()方法中,如果HTTP请求方式不是GET和HEAD,就创建一个包含StringContent的Response对象,否则就创建一个包含FileContent的Response对象。
下面主要介绍FileContent类的实现。FileContent类有一个成员变量fileChannel,它表示读文件的通道。FileContent类的send()方法把fileChannel中的数据发送到ChannelIO的SocketChannel中,如果文件中的所有数据发送完毕,send()方法就返回false。下面是FileContent类的源程序。
2.9、运行HTTP服务器
运行命令“java HttpServer”,就启动了HTTP服务器。在本范例的root目录下存放了各种供浏览器访问的文档,比如login.htm、hello.htm和data.rar文件等。打开IE浏览器,输入URL:http://localhost/login.htm或者http://localhost/data.rar,就能接收到服务器发送过来的相应文档。如果浏览器按照POST方式访问hello.htm,服务器就会返回HTTP405错误,因为本服务器不支持POST方式。
3、总结
HTTP是目前使用非常广泛的应用层协议,它规定了在网络上传输文档(主要是HTML格式的网页)的规则。HTTP的客户程序主要是浏览器。浏览器访问一个远程HTTP服务器上的网页的步骤如下:
- (1)建立与远程服务器的连接。
- (2)发送HTTP请求。
- (3)接收HTTP响应,断开与远程服务器的连接。
- (4)展示HTTP响应中的网页内容。
HTTP服务器必须接收HTTP请求,对它进行解析,然后返回相应的HTTP响应结果。本章创建了一个非阻塞的HTTP服务器,它首先读取HTTP请求,把它们存放在字节缓冲区内,当缓冲区的容量不够时,会扩充它的容量,以保证容纳HTTP请求的所有数据。接着,程序把字节缓冲区内的字节转换为字符串,对其进行解析,获得HTTP请求中的请求方式、URI和协议版本等信息,然后创建相应的HTTP响应,把它发送给客户程序。