简介

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

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

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