HTTP 客户程序必须先发出一个 HTTP 请求,然后才能接收到来自 HTTP 服器的响应,浏览器就是最常见的 HTTP 客户程序。HTTP 客户程序和 HTTP 服务器分别由不同的软件开发商提供,它们都可以用任意的编程语言编写。HTTP 严格规定了 HTTP 请求和 HTTP 响应的数据格式,只要 HTTP 服务器与客户程序都遵守 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 的版本
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
下面是一个 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 的版本、状态代码、描述
响应头 (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>
下例(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 服务器范例的模型
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(); } }
自定义的 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(); } }
Handler 接口负责处理各种事件,它的定义如下:
public interface Handler { public void handle(SelectionKey key) throws IOException; }
Handler 接口有 AcceptHandler 和 RequestHandler 两个实现类。AcceptHandler 负责处理接收连接就绪事件,RequestHandler 负责处理读就绪和写就绪事件。更确切地说,RequestHandler 负责接收客户的 HTTP 请求,以及发送 HTTP 响应
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); } }
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); } }
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); } }
Response 类表示 HTTP 响应,它有三个成员变量:code、headerBufer 和 content,它们分别表示 HTTP 响应中的状态代码、响应头和正文
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 << 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(); } }
Response 类有一个成员变量 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 //如果内容还没有准备好,就抛出 IlleqalstateException public boolean send(ChannelIO cio) throws IOException; //当服务器发送内容完毕,就调用此方法,释放内容占用的资源 public void release() throws IOException; }
Content 接口有 StringContent 和 FileContent 两个实现类,StringContent 表示字符串形式的正文,FileContent 表示文件形式的正文
FileContent 类有一个成员变量 fleChannel,它表示读文件的通道。FileContent 类的 send() 方法把 fileChannel 中的数据发送到 ChannelIO 的 SocketChannel 中,如果文件中的所有数据发送完毕,send() 方法就返回 false
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; } } }