1. 程式人生 > >NIO實踐-HTTP互動實現暨簡版Tomcat互動核心

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