Java创建非阻塞的HTTP服务器的实现
作者:布道师小羊
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响应,把它发送给客户程序。
到此这篇关于Java创建非阻塞的HTTP服务器的实现的文章就介绍到这了,更多相关Java 非阻塞HTTP服务器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!