你好,欢迎来到电脑编程技巧与维护杂志社! 杂志社简介广告服务读者反馈编程社区  
合订本订阅
 
 
您的位置:杂志经典 / 跟高手学编程
1.2 用Java实现非阻塞的HTTP服务器(下)
 

6.代表HTTP请求的Request

RequestHandler通过ChannelIO读取HTTP请求数据时,这些数据放在requestByteBuffer中。当HTTP请求的所有数据接收完毕,就要对requestByteBuffer中的数据进行解析,然后创建相应的Request对象。Request对象就表示特定的HTTP请求。

本范例仅支持GETHEAD请求方式,在这两种方式下,HTTP请求没有正文部分,并且以“\r\n\r\n”结尾。Request类有三个成员变量:actionuriversion,它们分别表示HTTP请求中的请求方式、URIHTTP协议的版本。例程6Request类的源程序:

//例程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;

  }

 

  /*

   * 删除请求正文本范例仅支持GETHEAD请求方式不处理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响应。它有三个成员变量:codeheaderBuffercontent,它们分别表示HTTP响应中的状态代码、响应头和正文。Response类的prepare()方法负责准备HTTP响应的响应头和正文内容,send()方法负责发送HTTP响应的所有数据。例程7Response类的源程序:

//例程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接口有两个实现类:StringContentFileContentStringContent表示字符串形式的正文,FileContent表示文件形式的正文。例如在RequestHandler类的build()方法中,如果HTTP请求发式不是GETHEAD,就创建一个包含StringContentResponse对象,否则就创建一个包含FileContentResponse对象。

  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中的数据发送到ChannelIOSocketChannel中,如果文件中的所有数据发送完毕,send()方法就返回false。例程8FileContent类的源程序:

//例程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.htmhello.htmdata.rar文件等。打开IE浏览器,输入URLhttp://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响应,把它发送给客户程序。

  推荐精品文章

·2024年12月目录 
·2024年11月目录 
·2024年10月目录 
·2024年9月目录 
·2024年8月目录 
·2024年7月目录 
·2024年6月目录 
·2024年5月目录 
·2024年4月目录 
·2024年3月目录 
·2024年2月目录 
·2024年1月目录
·2023年12月目录
·2023年11月目录

  联系方式
TEL:010-82561037
Fax: 010-82561614
QQ: 100164630
Mail:gaojian@comprg.com.cn

  友情链接
 
Copyright 2001-2010, www.comprg.com.cn, All Rights Reserved
京ICP备14022230号-1,电话/传真:010-82561037 82561614 ,Mail:gaojian@comprg.com.cn
地址:北京市海淀区远大路20号宝蓝大厦E座704,邮编:100089