BlackHole開發日記-使用三種不一樣IO模型實現一個DNS代理服務器

BlackHoleJ是一個DNS服務器。他的一個功能是,對於它解析不了的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&notify機制,從而使阻塞線程交出控制權,避免更多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。

AIO方式的DNS解析

發送方的僞代碼大概是這樣:

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開銷卻有了下降。

相關文章
相關標籤/搜索