BlackHoleJ是一個DNS服務器。他的一個功能是,對於它解析不了的DNS請求,它將請求轉發到另一臺DNS服務器,而後再將其響應返回給客戶端,起到一個DNS代理的做用。服務器
這個功能的實現經歷了三個版本,也對應了三個經典的IO模型。多線程
###BIO模型(Blocking I/O)異步
BlackHoleJ代理模式最開始的IO模型,實現很簡單,當client請求過來時,新建一個線程處理,而後再線程中調用DatagramChannel發送UDP包,同時阻塞等待,最後接收到結果後返回。測試
public byte[] forward(byte[] query) throws IOException { DatagramChannel dc = null; dc = DatagramChannel.open(); SocketAddress address = new InetSocketAddress(configure.getDnsHost(), Configure.DNS_PORT); dc.connect(address); ByteBuffer bb = ByteBuffer.allocate(512); bb.put(query); bb.flip(); dc.send(bb, address); bb.clear(); dc.receive(bb); bb.flip(); byte[] copyOfRange = Arrays.copyOfRange(bb.array(), 0, 512); return copyOfRange; }
其中dc.receive(bb)一步是阻塞的。由於請求外部DNS服務器每每耗時較長,因此爲了達到快速響應,不得不開不少線程進行處理。同時每一個線程都須要進行輪詢dc.receive(bb)是否可用,會消耗更多CPU資源。線程
###Select模型(I/O multiplexing)代理
BlackHoleJ 1.1開始使用的IO模型。由於DNS使用UDP協議,而UDP實際上是無鏈接的,因此全部請求以及響應複用一個DatagramChannel也毫無問題。同時預先使用DatagramChannel.bind(port)綁定某端口,那麼對外部DNS服務器的轉發和接收均可以使用這個端口。惟一須要作的就是經過DNS包的特徵,來判斷究竟是哪一個客戶端的請求!而這個特徵也很好選擇,DNS包的headerId和question域便可知足需求。code
發送方的僞代碼大概是這樣:對象
public byte[] forward(byte[] queryBytes) { multiUDPReceiver.putForwardAnswer(query, forwardAnswer); forward(queryBytes); forwardAnswer.getLock.getCondition().await(); return answer.getAnswer(); }
接收方是一個獨立的線程,代碼大概是這樣的:ip
public void receive() { ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { datagramChannel.receive(byteBuffer); final byte[] answer = Arrays.copyOfRange(byteBuffer.array(), 0, 512); getForwardAnswer(answer).setAnswer(answer); getForwardAnswer(answer).getLock.getCondition().notify(); } }
這裏forwardAnswer是一個包含了響應結果和一個鎖的對象(這裏用到了Java的Condition.wait¬ify機制,從而使阻塞線程交出控制權,避免更多CPU輪詢)。還有一部分是multiUDPReceiver。這裏multiUDPReceiver.putForwardAnswer(query, forwardAnswer)其實是把forwardAnswer註冊到一個Map裏。資源
這樣作的好處是僅僅在一個線程檢查本來的多路I/O是否就緒,也就是I/O multiplexing。這跟Linux下select模型是同樣的。
###AIO模型(Asynchronous I/O)
BlackHoleJ 1.1.3-dev開始,使用了基於回調的AIO模型。這裏創建了UDPConnectionResponser對象,裏面封裝了client的IP和來源端口號。每次收到外部DNS響應時,再根據響應內容找到這個client的IP和來源端口號,從新發送便可。
這實際上就是封裝了callback的異步IO。
發送方的僞代碼大概是這樣:
public void forward(byte[] queryBytes) { multiUDPReceiver.putForwardAnswer(query, forwardAnswer); forward(queryBytes); }
接收方的代碼大概是這樣:
public void receive() { ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { datagramChannel.receive(byteBuffer); final byte[] answer = Arrays.copyOfRange(byteBuffer.array(), 0, 512); getForwardAnswer(answer).getResponser().response(answer); } }
這裏getResponser().response()直接將結果返回給客戶端。
###測試:
使用queryperf進行了測試,使用AIO模型以後,僅僅單線程就達到了40000qps,比1.1.2效率高出了25%,而CPU開銷卻有了下降。