簡介
在前面的文章中,我們提到了使用netty構建tcp和udp的客戶端向已經公佈的DNS服務器進行域名請求服務。基本的流程是藉助於netty本身的NIO通道,將要查詢的信息封裝成爲DNSMessage,通過netty搭建的channel發送到服務器端,然後從服務器端接受返回數據,將其編碼爲DNSResponse,進行消息的處理。
那麼DNS Server是否可以用netty實現呢?
答案當然是肯定的,但是之前也講過了DNS中有很多DnsRecordType,所以如果想實現全部的支持類型可能並現實,這裏我們就以最簡單和最常用的A類型爲例,用netty來實現一下DNS的TCP服務器。
搭建netty服務器
因爲是TCP請求,所以這裏使用基於NIO的netty server服務,也就是NioEventLoopGroup和NioServerSocketChannel,netty服務器的代碼如下:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup,
workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new Do53ServerChannelInitializer());
final Channel channel = bootstrap.bind(dnsServerPort).channel();
channel.closeFuture().sync();
因爲是服務器,所以我們需要兩個EventLoopGroup,一個是bossGroup,一個是workerGroup。
將這兩個group傳遞給ServerBootstrap,並指定channel是NioServerSocketChannel,然後添加自定義的Do53ServerChannelInitializer即可。
Do53ServerChannelInitializer中包含了netty自帶的tcp編碼解碼器和自定義的服務器端消息處理方式。
這裏dnsServerPort=53,也是默認的DNS服務器的端口值。
DNS服務器的消息處理
Do53ServerChannelInitializer是我們自定義的initializer,裏面爲pipline添加了消息的處理handler:
class Do53ServerChannelInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(
new TcpDnsQueryDecoder(),
new TcpDnsResponseEncoder(),
new Do53ServerInboundHandler());
}
}
這裏我們添加了兩個netty自帶的編碼解碼器,分別是TcpDnsQueryDecoder和TcpDnsResponseEncoder。
對於netty服務器來說,接收到的是ByteBuf消息,爲了方便服務器端的消息讀取,需要將ByteBuf解碼爲DnsQuery,這也就是TcpDnsQueryDecoder在做的事情。
public final class TcpDnsQueryDecoder extends LengthFieldBasedFrameDecoder
TcpDnsQueryDecoder繼承自LengthFieldBasedFrameDecoder,也就是以字段長度來區分對象的起始位置。這和TCP查詢傳過來的數據結構是一致的。
下面是它的decode方法:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf)super.decode(ctx, in);
return frame == null ? null : DnsMessageUtil.decodeDnsQuery(this.decoder, frame.slice(), new DnsQueryFactory() {
public DnsQuery newQuery(int id, DnsOpCode dnsOpCode) {
return new DefaultDnsQuery(id, dnsOpCode);
}
});
}
decode接受一個ByteBuf對象,首先調用LengthFieldBasedFrameDecoder的decode方法,將真正需要解析的內容解析出來,然後再調用DnsMessageUtil的decodeDnsQuery方法將真正的ByteBuf內容解碼成爲DnsQuery返回。
這樣就可以在自定義的handler中處理DnsQuery消息了。
上面代碼中,自定義的handler叫做Do53ServerInboundHandler:
class Do53ServerInboundHandler extends SimpleChannelInboundHandler<DnsQuery>
從定義看,Do53ServerInboundHandler要處理的消息就是DnsQuery。
看一下它的channelRead0方法:
protected void channelRead0(ChannelHandlerContext ctx,
DnsQuery msg) throws Exception {
DnsQuestion question = msg.recordAt(DnsSection.QUESTION);
log.info("Query is: {}", question);
ctx.writeAndFlush(newResponse(msg, question, 1000, QUERY_RESULT));
}
我們從DnsQuery的QUESTION section中拿到DnsQuestion,然後解析DnsQuestion的內容,根據DnsQuestion的內容返回一個response給客戶端。
這裏的respone是我們自定義的:
private DefaultDnsResponse newResponse(DnsQuery query,
DnsQuestion question,
long ttl, byte[]... addresses) {
DefaultDnsResponse response = new DefaultDnsResponse(query.id());
response.addRecord(DnsSection.QUESTION, question);
for (byte[] address : addresses) {
DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(
question.name(),
DnsRecordType.A, ttl, Unpooled.wrappedBuffer(address));
response.addRecord(DnsSection.ANSWER, queryAnswer);
}
return response;
}
上面的代碼封裝了一個新的DefaultDnsResponse對象,並使用query的id作爲DefaultDnsResponse的id。並將question作爲response的QUESEION section。
除了QUESTION section,response中還需要ANSWER section,這個ANSWER section需要填充一個DnsRecord。
這裏構造了一個DefaultDnsRawRecord,傳入了record的name,type,ttl和具體內容。
最後將構建好的DefaultDnsResponse返回。
因爲客戶端查詢的是A address,按道理我們需要通過QUESTION中傳入的domain名字,然後根據DNS服務器中存儲的記錄進行查找,最終返回對應域名的IP地址。
但是因爲我們只是模擬的DNS服務器,所以並沒有真實的域名IP記錄,所以這裏我們僞造了一個ip地址:
private static final byte[] QUERY_RESULT = new byte[]{46, 53, 107, 110};
然後調用Unpooled的wrappedBuffer方法,將byte數組轉換成爲ByteBuf,傳入DefaultDnsRawRecord的構造函數中。
這樣我們的DNS服務器就搭建好了。
DNS客戶端消息請求
上面我們搭建好了DNS服務器,接下來就可以使用DNS客戶端來請求DNS服務器了。
這裏我們使用之前創建好的netty DNS客戶端,只不過進行少許改動,將DNS服務器的域名和IP地址替換成下面的值:
Do53TcpClient client = new Do53TcpClient();
final String dnsServer = "127.0.0.1";
final int dnsPort = 53;
final String queryDomain ="www.flydean.com";
client.startDnsClient(dnsServer,dnsPort,queryDomain);
dnsServer就填本機的IP地址,dnsPort就是我們剛剛創建的默認端口53。
首先運行DNS服務器:
INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2] REGISTERED
INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2] BIND: 0.0.0.0/0.0.0.0:53
INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] ACTIVE
可以看到DNS服務器已經準備好了,綁定的端口是53。
然後運行上面的客戶端,在客戶端可以得到下面的結果:
INFO c.f.d.Do53TcpChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO c.f.d.Do53TcpChannelInboundHandler - ip address is: 46.53.107.110
可以看到DNS查詢成功,並且返回了我們在服務器中預設的值。
然後再看一下服務器端的輸出:
INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] READ: [id: 0x44d4c761, L:/127.0.0.1:53 - R:/127.0.0.1:65471]
INFO i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] READ COMPLETE
INFO c.f.d.Do53ServerInboundHandler - Query is: DefaultDnsQuestion(www.flydean.com. IN A)
可以看到服務器端成功和客戶端建立了連接,併成功接收到了客戶端的查詢請求。
總結
以上就是使用netty默認DNS服務器端的實現原理和例子。因爲篇幅有限,這裏只是默認了type爲A address的情況,對其他type感興趣的朋友可以自行探索。
本文的代碼,大家可以參考: