徒手用Java來寫個Web服務器和框架吧<第一章:NIO篇>

由於有個不會存在大量鏈接的小的Web服務器需求,不至於用上重量級服務器,因而本身動手寫一個服務器。java

同時也提供了一個簡單的Web框架。可以簡單的使用了。git

大致的需求包括github

  1. 可以處理HTTP協議。
  2. 可以提供接口讓使用者編寫本身的服務。

會省略一些暫時影響察看的代碼。還不夠完善,供記錄問題和解決辦法之用,可能會修改許多地方。服務器

讓咱們開始吧~多線程

// 更新 2015年09月30日 關於讀事件框架

Project的地址 : Github異步


從ServerSocket開始

點這裏是這部分的完整代碼,能夠對照察看ide

你們都知道HTTP協議使用的是TCP服務。 而要用TCP通訊都得從ServerSocket開始。ServerSocket監聽指定IP地址指定端口以後,另外一端即可以經過鏈接這個ServerSocket來創建一對一的Socket進行收發數據。測試

咱們先從命令行參數裏得到要監聽的ip地址和端口號,固然沒有的話使用默認的。this

 1 public static void main(String[] args) {
 2     ...
 3     InetAddress ip = null;
 4     int port;
 5     if (args.length == 2 && args[1].matches(".+:\\d+")) {
 6         ...
 7             ip = InetAddress.getByName(address[0]);
 8         ...
 9     } else {
10         ...
11             ip = InetAddress.getLocalHost();
12         ...            
13         port = 8080;
14         System.out.println("未指定地址和端口,使用默認ip和端口..." + ip.getHostAddress() + ":" + port);
15     }
16 
17     Server server = new Server(ip, port);
18     server.start();
19 }

 

輸入是 start 123.45.67.89:8080 或者直接一個 start

InetAddress.getByName(address[0])經過一個IP地址的字符串構造一個InetAddress對象。

InetAddress.getLocalHost() 獲取localhost的InetAddress對象。


接下來看看Server類。

首先,這個服務器要輕量級,不宜建立太多線程。考慮使用NIO來進行IO處理,一個線程處理IO。因此咱們須要一個Selector來選擇已經就緒的管道,同時用一個線程池來處理任務。(能夠用Runtime.getRuntime().availableProcessors()獲取可用的處理器核數。)

Server啓動時首先進行ServerSocket的綁定以及其餘的初始化工做。

1     ServerSocketChannel serverChannel;
2     registerServices();
3     serverChannel = ServerSocketChannel.open();
4     serverChannel.bind(new InetSocketAddress(this.ip, this.port));
5     serverChannel.configureBlocking(false);
6     selector = Selector.open();
7     serverChannel.register(selector, SelectionKey.OP_ACCEPT);

 

registerServices() 暫時先忽略,是用來註冊用戶寫的服務的。

因爲是NIO,在這裏是用的ServerSocketChannel,綁定到ip和端口,設置好非阻塞,註冊ACCEPT事件。不設置非阻塞狀態是不能使用Selectior的。

而後開始循環監聽和處理事件

 

 1 public void start() {
 2     init();
 3     while (true) {
 4         ...
 5         selector.select();
 6         ...
 7         Set<SelectionKey> readyKeys = selector.selectedKeys();
 8         Iterator<SelectionKey> iterator = readyKeys.iterator();
 9         while (iterator.hasNext()) {
10             SelectionKey key = iterator.next();
11             iterator.remove();
12             if (key.isAcceptable()) {
13                 ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
14                 ...//處理接受事件
15             } else if (key.isReadable()) {
16                 SocketChannel client = (SocketChannel) key.channel();
17                 ...//處理讀事件
18             } else if (key.isWritable()) {
19                 SocketChannel client = (SocketChannel) key.channel();
20                 ...//處理寫事件
21             }
22             ...
23         }
24     }
25 }

在我看來SelectionKey指的就是一個事件,它關聯一個channel而且能夠攜帶一個對象。
會阻塞直到有註冊的事件來臨。 獲取一個SelectionKey以後須要使用將它從selectedKeys中去除,否則下次仍然會獲取到這個key。slector.select()iterator.next()selector.select()

下面來分析每一個事件。

Accept事件

Accept事件其實很簡單,就是能夠來了一個Socket能夠創建鏈接了。 那麼就像下面這樣,accept建立一個鏈接後,在SocketChannel監聽Read事件,等到有數據能夠讀的時候就能夠進行讀取。

1 if (key.isAcceptable()) {
2     ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
3     SocketChannel client = serverSocket.accept();
4     client.configureBlocking(false);
5     client.register(selector, SelectionKey.OP_READ);
6 }

 

Read事件

這個事件就能夠接收到HTTP請求了。讀取到數據以後提交給Controller進行異步的HTTP請求解析,根據FilePath轉發給服務處理類。處理完後會給通道註冊WRITE的監聽。client.register(selector, SelectionKey.OP_WRITE)

並讓key攜帶Response對象(將在後續章節寫出)

1 if (key.isReadable()) {
2     SocketChannel client = (SocketChannel) key.channel();
3     ByteBuffer buffer = ByteBuffer.allocate(4096);
4     client.read(buffer);
5     executor.execute(new Controller(buffer, client, selector));
6 }

 

這裏存在的問題是不知道如何處理過大的請求,或許能夠利用傳輸長度[1]重複讀取再合併?

同時還有另外一個問題。在 selector.select() 已經阻塞後,在另外一個線程註冊了事件,select沒法獲取,在只有一個鏈接的測試環境下彷佛沒辦法。

因此仍需定一個超時時間。好比 if (selector.select(500) == 0) { continue; }  

------更新 2015年09月30日------

屢次實驗發現,一次請求可能不是一次讀完。因此根據讀到的http首部中的Content-Length進行持續讀取

因此決定直接把channel直接給Connector(原爲Controller)處理。同時取消對讀取事件的興趣。

SocketChannel client = (SocketChannel) key.channel();
executor.execute(new Connector(client, selector));

key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);

 

另外關於在另外一個線程註冊事件select已經在阻塞結果沒法知道的問題。

可使用 selector.wakeup(); 進行強制選擇。

Write事件

這個事件將Response寫入SocketChannel。

SocketChannel client = (SocketChannel) key.channel();
Response response = (Response) key.attachment();
ByteBuffer byteBuffer = response.getByteBuffer();
if (byteBuffer.hasRemaining()) {
    client.write(byteBuffer);
}
if (!byteBuffer.hasRemaining()) {
    key.cancel();
    client.close();
}

 


若是發現什麼問題或者有什麼建議請指教。謝謝~

 

附錄區:

[1] 當消息主體出如今消息中時,一條消息的傳輸長度(transfer-length)是消息主體(messagebody)
的長度;也就是說在實體主體被應用了傳輸編碼(transfer-coding)後。當消息中出現
消息主體時,消息主體的傳輸長度(transfer-length)由下面(以優先權的順序)決定:

  1. 任何不能包含消息主體(message-body)的消息(這種消息如1xx,204和304響應和任
    何HEAD方法請求的響應)老是被頭域後的第一個空行(CRLF)終止,無論消息裏是否存在
    實體頭域(entity-header fields)。
  2. 若是Transfer-Encoding頭域(見14.41節)出現,而且它的域值是非」「dentity」傳輸編碼
    值,那麼傳輸長度(transfer-length)被「塊」(chunked)傳輸編碼定義,除非消息由於經過
    關閉鏈接而結束。
  3. 若是出現Content-Length頭域(屬於實體頭域)(見14.13節),那麼它的十進制值(以
    字節表示)即表明實體主體長度(entity-length,譯註:實體長度其實就是實體主體的長度,
    之後把entity-length翻譯成實體主體的長度)又表明傳輸長度(transfer-length)。Content-
    Length 頭域不能包含在消息中,若是實體主體長度(entity-length)和傳輸長度(transferlength)
    二者不相等(也就是說,出現Transfer-Encodind頭域)。若是一個消息即存在傳輸譯
    碼(Transfer-Encoding)頭域而且也Content-Length頭域,後者會被忽略。
  4. 若是消息用到媒體類型「multipart/byteranges」,而且傳輸長度(transfer-length)另外也沒
    有指定,那麼這種自我定界的媒體類型定義了傳輸長度(transfer-length)。這種媒體類型不能
    被利用除非發送者知道接收者能怎樣去解析它; HTTP1.1客戶端請求裏若是出現Range頭域
    而且帶有多個字節範圍(byte-range)指示符,這就意味着客戶端能解析multipart/byteranges
    響應。
    一個Range請求頭域可能會被一個不能理解multipart/byteranges的HTTP1.0代理(proxy)
    再次轉發;在這種狀況下,服務器必須能利用這節的1,3或5項裏定義的方法去定界此消息。
  5. 經過服務器關閉鏈接能肯定消息的傳輸長度。(請求端不能經過關閉鏈接來指明請求消息體的結束,由於這樣可讓服務器沒有機會繼續給予響應)。爲了與HTTP/1.0應用程序兼容,包含HTTP/1.1消息主體的請求必須包括一個有效的內容長度(Content-Length)頭域,除非服務器是HTTP/1.1遵循的。若是一個請求包含一個消息主體而且沒有給出內容長度(Content-Length),那麼服務器若是不能判斷消息長度的話應該以400響應(錯誤的請求),或者以411響應(要求長度)若是它堅持想要收到一個有效內容長度(Content-length)。全部的能接收實體的HTTP/1.1應用程序必須能接受"chunked"的傳輸編碼(3.6節),所以當消息的長度不能被提早肯定時,能夠利用這種機制來處理消息。消息不能同時都包括內容長度(Content-Length)頭域和非identity傳輸編碼。若是消息包括了一個非identity的傳輸編碼,內容長度(Content-Length)頭域必須被忽略.當內容長度(Content-Length)頭域出如今一個具備消息主體(message-body)的消息裏,它的域值必須精確匹配消息主體裏字節數量。HTTP/1.1用戶代理(user agents)當接收了一個無效的長度時必須能通知用戶。
相關文章
相關標籤/搜索