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开销却有了下降。