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感興趣的朋友可以自行探索。

本文的程式碼,大家可以參考:

learn-netty4