之前我們講到了如何在netty中構建client向DNS伺服器進行域名解析請求。使用的是最常見的TCP協定,也叫做Do53/TCP。
事實上除了TCP協定之外,DNS伺服器還接收UDP協定。這個協定叫做DNS-over-UDP/53,簡稱("Do53")。
本文將會一步一步帶領大家在netty中搭建使用UDP的DNS使用者端。
因為這裡使用的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查詢訊息了。
先看具體的查詢程式碼:
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地址。
在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查詢的詳細講解。
本文的程式碼,大家可以參考:
更多內容請參考 http://www.flydean.com/55-netty-dns-over-udp/
最通俗的解讀,最深刻的乾貨,最簡潔的教學,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!