基于 Java NIO 实现简单的 HTTP 服务器
1.簡介
本文是上一篇文章實踐篇,在上一篇文章中,我分析了選擇器 Selector 的原理。本篇文章,我們來說說 Selector 的應用,如標題所示,這里我基于 Java NIO 實現了一個簡單的 HTTP 服務器。在接下來的章節中,我會詳細講解 HTTP 服務器實現的過程。另外,本文所對應的代碼已經上傳到 GitHub 上了,需要的自取,倉庫地址為?toyhttpd。好了,廢話不多說,進入正題吧。
?2. 實現
本節所介紹的 HTTP 服務器是一個很簡單的實現,僅支持 HTTP 協議極少的特性。包括識別文件后綴,并返回相應的 Content-Type。支持200、400、403、404、500等錯誤碼等。由于支持的特性比較少,所以代碼邏輯也比較簡單,這里羅列一下:
接下來我們按照處理請求和響應請求兩步操作,來說說代碼實現。先來看看核心的代碼結構,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | /*** TinyHttpd** @author code4wt* @date 2018-03-26 22:28:44*/ public class TinyHttpd {private static final int DEFAULT_PORT = 8080;private static final int DEFAULT_BUFFER_SIZE = 4096;private static final String INDEX_PAGE = "index.html";private static final String STATIC_RESOURCE_DIR = "static";private static final String META_RESOURCE_DIR_PREFIX = "/meta/";private static final String KEY_VALUE_SEPARATOR = ":";private static final String CRLF = "\r\n";private int port;public TinyHttpd() {this(DEFAULT_PORT);}public TinyHttpd(int port) {this.port = port;}public void start() throws IOException {// 初始化 ServerSocketChannelServerSocketChannel ssc = ServerSocketChannel.open();ssc.socket().bind(new InetSocketAddress("localhost", port));ssc.configureBlocking(false);// 創建 SelectorSelector selector = Selector.open();// 注冊事件ssc.register(selector, SelectionKey.OP_ACCEPT);while(true) {int readyNum = selector.select();if (readyNum == 0) {continue;}Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> it = selectedKeys.iterator();while (it.hasNext()) {SelectionKey selectionKey = it.next();it.remove();if (selectionKey.isAcceptable()) {SocketChannel socketChannel = ssc.accept();socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);} else if (selectionKey.isReadable()) {// 處理請求request(selectionKey);selectionKey.interestOps(SelectionKey.OP_WRITE);} else if (selectionKey.isWritable()) {// 響應請求response(selectionKey);}}}}private void request(SelectionKey selectionKey) throws IOException {...}private Headers parseHeader(String headerStr) {...}private void response(SelectionKey selectionKey) throws IOException {...}private void handleOK(SocketChannel channel, String path) throws IOException {...}private void handleNotFound(SocketChannel channel) {...}private void handleBadRequest(SocketChannel channel) {...}private void handleForbidden(SocketChannel channel) {...}private void handleInternalServerError(SocketChannel channel) {...}private void handleError(SocketChannel channel, int statusCode) throws IOException {...}private ByteBuffer readFile(String path) throws IOException {...}private String getExtension(String path) {...}private void log(String ip, Headers headers, int code) {} } |
上面的代碼是 HTTP 服務器的核心類的代碼結構。其中 request 負責處理請求,response 負責響應請求。handleOK 方法用于響應正常的請求,handleNotFound 等方法用于響應出錯的請求。readFile 方法用于讀取資源文件,getExtension 則是獲取文件后綴。
?2.1 處理請求
處理請求的邏輯比較簡單,主要的工作是解析消息頭。相關代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | private void request(SelectionKey selectionKey) throws IOException {// 從通道中讀取請求頭數據SocketChannel channel = (SocketChannel) selectionKey.channel();ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);channel.read(buffer);buffer.flip();byte[] bytes = new byte[buffer.limit()];buffer.get(bytes);String headerStr = new String(bytes);try {// 解析請求頭Headers headers = parseHeader(headerStr);// 將請求頭對象放入 selectionKey 中selectionKey.attach(Optional.of(headers));} catch (InvalidHeaderException e) {selectionKey.attach(Optional.empty());} }private Headers parseHeader(String headerStr) {if (Objects.isNull(headerStr) || headerStr.isEmpty()) {throw new InvalidHeaderException();}// 解析請求頭第一行int index = headerStr.indexOf(CRLF);if (index == -1) {throw new InvalidHeaderException();}Headers headers = new Headers();String firstLine = headerStr.substring(0, index);String[] parts = firstLine.split(" ");/** 請求頭的第一行必須由三部分構成,分別為 METHOD PATH VERSION* 比如:* GET /index.html HTTP/1.1*/if (parts.length < 3) {throw new InvalidHeaderException();}headers.setMethod(parts[0]);headers.setPath(parts[1]);headers.setVersion(parts[2]);// 解析請求頭屬于部分parts = headerStr.split(CRLF);for (String part : parts) {index = part.indexOf(KEY_VALUE_SEPARATOR);if (index == -1) {continue;}String key = part.substring(0, index);if (index == -1 || index + 1 >= part.length()) {headers.set(key, "");continue;}String value = part.substring(index + 1);headers.set(key, value);}return headers; } |
簡單總結一下上面的代碼邏輯,首先是從通道中讀取請求頭,然后解析讀取到的請求頭,最后將解析出的 Header 對象放入 selectionKey 中。處理請求的邏輯很簡單,不多說了。
?2.2 響應請求
看完處理請求的邏輯,接下來再來看看響應請求的邏輯。代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | private void response(SelectionKey selectionKey) throws IOException {SocketChannel channel = (SocketChannel) selectionKey.channel();// 從 selectionKey 中取出請求頭對象Optional<Headers> op = (Optional<Headers>) selectionKey.attachment();// 處理無效請求,返回 400 錯誤if (!op.isPresent()) {handleBadRequest(channel);channel.close();return;}String ip = channel.getRemoteAddress().toString().replace("/", "");Headers headers = op.get();// 如果請求 /meta/ 路徑下的資源,則認為是非法請求,返回 403 錯誤if (headers.getPath().startsWith(META_RESOURCE_DIR_PREFIX)) {handleForbidden(channel);channel.close();log(ip, headers, FORBIDDEN.getCode());return;}try {handleOK(channel, headers.getPath());log(ip, headers, OK.getCode());} catch (FileNotFoundException e) {// 文件未發現,返回 404 錯誤handleNotFound(channel);log(ip, headers, NOT_FOUND.getCode());} catch (Exception e) {// 其他異常,返回 500 錯誤handleInternalServerError(channel);log(ip, headers, INTERNAL_SERVER_ERROR.getCode());} finally {channel.close();} }// 處理正常的請求 private void handleOK(SocketChannel channel, String path) throws IOException {ResponseHeaders headers = new ResponseHeaders(OK.getCode());// 讀取文件ByteBuffer bodyBuffer = readFile(path);// 設置響應頭headers.setContentLength(bodyBuffer.capacity());headers.setContentType(ContentTypeUtils.getContentType(getExtension(path)));ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());// 將響應頭和資源數據一同返回channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer}); }// 處理請求資源未發現的錯誤 private void handleNotFound(SocketChannel channel) {try {handleError(channel, NOT_FOUND.getCode());} catch (Exception e) {handleInternalServerError(channel);} }private void handleError(SocketChannel channel, int statusCode) throws IOException {ResponseHeaders headers = new ResponseHeaders(statusCode);// 讀取文件ByteBuffer bodyBuffer = readFile(String.format("/%d.html", statusCode));// 設置響應頭headers.setContentLength(bodyBuffer.capacity());headers.setContentType(ContentTypeUtils.getContentType("html"));ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());// 將響應頭和資源數據一同返回channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer}); } |
上面的代碼略長,不過邏輯仍然比較簡單。首先,要判斷請求頭存在,以及資源路徑是否合法。如果都合法,再去讀取資源文件,如果文件不存在,則返回 404 錯誤碼。如果發生其他異常,則返回 500 錯誤。如果沒有錯誤發生,則正常返回響應頭和資源數據。這里只貼了核心代碼,其他代碼就不貼了,大家自己去看吧。
?2.3 效果演示
分析完代碼,接下來看點輕松的吧。下面貼一張代碼的運行效果圖,如下:
?3.總結
本文所貼的代碼是我在學習 Selector 過程中寫的,核心代碼不到 300 行。通過動手寫代碼,也使得我加深了對 Selector 的了解。在學習 JDK 的過程中,強烈建議大家多動手寫代碼。通過寫代碼,并踩一些坑,才能更加熟練運用相關技術。這個是我寫 NIO 系列文章的一個感觸。
好了,本文到這里結束。謝謝閱讀!
- 本文鏈接:?https://www.tianxiaobo.com/2018/04/04/基于-Java-NIO-實現簡單的-HTTP-服務器/
from:?http://www.tianxiaobo.com/2018/04/04/%E5%9F%BA%E4%BA%8E-Java-NIO-%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84-HTTP-%E6%9C%8D%E5%8A%A1%E5%99%A8/?
總結
以上是生活随笔為你收集整理的基于 Java NIO 实现简单的 HTTP 服务器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java NIO之选择器
- 下一篇: AbstractQueuedSynchr