Java 网络编程 —— 创建非阻塞的 HTTP 服务器
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
- HTTP 请求的第一行包括请求方式、URI 和协议版本这三项内容,以空格分开:
-
请求头(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 响应中的状态代码、响应头和正文
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();
}
}
8. 代表响应正文的 Content 接口及其实现类
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;
}
}
}