乐趣区

关于dns:没错请求DNS服务器还可以使用UDP协议

简介

之前咱们讲到了如何在 netty 中构建 client 向 DNS 服务器进行域名解析申请。应用的是最常见的 TCP 协定,也叫做 Do53/TCP。

事实上除了 TCP 协定之外,DNS 服务器还接管 UDP 协定。这个协定叫做 DNS-over-UDP/53,简称 (“Do53”)。

本文将会一步一步率领大家在 netty 中搭建应用 UDP 的 DNS 客户端。

搭建 netty 客户端

因为这里应用的 UDP 协定,netty 为 UDP 协定提供了专门的 channel 叫做 NioDatagramChannel。EventLoopGroup 还是能够应用罕用的 NioEventLoopGroup, 这样咱们搭建 netty 客户端的代码和罕用的 NIO UDP 代码没有太大的区别,如下所示:

EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioDatagramChannel.class)
                    .handler(new Do53UdpChannelInitializer());
            final Channel ch = b.bind(0).sync().channel();

这里的 EventLoopGroup 应用的是 NioEventLoopGroup,作为 client 端 Bootstrap 的 group。

因为要应用 UDP 协定进行传输,所以这里的 channel 应用的是 NioDatagramChannel。

设置好 channel 之后,传入咱们自定义的 handler,netty client 就搭建结束了。

因为是 UDP,所以这里没有应用 TCP 中的 connect 办法,而是应用 bind 办法来取得 channel。

Do53UdpChannelInitializer 中蕴含了 netty 提供的 UDP DNS 的编码解码器,还有自定义的音讯处理器,咱们会在前面的章节中具体进行介绍。

在 netty 中发送 DNS 查问申请

搭建好 netty 客户端之后,接下来就是应用客户端发送 DNS 查问音讯了。

先看具体的查问代码:

int randomID = (int) (System.currentTimeMillis() / 1000);
            DnsQuery query = new DatagramDnsQuery(null, addr, randomID).setRecord(
                    DnsSection.QUESTION,
                    new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
            ch.writeAndFlush(query).sync();
            boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS);
                        if (!result) {log.error("DNS 查问失败");
                ch.close().sync();
            }

查问的逻辑是先构建 UDP 的 DnsQuery 申请包,而后将这申请包写入到 channel 中,而后期待音讯处理完毕。

DnsQuery 之前咱们曾经介绍过了,他是 netty 中所有 DNS 查问的根底类。

public interface DnsQuery extends DnsMessage 

DnsQuery 的子类有两个,别离是 DatagramDnsQuery 和 DefaultDnsQuery。这两个实现类一个示意 UDP 协定的查问,一个示意 TCP 协定的查问。

咱们看下 UDP 协定的 DatagramDnsQuery 具体定义:

public class DatagramDnsQuery extends DefaultDnsQuery implements AddressedEnvelope<DatagramDnsQuery, InetSocketAddress> 

能够看到 DatagramDnsQuery 不仅仅继承自 DefaultDnsQuery,还实现了 AddressedEnvelope 接口。

AddressedEnvelope 是 netty 中 UDP 包的定义,所以要想在 netty 中发送基于 UDP 协定的数据包,就必须实现 AddressedEnvelope 中定义的办法。

作为一个 UDP 数据包,除了根本的 DNS 查问中所须要的 id 和 opCode 之外,还须要提供两个额定的地址, 别离是 sender 和 recipient:

    private final InetSocketAddress sender;
    private final InetSocketAddress recipient;

所以 DatagramDnsQuery 的构造函数能够接管 4 个参数:

    public DatagramDnsQuery(InetSocketAddress sender, InetSocketAddress recipient, int id, DnsOpCode opCode) {super(id, opCode);
        if (recipient == null && sender == null) {throw new NullPointerException("recipient and sender");
        } else {
            this.sender = sender;
            this.recipient = recipient;
        }
    }

这里 recipient 和 sender 不能同时为空。

在下面的代码中,咱们构建 DatagramDnsQuery 时,传入了服务器的 InetSocketAddress:

final String dnsServer = "223.5.5.5";
        final int dnsPort = 53;
 InetSocketAddress addr = new InetSocketAddress(dnsServer, dnsPort);

并且随机生成了一个 ID。而后调用 setRecord 办法填充查问的数据。

.setRecord(DnsSection.QUESTION,
                    new DefaultDnsQuestion(queryDomain, DnsRecordType.A));

