HTTP 概述
HTTP 客户程序必须先发出一个 HTTP 请求,然后才能接收到来自 HTTP 服器的响应,浏览器就是最常见的 HTTP 客户程序。HTTP 客户程序和 HTTP 服务器分别由不同的软件开发商提供,它们都可以用任意的编程语言编写。HTTP 严格规定了 HTTP 请求和 HTTP 响应的数据格式,只要 HTTP 服务器与客户程序都遵守 HTTP,就能彼此看得懂对方发送的消息
1. HTTP 请求格式
下面是一个 HTTP 请求的例子- POST /hello.jsp HTTP/1.1
- Accept:image/gif, image/jpeg, */*
- Referer: http://localhost/login.htm
- Accept-Language: en,zh-cn;q=0.5
- Content-Type: application/x-www-form-urlencoded
- Accept-Encoding: gzip, deflate
- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 10.0)
- Host: localhost
- Content-Length:43
- Connection: Keep-Alive
- Cache-Control: no-cache
- username=root&password=12346&submit=submit
复制代码 HTTP 规定,HTTP 请求由三部分构成,分别是:
- 请求方法、URI、HTTP 的版本
- HTTP 请求的第一行包括请求方式、URI 和协议版本这三项内容,以空格分开:POST /hello.jsp HTTP/1.1
- 请求头(Request Header)
- 请求头包含许多有关客户端环境和请求正文的有用信息。例如,请求头可以声明浏览器的类型、所用的语言、请求正文的类型,以及请求正文的长度等
- Accept:image/gif, image/jpeg, */*
- Referer: http://localhost/login.htm
- Accept-Language: en,zh-cn;q=0.5 //浏览器所用的语言
- Content-Type: application/x-www-form-urlencoded //正文类型
- Accept-Encoding: gzip, deflate
- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 10.0) //浏览器类型
- Host: localhost //远程主机
- Content-Length:43 //正文长度
- Connection: Keep-Alive
- Cache-Control: no-cache
复制代码
- 请求正文(Request Content)
- HTTP 规定,请求头和请求正文之间必须以空行分割(即只有 CRLF 符号的行),这个空行非常重要,它表示请求头已经结束,接下来是请求正文,请求正文中可以包含客户以 POST 方式提交的表单数据
- username=root&password=12346&submit=submit
复制代码
2. HTTP 响应格式
下面是一个 HTTP 响应的例子- HTTP/1.1 200 0K
- Server: nio/1.1
- Content-type: text/html; charset=GBK
- Content-length:97
-
- <html>
- <head>
- <title>helloapp</title>
- </head>
- <body >
- <h1>hello</h1>
- </body>
- </htm1>
复制代码 HTTP 响应也由三部分构成,分别是:
- HTTP 的版本、状态代码、描述
- HTTP 响应的第一行包括服务器使用的 HTTP 的版本、状态代码,以及对状态代码的描述,这三项内容之间以空格分割
- 响应头 (Response Header)
- 响应头也和请求头一样包含许多有用的信息,例如服务器类型、正文类型和正文长度等
- Server: nio/1.1 //服务器类型
- Content-type: text/html; charset=GBK //正文类型
- Content-length:97 //正文长度
复制代码
- 响应正文(Response Content)
- 响应正文就是服务器返回的具体的文档,最常见的是 HTML 网页。HTTP 响应头与响应正文之间也必须用空行分隔
- <html>
- <head>
- <title>helloapp</title>
- </head>
- <body >
- <h1>hello</h1>
- </body>
- </htm1>
复制代码
创建阻塞的 HTTP 服务器
下例(SimpleHttpServer)创建了一个非常简单的 HTTP 服务器,它接收客户程序的 HTTP 请求,把它打印到控制台。然后对 HTTP 请求做简单的解析,如果客户程序请求访问 login.htm,就返回该网页,否则一律返回 hello.htm 网页。login.htm 和 hello.htm 文件位于 root 目录下
SimpleHttpServer 监听 80 端口,按照阻塞模式工作,采用线程池来处理每个客户请求- public class SimpleHttpServer {
-
- private int port = 80;
- private ServerSocketChannel serverSocketChannel = null;
- private ExecutorService executorService;
- private static final int POOL MULTIPLE = 4;
- private Charset charset = Charset.forName("GBK");
-
- public SimpleHttpServer() throws IOException {
- executorService= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * POOL MULTIPLE);
- serverSocketChannel = ServerSocketChannel.open();
- serverSocketChannel.socket().setReuseAddress(true);
- serverSocketChannel.socket().bind(new InetSocketAddress(port));
- System.out.println("服务器启动");
- }
-
- public void service() {
- while (true) {
- SocketChannel socketChannel = null;
- try {
- socketChannel = serverSocketChannel.accept();
- executorService.execute(new Handler(socketChannel));
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- public static void main(String args[])throws IOException {
- new SimpleHttpServer().service();
- }
-
- public String decode(ByteBuffer buffer) {......} //解码
-
- public ByteBuffer encode(String str) {......} //编码
-
- //Handler是内部类,负责处理HTTP请求
- class Handler implements Runnable {
-
- private SocketChannel socketChannel;
-
- public Handler(SocketChannel socketChannel) {
- this.socketChannel = socketChannel;
- }
-
- public void run() {
- handle(socketChannel);
- }
-
- public void handle(SocketChannel socketChannel) {
- try {
- Socket socket = socketChannel.socket();
- ByteBuffer buffer = ByteBuffer.allocate(1024);
-
- //接收HTTP请求,假定其长度不超过1024字节
- socketChannel.read(buffer);
- buffer.flip();
- String request = decode(buffer);
- //打印HTTP请求
- System.out.print(request);
-
- //生成HTTP响应结果
- StringBuffer sb = new StringBuffer("HTTP/1.1 200 0K\r\n");
- sb.append("Content-Type:text/html\r\n\r\n");
- //发送HTTP响应的第1行和响应头
- socketChannel.write(encode(sb.toString()));
-
- FileInputStream in;
- //获得HTTP请求的第1行
- String firstLineOfRequest = request.substring(0, request.indexOf("\r\n"));
- if(firstLineOfRequest.indexOf("login.htm") != -1) {
- in = new FileInputStream("login.htm");
- } else {
- in = new FileInputStream("hello.htm");
- }
-
- FileChannel fileChannel = in.getChannel();
- //发送响应正文
- fileChannel.transferTo(0, fileChannel.size(), socketChannel);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if(socketChannel != null) {
- //关闭连接
- socketChannel.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
复制代码 创建非阻塞的 HTTP 服务器
下面是本节所介绍的非阻塞的 HTTP 服务器范例的模型
- HttpServer:服务器主程序,由它启动服务器
- AcceptHandler:负责接收客户连接
- RequestHandler:负责接收客户的 HTTP 请求,对其解析,然后生成相应的 HTTP 响应,再把它发送给客户
- Request:表示 HTTP 请求
- Response:表示 HTTP 响应
- Content:表示 HTTP 响应的正文
1. 服务器主程序 HttpServer
HttpServer 仅启用了单个主线程,采用非阻塞模式来接收客户连接,以及收发数据- public class HttpServer {
-
- private Selector selector = null;
- private ServerSocketChannel serverSocketChannel = null;
- private int port = 80;
- private Charset charset = Charset.forName("GBK");
-
- public HttpServer() throws IOException {
- //创建Selector和ServerSocketChannel
- //把ServerSocketchannel设置为非阻塞模式,绑定到80端口
- ......
- }
-
- public void service() throws IOException {
- //注册接收连接就绪事件
- serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, new AcceptHandler());
- while(true) {
- int n = selector.select();
- if(n==0) continue;
- Set readyKeys = selector.selectedKeys();
- Iterator it = readyKeys.iterator();
- while(it.hasNext()) {
- SelectionKey key = null;
- try {
- key = (SelectionKey) it.next();
- it.remove();
- final Handler handler = (Handler) key.attachment();
- handler.handle(key); //由 Handler 处理相关事件
- } catch(IOException e) {
- e.printStackTrace();
- try {
- if(key != null) {
- key.cancel();
- key.channel().close();
- }
- } catch(Exception ex) {
- e.printStackTrace();
- }
- }
- }
- }
- }
-
- public static void main(String args[])throws Exception {
- final HttpServer server = new HttpServer();
- server.service();
- }
- }
复制代码 2. 具有自动增长的缓冲区的 ChannelIO 类
自定义的 ChannelIO 类对 SocketChannel 进行了包装,增加了自动增长缓冲区容量的功能。当调用 socketChannel.read(ByteBuffer bufer) 方法时,如果 buffer 已满,即使通道中还有未接收的数据,read 方法也不会读取任何数据,而是直接返回 0,表示读到了零字节
为了能读取通道中的所有数据,必须保证缓冲区的容量足够大。在 ChannelIO 类中有一个 requestBuffer 变量,它用来存放客户的 HTTP 请求数据,当 requestBuffer 剩余容量已经不足 5%,并且还有 HTTP 请求数据未接收时,ChannellO 会自动扩充 requestBuffer 的容量,该功能由 resizeRequestBuffer() 方法完成- public class ChannelIO {
-
- protected SocketChannel socketChannel;
- protected ByteBuffer requestBuffer; //存放请求数据
- private static int requestBufferSize = 4096;
-
- public ChannelIO(SocketChannel socketChannel, boolean blocking) throws IOException {
- this.socketChannel = socketChannel;
- socketChannel.configureBlocking(blocking); //设置模式
- requestBuffer = ByteBuffer.allocate(requestBufferSize);
- }
-
- public SocketChannel
- () {
- return socketChannel;
- }
-
- /**
- * 如果原缓冲区的剩余容量不够,就创建一个新的缓冲区,容量为原来的两倍
- * 并把原来缓冲区的数据拷贝到新缓冲区
- */
- protected void resizeRequestBuffer(int remaining) {
- if (requestBuffer.remaining() < remaining) {
- ByteBuffer bb = ByteBuffer.allocate(requestBuffer.capacity() * 2);
- requestBuffer.flip();
- bb.put(requestBuffer); //把原来缓冲区中的数据拷贝到新的缓冲区
- requestBuffer = bb;
- }
- }
-
- /**
- * 接收数据,把它们存放到requestBuffer
- * 如果requestBuffer的剩余容量不足5%
- * 就通过resizeRequestBuffer()方法扩充容量
- */
- public int read() throws IOException {
- resizeRequestBuffer(requestBufferSize/20);
- return socketChannel.read(requestBuffer);
- }
-
- /** 返回requestBuffer,它存放了请求数据 */
- public ByteBuffer getReadBuf() {
- return requestBuffer;
- }
-
- /** 发送参数指定的 ByteBuffer 的数据 */
- public int write(ByteBuffer src) throws IOException {
- return socketChannel.write(src);
- }
-
- /** 把FileChannel的数据写到SocketChannel */
- public long transferTo(FileChannel fc, long pos, long len) throws IOException {
- return fc.transferTo(pos, len, socketChannel);
- }
-
- /** 关闭SocketChannel */
- public void close() throws IOException {
- socketChannel.close();
- }
- }
复制代码 3. 负责处理各种事件的 Handler 接口
Handler 接口负责处理各种事件,它的定义如下:- public interface Handler {
- public void handle(SelectionKey key) throws IOException;
- }
复制代码 Handler 接口有 AcceptHandler 和 RequestHandler 两个实现类。AcceptHandler 负责处理接收连接就绪事件,RequestHandler 负责处理读就绪和写就绪事件。更确切地说,RequestHandler 负责接收客户的 HTTP 请求,以及发送 HTTP 响应
4. 负责处理接收连接就绪事件的 AcceptHandler类
AcceptHandler 负责处理接收连接就绪事件,获得与客户连接的 SocketChannel,然后向 Selector 注册读就绪事件,并且创建了一个 RequestHandler,把它作为 SelectionKey 的附件。当读就绪事件发生时,将由这个 RequestHandler 来处理该事件- public class AcceptHandler implements Handler {
-
- public void handle(SelectionKey key) throws IOException {
- ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
- //在非阻塞模式下,serverSocketChannel.accept()有可能返回null
- SocketChannel socketChannel = serverSocketChannel.accept();
- if (socketChannel == null) return;
- //ChannelIO设置为采用非阻塞模式
- ChannelIO cio = new ChannelIO(socketChannel, false);
- RequestHandler rh = new RequestHandler(cio);
- //注册读就绪事件,把RequestHandler作为附件
- socketChannel.register(key.selector(), SelectionKey.OP_READ, rh);
- }
- }
复制代码 5. 负责接收 HTTP 请求和发送 HTTP 响应的 RequestHandler 类
RequestHandler 先通过 ChannelIO 来接收 HTTP 请求,当接收到 HTTP 请求的所有数据后,就对 HTTP 请求数据进行解析,创建相应的 Request 对象,然后依据客户的请求内容,创建相应的 Response 对象,最后发送 Response 对象中包含的 HTTP 响应数据。为了简化程序,RequestHandler 仅仅支持 GET 和 HEAD 两种请求方式- public class RequestHandler implements Handler {
-
- private ChannelIO channelIO;
- //存放HTTP请求的缓冲区
- private ByteBuffer requestByteBuffer = null;
- //表示是否已经接收到HTTP请求的所有数据
- private boolean requestReceived = false;
- //表示HTTP请求
- private Request request = null;
- //表示HTTP响应
- private Response response = null;
-
- RequestHandler(ChannelIO channelIO) {
- this.channelIO = channelIO;
- }
-
- /** 接收HTTP请求,发送HTTP响应 */
- public void handle(SelectionKey sk) throws IOException {
- try {
- //如果还没有接收HTTP请求的所有数据,就接收HTTP请求
- if (request == null) {
- if (!receive(sk)) return;
- requestByteBuffer.flip();
- //如果成功解析了HTTP请求,就创建一个Response对象
- if (parse()) build();
- try {
- //准备HTTP响应的内容
- response.prepare();
- } catch (IOException x) {
- response.release();
- response = new Response(Response.Code.NOT_FOUND, new StringContent(x.getMessage()));
- response.prepare();
- }
-
- if (send()) {
- //如果HTTP响应没有发送完毕,则需要注册写就绪事件,以便在写就绪事件发生时继续发送数据
- sk.interestOps(SelectionKey.OP_WRITE);
- } else {
- //如HTTP响应发送完毕,就断开底层连接,并且释放Response占用资源
- channelIO.close();
- response.release();
- }
- } else {
- //如果已经接收到HTTP请求的所有数据
- //如果HTTP响应发送完毕
- if (!send()) {
- channelIO.close();
- response.release();
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- channelIO.close();
- if (response != null) {
- response.release();
- }
- }
- }
-
- /**
- * 接收HTTP请求,如果已经接收到了HTTP请求的所有数据,就返回true,否则返回false
- */
- private boolean receive(SelectionKey sk) throws IOException {
- ByteBuffer tmp = null;
- //如果已经接收到HTTP请求的所有数据,就返回true
- if (requestReceived) return true;
- //如果已经读到通道的末尾,或者已经读到HTTP请求数据的末尾标志,就返回true
- if ((channelIO.read() < 0) || Request.isComplete(channelIO.getReadBuf())) {
- requestByteBuffer = channelIO.getReadBuf();
- return (requestReceived = true);
- }
- return false;
- }
-
- /**
- * 通过Request类的parse()方法,解析requestByteBuffer的HTTP请求数据
- * 构造相应的Request对象
- */
- private boolean parse() throws IOException {
- try {
- request = Request.parse(requestByteBuffer);
- return true;
- } catch (MalformedRequestException x) {
- //如果HTTP请求的格式不正确,就发送错误信息
- response = new Response(Response.Code.BAD_REQUEST, new StringContent(x))
- }
- return false;
- }
-
- /** 创建HTTP响应 */
- 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);
- }
- }
-
- /** 发送HTTP响应,如果全部发送完毕,就返回false,否则返回true */
- private boolean send() throws IOException {
- return response.send(channelIO);
- }
- }
复制代码 6. 代表 HTTP 请求的 Request 类
RequestHandler 通过 ChannelIO 读取 HTTP 请求数据时,这些数据被放在 requestByteBuffer 中。当 HTTP 请求的所有数据接收完毕,就要对 requestByteBufer 的数据进行解析,然后创建相应的 Request 对象。Request 对象就表示特定的 HTTP 请求- public class Request {
-
- //枚举类,表示HTTP请求方式
- static enum Action {
- GET,PUT,POST,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; //HTTP版本
- private URI 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;
- }
-
- /**
- * 删除请求正文
- */
- 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.substrinq(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[l] = "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
- URI u;
- try {
- 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、headerBufer 和 content,它们分别表示 HTTP 响应中的状态代码、响应头和正文
[code]public class Response implements Sendable { //枚举类,表示状态代码 static enum Code { OK(200, "OK"), BAD_REQUEST(400, "Bad Request"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"); private int number; private String reason; private Code(int i, String r) { number = i; reason =r; } public String toString() { return number + " " + reason; } } 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); } /** 创建响应头的内容,把它存放到ByteBuffer */ private ByteBuffer headers() { CharBuffer cb = CharBuffer.allocate(1024); while(true) { 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(CRIE); cb.put("Content-length: ").put(Long.toString(content.length())).put(CRLF); cb.put(CRLF); break; } catch (BufferOverflowException x) { assert(cb.capacity() < (1 |