netty系列之:來,手把手教你使用netty搭建一個DNS tcp服務器 簡介 搭建netty服務器 DNS服務器的消息處理 DNS客戶端消息請求 總結

簡介

在前面的文章中,我們提到了使用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章