DnsSection 有 4 个,别离是:

    QUESTION,
    ANSWER,
    AUTHORITY,
    ADDITIONAL;

这里是查问操作,所以须要设置 DnsSection.QUESTION。它的值是一个 DnsQuestion:

public class DefaultDnsQuestion extends AbstractDnsRecord implements DnsQuestion 

在这个查问中,咱们传入了要查问的 domain 值:www.flydean.com,还有查问的类型 A:address, 示意的是域名的 IP 地址。

DNS 音讯的解决

在 Do53UdpChannelInitializer 中为 pipline 增加了 netty 提供的 UDP 编码解码器和自定义的音讯处理器:

class Do53UdpChannelInitializer extends ChannelInitializer<DatagramChannel> {
    @Override
    protected void initChannel(DatagramChannel ch) throws Exception {ChannelPipeline p = ch.pipeline();
        p.addLast(new DatagramDnsQueryEncoder())
                .addLast(new DatagramDnsResponseDecoder())
                .addLast(new Do53UdpChannelInboundHandler());
    }
}

DatagramDnsQueryEncoder 负责将 DnsQuery 编码成为 DatagramPacket, 从而能够在 NioDatagramChannel 中进行传输。

public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEnvelope<DnsQuery, InetSocketAddress>> {

DatagramDnsQueryEncoder 继承自 MessageToMessageEncoder,要编码的对象是 AddressedEnvelope,也就是咱们构建的 DatagramDnsQuery。

看一下它外面最外围的 encode 办法:

    protected void encode(ChannelHandlerContext ctx, AddressedEnvelope<DnsQuery, InetSocketAddress> in, List<Object> out) throws Exception {InetSocketAddress recipient = (InetSocketAddress)in.recipient();
        DnsQuery query = (DnsQuery)in.content();
        ByteBuf buf = this.allocateBuffer(ctx, in);
        boolean success = false;
        try {this.encoder.encode(query, buf);
            success = true;
        } finally {if (!success) {buf.release();
            }
        }
        out.add(new DatagramPacket(buf, recipient, (InetSocketAddress)null));
    }

基本思路就是从 AddressedEnvelope 中取出 recipient 和 DnsQuery,而后调用 encoder.encode 办法将 DnsQuery 进行编码,最初将这些数据封装到 DatagramPacket 中。

这里的 encoder 是一个 DnsQueryEncoder 实例,专门用来编码 DnsQuery 对象。

DatagramDnsResponseDecoder 负责将承受到的 DatagramPacket 对象解码成为 DnsResponse 供后续的自定义程序读取应用:

public class DatagramDnsResponseDecoder extends MessageToMessageDecoder<DatagramPacket> 

看一下它的 decode 办法:

    protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List<Object> out) throws Exception {
        try {out.add(this.decodeResponse(ctx, packet));
        } catch (IndexOutOfBoundsException var5) {throw new CorruptedFrameException("Unable to decode response", var5);
        }
    }

下面的 decode 办法实际上调用了 DnsResponseDecoder 的 decode 办法进行解码操作。

最初就是自定义的 Do53UdpChannelInboundHandler 用来进行音讯的读取和解析:

    private static void readMsg(DatagramDnsResponse msg) {if (msg.count(DnsSection.QUESTION) > 0) {DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0);
            log.info("question is :{}", question);
        }
        for (int i = 0, count = msg.count(DnsSection.ANSWER); i < count; i++) {DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
            if (record.type() == DnsRecordType.A) {
                // A 记录用来指定主机名或者域名对应的 IP 地址
                DnsRawRecord raw = (DnsRawRecord) record;
                System.out.println(NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
            }
        }
    }

自定义 handler 承受的是一个 DatagramDnsResponse 对象,解决逻辑也很简略,首先读取 msg 中的 QUESTION,并打印进去。

而后读取 msg 中的 ANSWER 字段,如果 ANSWER 的类型是 A address, 那么就调用 NetUtil.bytesToIpAddress 办法将其转换成为 IP 地址输入。

最初咱们可能失去上面的输入:

question is :DefaultDnsQuestion(www.flydean.com. IN A)
49.112.38.167

总结

以上就是在 netty 中应用 UDP 协定进行 DNS 查问的具体解说。

本文的代码,大家能够参考:

learn-netty4

更多内容请参考 http://www.flydean.com/55-netty-dns-over-udp/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

退出移动版