由於有個不會存在大量鏈接的小的Web服務器需求,不至於用上重量級服務器,因而本身動手寫一個服務器。java
同時也提供了一個簡單的Web框架。可以簡單的使用了。git
大致的需求包括github
會省略一些暫時影響察看的代碼。還不夠完善,供記錄問題和解決辦法之用,可能會修改許多地方。服務器
讓咱們開始吧~多線程
// 更新 2015年09月30日 關於讀事件框架
Project的地址 : Github異步
你們都知道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事件其實很簡單,就是能夠來了一個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 }
這個事件就能夠接收到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(); 進行強制選擇。
這個事件將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)由下面(以優先權的順序)決定: