因為有個不會存在大量連接的小的Web服務器需求,不至於用上重量級服務器,於是自己動手寫一個服務器。
同時也提供了一個簡單的Web框架。能夠簡單的使用了。
大體的需求包括
- 能夠處理HTTP協議。
- 能夠提供接口讓使用者編寫自己的服務。
會省略一些暫時影響察看的代碼。還不夠完善,供記錄問題和解決辦法之用,可能會修改許多地方。
讓我們開始吧~
// 更新 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)由下面(以優先權的順序)決定:
- 任何不能包含消息主體(message-body)的消息(這種消息如1xx,204和304響應和任
何HEAD方法請求的響應)總是被頭域后的第一個空行(CRLF)終止,不管消息里是否存在
實體頭域(entity-header fields)。 - 如果Transfer-Encoding頭域(見14.41節)出現,並且它的域值是非”“dentity”傳輸編碼
值,那么傳輸長度(transfer-length)被“塊”(chunked)傳輸編碼定義,除非消息因為通過
關閉連接而結束。 - 如果出現Content-Length頭域(屬於實體頭域)(見14.13節),那么它的十進制值(以
字節表示)即代表實體主體長度(entity-length,譯注:實體長度其實就是實體主體的長度,
以后把entity-length翻譯成實體主體的長度)又代表傳輸長度(transfer-length)。Content-
Length 頭域不能包含在消息中,如果實體主體長度(entity-length)和傳輸長度(transferlength)
兩者不相等(也就是說,出現Transfer-Encodind頭域)。如果一個消息即存在傳輸譯
碼(Transfer-Encoding)頭域並且也Content-Length頭域,后者會被忽略。 - 如果消息用到媒體類型“multipart/byteranges”,並且傳輸長度(transfer-length)另外也沒
有指定,那么這種自我定界的媒體類型定義了傳輸長度(transfer-length)。這種媒體類型不能
被利用除非發送者知道接收者能怎樣去解析它; HTTP1.1客戶端請求里如果出現Range頭域
並且帶有多個字節范圍(byte-range)指示符,這就意味着客戶端能解析multipart/byteranges
響應。
一個Range請求頭域可能會被一個不能理解multipart/byteranges的HTTP1.0代理(proxy)
再次轉發;在這種情況下,服務器必須能利用這節的1,3或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)當接收了一個
無效的長度時必須能通知用戶。