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


因為有個不會存在大量連接的小的Web服務器需求,不至於用上重量級服務器,於是自己動手寫一個服務器。

同時也提供了一個簡單的Web框架。能夠簡單的使用了。

大體的需求包括

  1. 能夠處理HTTP協議。
  2. 能夠提供接口讓使用者編寫自己的服務。

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

讓我們開始吧~

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

Project的地址 : Github


從ServerSocket開始

點這里是這部分的完整代碼,可以對照察看

大家都知道HTTP協議使用的是TCP服務。 而要用TCP通信都得從ServerSocket開始。ServerSocket監聽指定IP地址指定端口之后,另一端便可以通過連接這個ServerSocket來建立一對一的Socket進行收發數據。

我們先從命令行參數里獲得要監聽的ip地址和端口號,當然沒有的話使用默認的。

 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並且可以攜帶一個對象。
slector.select() 會阻塞直到有注冊的事件來臨。 獲取一個SelectionKey之后需要使用iterator.next()將它從selectedKeys中去除,不然下次selector.select()仍然會獲取到這個key。

下面來分析每個事件。

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)當接收了一個
    無效的長度時必須能通知用戶。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM