HTTP請求和響應報文與簡單實現Java Http伺服器
阿新 • • 發佈:2020-08-12
報文結構
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);
}
}