HTTP请求和响应报文与简单实现Java Http服务器
报文结构
HTTP 报文包含以下三个部分:
- 起始行
报文的第一行是起始行,在请求报文中用来说明要做什么,而在响应报文中用来说明出现了什么情况。 - 首部
起始行后面有零个或多个首部字段。每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(:)来分隔。
首部以一个空行结束。添加一个首部字段和添加新行一样简单。 - 主体
空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了要发送给 Web 服务器的数据;响应主体中装载了要返回给客户端的数据。
起始行和首部都是文本形式且都是结构化的,而主体不同,主体中可以包含任意的二进制数据(比如图片,视频,音轨,软件程序)。当然,主体中也可以包含文本。
HTTP 请求报文
- 回车换行指代
\r\n
HTTP 响应报文
Http协议处理流程
流程说明:
- 客户端(浏览器)发起请求,并根据协议封装请求头与请求参数。
- 服务端接受连接
- 读取请求数据包
- 将数据包解码成 HttpRequest 对象
- 业务处理(异步),并将结果封装成 HttpResponse 对象
- 基于协议编码 HttpResponse
- 将编码后的数据写入管道
上一章博客 Java1.4从BIO模型发展到NIO模型
就已经介绍了如何实现一个简单的 NIO 事件驱动的服务器,处理了包括接受连接、读取数据、回写数据的流程。本文就主要针对解码和编码进行细致的分析。
关键步骤
HttpResponse 交给 IO 线程负责回写给客户端
API:java.nio.channels.SelectionKey 1.4
- Object attach(Object ob)
将给定的对象附加到此键。稍后可以通过{@link #attachment()}检索附件。 - Object attachment()
检索当前附件。
通过这个 API 我们就可以将写操作移出业务线程
service.submit(new Runnable() {
@Override
public void run() {
HttpResponse response = new HttpResponse();
if ("get".equalsIgnoreCase(request.method)) {
servlet.doGet(request, response);
} else if ("post".equalsIgnoreCase(request.method)) {
servlet.doPost(request, response);
}
// 获得响应
key.interestOps(SelectionKey.OP_WRITE);
key.attach(response);
// socketChannel.register(key.selector(), SelectionKey.OP_WRITE, response);
// 坑:异步唤醒
key.selector().wakeup();
}
});
值得注意的是,因为我选择使用 select() 来遍历键,因此需要在业务线程准备好 HttpResponse 后,立即唤醒 IO 线程。
使用 ByteArrayOutputStream 来装缓冲区数据
private void read(SocketChannel socketChannel, ByteArrayOutputStream out) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (socketChannel.read(buffer) > 0) {
buffer.flip(); // 切换到读模式
out.write(buffer.array());
buffer.clear(); // 清理缓冲区
}
}
一次可能不能读取所有报文数据,所以用 ByteArrayOutputStream 来连接数据。
读取到空报文,抛出NullPointerException
在处理读取数据时读取到空数据时,意外导致 decode 方法抛出 NullPointerException,所以屏蔽了空数据的情况
// 坑:浏览器空数据
if (out.size() == 0) {
System.out.println("关闭连接:"+ socketChannel.getRemoteAddress());
socketChannel.close();
return;
}
长连接与短连接
通过 响应首部可以控制保持连接还是每次重新建立连接。
public void doGet(HttpRequest request, HttpResponse response) {
System.out.println(request.url);
response.body = "<html><h1>Hello World!</h1></html>";
response.headers = new HashMap<>();
// response.headers.put("Connection", "keep-alive");
response.headers.put("Connection", "close");
}
加入关闭连接请求头 response.headers.put("Connection", "close");
实验结果如下图:
如果改为 response.headers.put("Connection", "keep-alive");
实验结果如下图:
总结
本文使用 java.nio.channels 中的类实现了一个简陋的 Http 服务器。实现了网络 IO 逻辑与业务逻辑分离,分别运行在 IO 线程和 业务线程池中。
- HTTP 是基于 TCP 协议之上的半双工通信协议,客户端向服务端发起请求,服务端处理完成后,给出响应。
- HTTP 报文主要由三部分构成:起始行,首部,主体。
其中起始行是必须的,首部和主体都是非必须的。起始行和首部都采用文本格式且都是结构化的。主体部分既可以是二进制数据也可以是文本格式的数据。
参考代码
- 工具类
Code
,因为sun.net.httpserver.Code
无法直接使用,所以拷贝一份出来使用。 - HttpRequest & HttpResponse 实体类
public class HttpRequest {
String method; // 请求方法
String url; // 请求地址
String version; // http版本
Map<String, String> headers; // 请求头
String body; // 请求主体
}
public class HttpResponse {
String version; // http版本
int code; // 响应码
String status; // 状态信息
Map<String, String> headers; // 响应头
String body; // 响应数据
}
- HttpServlet
public class HttpServlet {
public void doGet(HttpRequest request, HttpResponse response) {
System.out.println(request.url);
response.body = "<html><h1>Hello World!</h1></html>";
}
public void doPost(HttpRequest request, HttpResponse response) {
}
}
- HttpServer 一个简陋的 Http 服务器
public class HttpServer {
final int port;
private final Selector selector;
private final HttpServlet servlet;
ExecutorService service;
/**
* 初始化
* @param port
* @param servlet
* @throws IOException
*/
public HttpServer(int port, HttpServlet servlet) throws IOException {
this.port = port;
this.servlet = servlet;
this.service = Executors.newFixedThreadPool(5);
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(80));
selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 启动
*/
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
poll(selector);
} catch (IOException e) {
System.out.println("服务器异常退出...");
e.printStackTrace();
}
}
}, "Selector-IO").start();
}
public static void main(String[] args) throws IOException {
try {
HttpServer server = new HttpServer(80, new HttpServlet());
server.start();
System.out.println("服务器启动成功, 您现在可以访问 http://localhost:" + server.port);
} catch (IOException e) {
System.out.println("服务器启动失败...");
e.printStackTrace();
}
System.in.read();
}
/**
* 轮询键集
* @param selector
* @throws IOException
*/
private void poll(Selector selector) throws IOException {
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
iterator.remove();
}
}
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
// 1. 读取数据
ByteArrayOutputStream out = new ByteArrayOutputStream();
read(socketChannel, out);
// 坑:浏览器空数据
if (out.size() == 0) {
System.out.println("关闭连接:"+ socketChannel.getRemoteAddress());
socketChannel.close();
return;
}
// 2. 解码
final HttpRequest request = decode(out.toByteArray());
// 3. 业务处理
service.submit(new Runnable() {
@Override
public void run() {
HttpResponse response = new HttpResponse();
if ("get".equalsIgnoreCase(request.method)) {
servlet.doGet(request, response);
} else if ("post".equalsIgnoreCase(request.method)) {
servlet.doPost(request, response);
}
// 获得响应
key.interestOps(SelectionKey.OP_WRITE);
key.attach(response);
// 坑:异步唤醒
key.selector().wakeup();
// socketChannel.register(key.selector(), SelectionKey.OP_WRITE, response);
}
});
}
/**
* 从缓冲区读取数据并写入 {@link ByteArrayOutputStream}
* @param socketChannel
* @param out
* @throws IOException
*/
private void read(SocketChannel socketChannel, ByteArrayOutputStream out) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (socketChannel.read(buffer) > 0) {
buffer.flip(); // 切换到读模式
out.write(buffer.array());
buffer.clear(); // 清理缓冲区
}
}
/**
* 解码 Http 请求报文
* @param array
* @return
*/
private HttpRequest decode(byte[] array) {
try {
HttpRequest request = new HttpRequest();
ByteArrayInputStream inStream = new ByteArrayInputStream(array);
InputStreamReader reader = new InputStreamReader(inStream);
BufferedReader in = new BufferedReader(reader);
// 解析起始行
String firstLine = in.readLine();
System.out.println(firstLine);
String[] split = firstLine.split(" ");
request.method = split[0];
request.url = split[1];
request.version = split[2];
// 解析首部
Map<String, String> headers = new HashMap<>();
while (true) {
String line = in.readLine();
// 首部以一个空行结束
if ("".equals(line.trim())) {
break;
}
String[] keyValue = line.split(":");
headers.put(keyValue[0], keyValue[1]);
}
request.headers = headers;
// 解析请求主体
CharBuffer buffer = CharBuffer.allocate(1024);
CharArrayWriter out = new CharArrayWriter();
while (in.read(buffer) > 0) {
buffer.flip();
out.write(buffer.array());
buffer.clear();
}
request.body = out.toString();
return request;
} catch (Exception e) {
System.out.println("解码 Http 失败");
e.printStackTrace();
}
return null;
}
private void handleWrite(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
HttpResponse response = (HttpResponse) key.attachment();
// 编码
byte[] bytes = encode(response);
channel.write(ByteBuffer.wrap(bytes));
key.interestOps(SelectionKey.OP_READ);
key.attach(null);
}
/**
* http 响应报文编码
* @param response
* @return
*/
private byte[] encode(HttpResponse response) {
StringBuilder builder = new StringBuilder();
if (response.code == 0) {
response.code = 200; // 默认成功
}
// 响应起始行
builder.append("HTTP/1.1 ").append(response.code).append(" ").append(Code.msg(response.code)).append("\r\n");
// 响应头
if (response.body != null && response.body.length() > 0) {
builder.append("Content-Length:").append(response.body.length()).append("\r\n");
builder.append("Content-Type:text/html\r\n");
}
if (response.headers != null) {
String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue())
.collect(Collectors.joining("\r\n"));
if (!headStr.isEmpty()) {
builder.append(headStr).append("\r\n");
}
}
// 首部以一个空行结束
builder.append("\r\n");
if (response.body != null) {
builder.append(response.body);
}
return builder.toString().getBytes();
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
Selector selector = key.selector();
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println(socketChannel);
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}