写点什么

netty 系列之: 在 netty 中使用 UDP 协议请求 DNS 服务器

作者:程序那些事
  • 2022 年 5 月 26 日
  • 本文字数:3765 字

    阅读完需:约 12 分钟

netty系列之:在netty中使用UDP协议请求DNS服务器

简介

之前我们讲到了如何在 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/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

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

发布于: 2022 年 05 月 26 日阅读数: 29
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:在netty中使用UDP协议请求DNS服务器_Java_程序那些事_InfoQ写作社区