6.代表HTTP请求的Request类
RequestHandler通过ChannelIO读取HTTP请求数据时,这些数据放在requestByteBuffer中。当HTTP请求的所有数据接收完毕,就要对requestByteBuffer中的数据进行解析,然后创建相应的Request对象。Request对象就表示特定的HTTP请求。
本范例仅支持GET和HEAD请求方式,在这两种方式下,HTTP请求没有正文部分,并且以“\r\n\r\n”结尾。Request类有三个成员变量:action、uri和version,它们分别表示HTTP请求中的请求方式、URI和HTTP协议的版本。例程6是Request类的源程序:
//例程6 Request.java
import java.net.*;
import java.nio.*;
import java.nio.charset.*;
import java.util.regex.*;
/* 代表客户的HTTP请求 */
public class Request {
static class Action { //枚举类,表示HTTP请求方式
private String name;
private Action(String name) { this.name = name; }
public String toString() { return name; }
static Action GET = new Action("GET");
static Action PUT = new Action("PUT");
static Action POST = new Action("POST");
static Action HEAD = new Action("HEAD");
public static Action parse(String s) {
if (s.equals("GET"))
return GET;
if (s.equals("PUT"))
return PUT;
if (s.equals("POST"))
return POST;
if (s.equals("HEAD"))
return HEAD;
throw new IllegalArgumentException(s);
}
}
private Action action;
private String version;
private URI uri;
public Action action() { return action; }
public String version() { return version; }
public URI uri() { return uri; }
private Request(Action a, String v, URI u) {
action = a;
version = v;
uri = u;
}
public String toString() {
return (action + " " + version + " " + uri);
}
private static Charset requestCharset = Charset.forName("GBK");
/* 判断ByteBuffer是否包含了HTTP请求的所有数据。
* HTTP请求以“\r\n\r\n”结尾。
*/
public static boolean isComplete(ByteBuffer bb) {
ByteBuffer temp=bb.asReadOnlyBuffer();
temp.flip();
String data=requestCharset.decode(temp).toString();
if(data.indexOf("\r\n\r\n")!=-1){
return true;
}
return false;
}
/*
* 删除请求正文,本范例仅支持GET和HEAD请求方式,不处理HTTP请求中的正文部分
*/
private static ByteBuffer deleteContent(ByteBuffer bb) {
ByteBuffer temp=bb.asReadOnlyBuffer();
String data=requestCharset.decode(temp).toString();
if(data.indexOf("\r\n\r\n")!=-1){
data=data.substring(0,data.indexOf("\r\n\r\n")+4);
return requestCharset.encode(data);
}
return bb;
}
/*
* 设定用于解析HTTP请求的字符串匹配模式。对于以下形式的HTTP请求:
*
* GET /dir/file HTTP/1.1
* Host: hostname
*
* 将被解析成:
*
* group[1] = "GET"
* group[2] = "/dir/file"
* group[3] = "1.1"
* group[4] = "hostname"
*/
private static Pattern requestPattern
= Pattern.compile("\\A([A-Z]+) +([^ ]+) +HTTP/([0-9\\.]+)$"
+ ".*^Host: ([^ ]+)$.*\r\n\r\n\\z",
Pattern.MULTILINE | Pattern.DOTALL);
/* 解析HTTP请求,创建相应的Request对象 */
public static Request parse(ByteBuffer bb) throws MalformedRequestException {
bb=deleteContent(bb); //删除请求正文
CharBuffer cb = requestCharset.decode(bb); //解码
Matcher m = requestPattern.matcher(cb); //进行字符串匹配
//如果HTTP请求与指定的字符串模式不匹配,说明请求数据不正确
if (!m.matches())
throw new MalformedRequestException();
Action a;
try { //获得请求方式
a = Action.parse(m.group(1));
} catch (IllegalArgumentException x) {
throw new MalformedRequestException();
}
URI u;
try { //获得URI
u = new URI("http://"
+ m.group(4)
+ m.group(2));
} catch (URISyntaxException x) {
throw new MalformedRequestException();
}
//创建一个Request对象,并将其返回
return new Request(a, m.group(3), u);
}
}
7.代表HTTP响应的Response类
Response类表示HTTP响应。它有三个成员变量:code、headerBuffer和content,它们分别表示HTTP响应中的状态代码、响应头和正文。Response类的prepare()方法负责准备HTTP响应的响应头和正文内容,send()方法负责发送HTTP响应的所有数据。例程7是Response类的源程序:
//例程7 Response.java
//此处省略import语句
public class Response implements Sendable {
static class Code { //枚举类,表示状态代码
private int number;
private String reason;
private Code(int i, String r) { number = i; reason = r; }
public String toString() { return number + " " + reason; }
static Code OK = new Code(200, "OK");
static Code BAD_REQUEST = new Code(400, "Bad Request");
static Code NOT_FOUND = new Code(404, "Not Found");
static Code METHOD_NOT_ALLOWED = new Code(405, "Method Not Allowed");
}
private Code code; //状态代码
private Content content; //响应正文
private boolean headersOnly; //表示HTTP响应中是否仅包含响应头
private ByteBuffer headerBuffer = null; //响应头
public Response(Code rc, Content c) {
this(rc, c, null);
}
public Response(Code rc, Content c, Request.Action head) {
code = rc;
content = c;
headersOnly = (head == Request.Action.HEAD);
}
private static String CRLF = "\r\n";
private static Charset responseCharset = Charset.forName("GBK");
/* 创建响应头的内容,把它存放到一个ByteBuffer中 */
private ByteBuffer headers() {
CharBuffer cb = CharBuffer.allocate(1024);
for (;;) {
try {
cb.put("HTTP/1.1 ").put(code.toString()).put(CRLF);
cb.put("Server: nio/1.1").put(CRLF);
cb.put("Content-type: ").put(content.type()).put(CRLF);
cb.put("Content-length: ")
.put(Long.toString(content.length())).put(CRLF);
cb.put(CRLF);
break;
} catch (BufferOverflowException x) {
assert(cb.capacity() < (1 << 16));
cb = CharBuffer.allocate(cb.capacity() * 2);
continue;
}
}
cb.flip();
return responseCharset.encode(cb); //编码
}
/* 准备HTTP响应中的正文以及响应头的内容 */
public void prepare() throws IOException {
content.prepare();
headerBuffer= headers();
}
/* 发送HTTP响应,如果全部发送完毕,返回false,否则返回true */
public boolean send(ChannelIO cio) throws IOException {
if (headerBuffer == null)
throw new IllegalStateException();
//发送响应头
if (headerBuffer.hasRemaining()) {
if (cio.write(headerBuffer) <= 0)
return true;
}
//发送响应正文
if (!headersOnly) {
if (content.send(cio))
return true;
}
return false;
}
/* 释放响应正文占用的资源 */
public void release() throws IOException {
content.release();
}
}
8.代表响应正文的Content接口及其实现类
Response类有一个成员变量content,表示响应正文,它被定义为Content类型:
private Content content; //响应正文
Content接口表示响应正文,它的定义如下:
public interface Content extends Sendable {
//正文的类型
String type();
//返回正文的长度。
//在正文还没有准备之前,即还没有调用prepare()方法之前,length()方法返回-1。
long length();
}
Content接口继承了Sendable接口,Sendable接口表示服务器端可发送给客户的内容,它的定义如下:
public interface Sendable {
// 准备发送的内容
public void prepare() throws IOException;
// 利用通道发送部分内容,如果所有内容发送完毕,就返回false
// 如果还有内容未发送,就返回true
// 如果内容还没有准备好,就抛出IllegalStateException
public boolean send(ChannelIO cio) throws IOException;
//当服务器发送内容完毕,就调用此方法,释放内容占用的资源
public void release() throws IOException;
}
Content接口有两个实现类:StringContent和FileContent。StringContent表示字符串形式的正文,FileContent表示文件形式的正文。例如在RequestHandler类的build()方法中,如果HTTP请求发式不是GET和HEAD,就创建一个包含StringContent的Response对象,否则就创建一个包含FileContent的Response对象。
private void build() throws IOException {
Request.Action action = request.action();
//仅仅支持GET和HEAD请求方式
if ((action != Request.Action.GET) &&
(action != Request.Action.HEAD)){
response = new Response(Response.Code.METHOD_NOT_ALLOWED,
new StringContent("Method Not Allowed"));
}else{
response = new Response(Response.Code.OK,
new FileContent(request.uri()), action);
}
}
下面主要介绍FileContent类的实现。FileContent类有一个成员变量fileChannel,它表示读文件的通道。FileContent类的send()方法把fileChannel中的数据发送到ChannelIO的SocketChannel中,如果文件中的所有数据发送完毕,send()方法就返回false。例程8是FileContent类的源程序:
//例程8 FileContent.java
//此处省略import语句
public class FileContent implements Content {
//假定文件的根目录为"root",该目录应该位于classpath下
private static File ROOT = new File("root");
private File file;
public FileContent(URI uri) {
file = new File(ROOT,
uri.getPath()
.replace('/',File.separatorChar));
}
private String type = null;
/* 确定文件类型 */
public String type() {
if (type != null) return type;
String nm = file.getName();
if (nm.endsWith(".html")|| nm.endsWith(".htm"))
type = "text/html; charset=iso-8859-1"; //HTML网页
else if ((nm.indexOf('.') < 0) || nm.endsWith(".txt"))
type = "text/plain; charset=iso-8859-1"; //文本文件
else
type = "application/octet-stream"; //应用程序
return type;
}
private FileChannel fileChannel = null;
private long length = -1; //文件长度
private long position = -1; //文件的当前位置
public long length() {
return length;
}
/* 创建FileChannel对象*/
public void prepare() throws IOException {
if (fileChannel == null)
fileChannel = new RandomAccessFile(file, "r").getChannel();
length = fileChannel.size();
position = 0;
}
/* 发送正文,如果发送完毕,就返回false,否则返回true */
public boolean send(ChannelIO channelIO) throws IOException {
if (fileChannel == null)
throw new IllegalStateException();
if (position < 0)
throw new IllegalStateException();
if (position >= length) {
return false; //如果发送完毕,就返回false
}
position += channelIO.transferTo(fileChannel, position, length - position);
return (position < length);
}
public void release() throws IOException {
if (fileChannel != null){
fileChannel.close(); //关闭fileChannel
fileChannel = null;
}
}
}
9.运行HTTP服务器
运行命令“java HttpServer”,就启动了HTTP服务器。在HttpServer类的classpath下,有一个root目录,在该目录下存放了各种供浏览器访问的文档,比如login.htm、hello.htm和data.rar文件等。打开IE浏览器,输入URL:http://localhost/login.htm或者http://localhost/data.rar,就能接收到服务器发送过来的相应文档。如果浏览器按照POST方式访问hello.htm,服务器会返回HTTP405错误,因为本服务器不支持POST方式。
HTTP协议是目前使用非常广泛的应用层协议,它规定了在网络上传输文档(主要是HTML格式的网页)的规则。HTTP协议的客户程序主要是浏览器。浏览器访问一个远程HTTP服务器上的网页的步骤如下:
(1)建立与远程服务器的连结。
(2)发送HTTP请求。
(3)接收HTTP响应,断开与远程服务器的连结。
(4)展示HTTP响应中的网页内容。
HTTP服务器必须接收HTTP请求,对它进行解析,然后返回相应的HTTP响应结果。本文创建了一个非阻塞的HTTP服务器,它首先读取HTTP请求,把它们存放在字节缓冲区内,当缓冲区的容量不够时,要扩充它的容量,以保证容纳HTTP请求的所有数据。接着,程序把字节缓冲区内的字节转换为字符串,对其进行解析,获得HTTP请求中的请求方式、URI和协议版本等信息,然后创建相应的HTTP响应,把它发送给客户程序。
|