netty系列之:來,手把手教你使用netty搭建一個DNS tcp伺服器
簡介
在前面的文章中,我們提到了使用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感興趣的朋友可以自行探索。
本文的程式碼,大家可以參考: