NIO實踐-HTTP互動實現暨簡版Tomcat互動核心
今天就NIO實現簡單的HTTP互動做一下筆記,進而來加深Tomcat原始碼印象。
一、關於HTTP
1、HTTP的兩個顯著特點,HTTP是一種可靠的超文字傳輸協議
第一、實際中,瀏覽器作為客戶端,每次訪問,必須明確指定IP、PORT。這是因為,HTTP協議底層傳輸就是使用的TCP方式。
第二、HTTP協議作為一種規範,簡單理解,首先,它傳輸的是文字(即字串,這個是區別於二級制資料的)。其次,他對文字的格式是有要求的。
2、HTTP約定的報文格式
對於以下報文格式,我們只需要對拿到的資料,進行readLine,然後做基於換行、回車、空格的判斷、切割等,就能拿到所有資訊。
二、系統架構
基於第一節的結論,我們就能啟動NIO作為服務端,然後用瀏覽器來發起客戶端接入、傳送資料,然後服務端回執。瀏覽器顯示回執。其中,瀏覽器核心持有一個客戶端SocketChannel,並且會自動維護其事件監聽。並且會自動按照HTTP協議報文格式來解析服務端返回的報文,並自動渲染。所以,我們只需要關注服務端,這裡涉及一下幾個步驟:
<1>、接收瀏覽器SocketChannel傳送的資料。
<2>、解碼:進行請求報文解析。
<3>、編碼:計算響應資料,並將響應資料封裝為HTTP協議格式。
<4>、寫入SocketChannel,即傳送給瀏覽器。
三、服務初始化
1、伺服器例項宣告
我們使用NO作為服務端,所以埠、多路複用器這些必不可少。與此同時,我們需要一個執行緒池去專門進行業務處理,其中具體的業務處理交給HttpServlet。
1 public class SimpleHttpServer { 2 // 服務埠 3 private int port; 4 // 處理器 5 private HttpServlet servlet; 6 // 輪詢器 7 private final Selector selector; 8 // 啟停標識 9 private volatile boolean run = false; 10 // 需要註冊的Channel,避免與輪詢器產生死鎖 11 private Set<SocketChannel> allConnections = new HashSet<>(); 12 // 執行業務執行緒池 13 private ExecutorService executor = Executors.newFixedThreadPool(5); 14 15 public SimpleHttpServer(int port, HttpServlet servlet) throws IOException { 16 this.port = port; 17 this.servlet = servlet; 18 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 19 selector = Selector.open(); 20 serverSocketChannel.bind(new InetSocketAddress(port)); 21 serverSocketChannel.configureBlocking(false); 22 // 一旦初始化就開始監聽客戶端接入事件 23 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 24 } 25 }
2、業務處理HttpServlet的細節
HttpServlet
1 public interface HttpServlet { 2 void doGet(Request request, Response response); 3 void doPost(Request request, Response response); 4 }
Request
1 public class Request { 2 Map<String, String> heads; 3 String url; 4 String method; 5 String version; 6 //請求內容 7 String body; 8 Map<String, String> params; 9 }
Response
1 public class Response { 2 Map<String, String> headers; 3 // 狀態碼 4 int code; 5 //返回結果 6 String body; 7 }
3、編解碼相關
編碼
1 //編碼Http 服務 2 private byte[] encode(Response response) { 3 StringBuilder builder = new StringBuilder(512); 4 builder.append("HTTP/1.1 ").append(response.code).append(Code.msg(response.code)).append("\r\n"); 5 if (response.body != null && response.body.length() != 0) { 6 builder.append("Content-Length: ") 7 .append(response.body.length()).append("\r\n") 8 .append("Content-Type: text/html\r\n"); 9 } 10 if (response.headers != null) { 11 String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue()) 12 .collect(Collectors.joining("\r\n")); 13 builder.append(headStr + "\r\n"); 14 } 15 builder.append("\r\n").append(response.body); 16 return builder.toString().getBytes(); 17 }
解碼
1 // 解碼Http服務 2 private Request decode(byte[] bytes) throws IOException { 3 Request request = new Request(); 4 BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes))); 5 String firstLine = reader.readLine(); 6 System.out.println(firstLine); 7 String[] split = firstLine.trim().split(" "); 8 request.method = split[0]; 9 request.url = split[1]; 10 request.version = split[2]; 11 //讀取請求頭 12 Map<String, String> heads = new HashMap<>(); 13 while (true) { 14 String line = reader.readLine(); 15 if (line.trim().equals("")) { 16 break; 17 } 18 String[] split1 = line.split(":"); 19 heads.put(split1[0], split1[1]); 20 } 21 request.heads = heads; 22 request.params = getUrlParams(request.url); 23 //讀取請求體 24 request.body = reader.readLine(); 25 return request; 26 }
獲取請求引數
1 private static Map getUrlParams(String url) { 2 Map<String, String> map = new HashMap<>(); 3 url = url.replace("?", ";"); 4 if (!url.contains(";")) { 5 return map; 6 } 7 if (url.split(";").length > 0) { 8 String[] arr = url.split(";")[1].split("&"); 9 for (String s : arr) { 10 if (s.contains("=")) { 11 String key = s.split("=")[0]; 12 String value = s.split("=")[1]; 13 map.put(key, value); 14 } else { 15 map.put(s, null); 16 } 17 } 18 return map; 19 } else { 20 return map; 21 } 22 }
四、互動實現
1、服務端啟動
對於已經初始化好的ServerSocketChannel,我們下來要做的無非就是while(true)輪詢selector。這個套路已經非常固定了。這裡我們啟動一個執行緒來輪詢:
1 public void start() { 2 this.run = true; 3 new Thread(() -> { 4 try { 5 while (run) { 6 selector.select(2000); 7 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); 8 while (iterator.hasNext()) { 9 SelectionKey key = iterator.next(); 10 iterator.remove(); 11 // 監聽客戶端接入 12 if (key.isAcceptable()) { 13 handleAccept(key); 14 } 15 // 監聽客戶端傳送訊息 16 else if (key.isReadable()) { 17 handleRead(key); 18 } 19 } 20 } 21 } catch (IOException e) { 22 e.printStackTrace(); 23 } 24 }, "selector-io").start(); 25 }
2、處理客戶端接入
1 // 當有客戶端接入的時候,為其註冊 可讀 事件監聽,等待客戶端傳送資料 2 private void handleAccept(SelectionKey key) throws IOException { 3 ServerSocketChannel channel = (ServerSocketChannel) key.channel(); 4 SocketChannel socketChannel = channel.accept(); 5 socketChannel.configureBlocking(false); 6 socketChannel.register(selector, SelectionKey.OP_READ); 7 }
3、處理客戶端傳送的訊息
1 /** 2 * 接收到客戶端傳送的資料進行處理 3 * 1、將客戶端的請求資料取出來,放到ByteArrayOutputStream。 4 * 2、將資料交給Servlet處理。 5 */ 6 private void handleRead(SelectionKey key) throws IOException { 7 final SocketChannel channel = (SocketChannel) key.channel(); 8 ByteBuffer buffer = ByteBuffer.allocate(1024); 9 final ByteArrayOutputStream out = new ByteArrayOutputStream(); 10 while (channel.read(buffer) > 0) { 11 buffer.flip(); 12 out.write(buffer.array(), 0, buffer.limit()); 13 buffer.clear(); 14 } 15 if (out.size() <= 0) { 16 channel.close(); 17 return; 18 } 19 process(channel, out); 20 }
4、業務處理併發送返回資料
1 private void process(SocketChannel channel, ByteArrayOutputStream out) { 2 executor.submit(() -> { 3 try { 4 Request request = decode(out.toByteArray()); 5 Response response = new Response(); 6 if (request.method.equalsIgnoreCase("GET")) { 7 servlet.doGet(request, response); 8 } else { 9 servlet.doPost(request, response); 10 } 11 channel.write(ByteBuffer.wrap(encode(response))); 12 } catch (Throwable e) { 13 e.printStackTrace(); 14 } 15 }); 16 }
五、單元測試
1 @Test 2 public void simpleHttpTest() throws IOException, InterruptedException { 3 SimpleHttpServer simpleHttpServer = new SimpleHttpServer(8080, new HttpServlet() { 4 @Override 5 public void doGet(Request request, Response response) { 6 System.out.println(request.url); 7 response.body="hello_word:" + System.currentTimeMillis(); 8 response.code=200; 9 response.headers=new HashMap<>(); 10 } 11 @Override 12 public void doPost(Request request, Response response) {} 13 }); 14 simpleHttpServer.start(); 15 new CountDownLatch(1).await(); 16 }
六、小結
以上,使用原生NIO實現了一個簡單的HTTP互動樣例,雖然,只做了自定義Servlet中做了GET方法的實現。其實原理已經很明瞭。真正的Tomcat互動核心,其實就是在這個原理的基礎上做了工業級軟體架構設計。小結一下:
<1>、瀏覽器位址列訪問,對於瀏覽器核心,可以理解觸發了兩個事件,OP_CONNECT事件、OP_WRITE事件。
<2>、NIO實現的服務端還是遵循固定套路。當監聽到OP_READ事件後,直接處理,然後回寫結果。
<3>、瀏覽器會在OP_WRITE事件後,自動變更監聽為OP_READ事件。等待服務端返回。
<4>、關於編碼、解碼、請求引數獲取等,均屬於HTTP協議的範疇,其實無關NIO。
<5>、服務端selector輪詢、accept接入channel註冊。這兩個操作之間使用的是用一個同步器,所以存在死鎖的風險。Tomcat裡邊做了很好的處理。這裡以後再聊。
謹以此筆記記錄一下原生NIO學習心得,為後續Tomcat原始碼部門鋪一下技術前提。
&n