简介
之前我们讲到了如何在 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/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
评论