親自教你在netty中使用TCP協(xié)議請(qǐng)求DNS服務(wù)器的詳細(xì)過(guò)程
簡(jiǎn)介
DNS的全稱domain name system,既然是一個(gè)系統(tǒng)就有客戶端和服務(wù)器之分。一般情況來(lái)說(shuō)我們并不需要感知這個(gè)DNS客戶端的存在,因?yàn)槲覀冊(cè)跒g覽器訪問(wèn)某個(gè)域名的時(shí)候,瀏覽器作為客戶端已經(jīng)實(shí)現(xiàn)了這個(gè)工作。
但是有時(shí)候我們沒(méi)有使用瀏覽器,比如在netty環(huán)境中,如何構(gòu)建一個(gè)DNS請(qǐng)求呢?
DNS傳輸協(xié)議簡(jiǎn)介
在RFC的規(guī)范中,DNS傳輸協(xié)議有很多種,如下所示:
- DNS-over-UDP/53簡(jiǎn)稱"Do53",是使用UDP進(jìn)行DNS查詢傳輸?shù)膮f(xié)議。
- DNS-over-TCP/53簡(jiǎn)稱"Do53/TCP",是使用TCP進(jìn)行DNS查詢傳輸?shù)膮f(xié)議。
- DNSCrypt,對(duì)DNS傳輸協(xié)議進(jìn)行加密的方法。
- DNS-over-TLS簡(jiǎn)稱"DoT",使用TLS進(jìn)行DNS協(xié)議傳輸。
- DNS-over-HTTPS簡(jiǎn)稱"DoH",使用HTTPS進(jìn)行DNS協(xié)議傳輸。
- DNS-over-TOR,使用VPN或者tunnels連接DNS。
這些協(xié)議都有對(duì)應(yīng)的實(shí)現(xiàn)方式,我們先來(lái)看下Do53/TCP,也就是使用TCP進(jìn)行DNS協(xié)議傳輸。
DNS的IP地址
先來(lái)考慮一下如何在netty中使用Do53/TCP協(xié)議,進(jìn)行DNS查詢。
因?yàn)镈NS是客戶端和服務(wù)器的模式,我們需要做的是構(gòu)建一個(gè)DNS客戶端,向已知的DNS服務(wù)器端進(jìn)行查詢。
已知的DNS服務(wù)器地址有哪些呢?
除了13個(gè)root DNS IP地址以外,還出現(xiàn)了很多免費(fèi)的公共DNS服務(wù)器地址,比如我們常用的阿里DNS,同時(shí)提供了IPv4/IPv6 DNS和DoT/DoH服務(wù)。
IPv4: 223.5.5.5 223.6.6.6 IPv6: 2400:3200::1 2400:3200:baba::1 DoH 地址: https://dns.alidns.com/dns-query DoT 地址: dns.alidns.com
再比如百度DNS,提供了一組IPv4和IPv6的地址:
IPv4: 180.76.76.76 IPv6: 2400:da00::6666
還有114DNS:
114.114.114.114 114.114.115.115
當(dāng)然還有很多其他的公共免費(fèi)DNS,這里我選擇使用阿里的IPv4:223.5.5.5為例。
有了IP地址,我們還需要指定netty的連接端口號(hào),這里默認(rèn)的是53。
然后就是我們要查詢的域名了,這里以www.flydean.com為例。
你也可以使用你系統(tǒng)中配置的DNS解析地址,以mac為例,可以通過(guò)nslookup進(jìn)行查看本地的DNS地址:
nslookup www.flydean.com Server: 8.8.8.8 Address: 8.8.8.8#53 Non-authoritative answer: www.flydean.com canonical name = flydean.com. Name: flydean.com Address: 47.107.98.187
Do53/TCP在netty中的使用
有了DNS Server的IP地址,接下來(lái)我們需要做的就是搭建netty client,然后向DNS server端發(fā)送DNS查詢消息。
搭建DNS netty client
因?yàn)槲覀冞M(jìn)行的是TCP連接,所以可以借助于netty中的NIO操作來(lái)實(shí)現(xiàn),也就是說(shuō)我們需要使用NioEventLoopGroup和NioSocketChannel來(lái)搭建netty客戶端:
final String dnsServer = "223.5.5.5";
final int dnsPort = 53;
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new Do53ChannelInitializer());
final Channel ch = b.connect(dnsServer, dnsPort).sync().channel();netty中的NIO Socket底層使用的就是TCP協(xié)議,所以我們只需要像常用的netty客戶端服務(wù)一樣構(gòu)建客戶端即可。
然后調(diào)用Bootstrap的connect方法連接到DNS服務(wù)器,就建立好了channel連接。
這里我們?cè)趆andler中傳入了自定義的Do53ChannelInitializer,我們知道handler的作用是對(duì)消息進(jìn)行編碼、解碼和對(duì)消息進(jìn)行讀取。因?yàn)槟壳拔覀儾⒉恢揽蛻舳瞬樵兊南⒏袷?,所以Do53ChannelInitializer的實(shí)現(xiàn)我們?cè)诤竺嬖龠M(jìn)行詳細(xì)講解。
發(fā)送DNS查詢消息
netty提供了DNS消息的封裝,所有的DNS消息,包括查詢和響應(yīng)都是DnsMessage的子類(lèi)。
每個(gè)DnsMessage都有一個(gè)唯一標(biāo)記的ID,還有代表這個(gè)message類(lèi)型的DnsOpCode。
對(duì)于DNS來(lái)說(shuō),opCode有下面這幾種:
public static final DnsOpCode QUERY = new DnsOpCode(0, "QUERY");
public static final DnsOpCode IQUERY = new DnsOpCode(1, "IQUERY");
public static final DnsOpCode STATUS = new DnsOpCode(2, "STATUS");
public static final DnsOpCode NOTIFY = new DnsOpCode(4, "NOTIFY");
public static final DnsOpCode UPDATE = new DnsOpCode(5, "UPDATE");因?yàn)槊總€(gè)DnsMessage都可能包含4個(gè)sections,每個(gè)section都以DnsSection來(lái)表示。因?yàn)橛?個(gè)section,所以在DnsSection定義了4個(gè)section類(lèi)型:
QUESTION,
ANSWER,
AUTHORITY,
ADDITIONAL;每個(gè)section里面又包含了多個(gè)DnsRecord, DnsRecord代表的就是Resource record,簡(jiǎn)稱為RR,RR中有一個(gè)CLASS字段,下面是DnsRecord中CLASS字段的定義:
int CLASS_IN = 1;
int CLASS_CSNET = 2;
int CLASS_CHAOS = 3;
int CLASS_HESIOD = 4;
int CLASS_NONE = 254;
int CLASS_ANY = 255;DnsMessage是DNS消息的統(tǒng)一表示,對(duì)于查詢來(lái)說(shuō),netty中提供了一個(gè)專門(mén)的查詢類(lèi)叫做DefaultDnsQuery。
先來(lái)看下DefaultDnsQuery的定義和構(gòu)造函數(shù):
public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery {
public DefaultDnsQuery(int id) {
super(id);
}
public DefaultDnsQuery(int id, DnsOpCode opCode) {
super(id, opCode);
}DefaultDnsQuery的構(gòu)造函數(shù)需要傳入id和opCode。
我們可以這樣定義一個(gè)DNS查詢:
int randomID = (int) (System.currentTimeMillis() / 1000);
DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)既然是QEURY,那么還需要設(shè)置4個(gè)sections中的查詢section:
query.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
這里調(diào)用的是setRecord方法向section中插入RR數(shù)據(jù)。
這里的RR數(shù)據(jù)使用的是DefaultDnsQuestion。DefaultDnsQuestion的構(gòu)造函數(shù)有兩個(gè),一個(gè)是要查詢的domain name,這里就是"www.flydean.com",另外一個(gè)參數(shù)是dns記錄的類(lèi)型。
dns記錄的類(lèi)型有很多種,在netty中有一個(gè)專門(mén)的類(lèi)DnsRecordType表示,DnsRecordType中定義了很多個(gè)類(lèi)型,如下所示:
public class DnsRecordType implements Comparable<DnsRecordType> {
public static final DnsRecordType A = new DnsRecordType(1, "A");
public static final DnsRecordType NS = new DnsRecordType(2, "NS");
public static final DnsRecordType CNAME = new DnsRecordType(5, "CNAME");
public static final DnsRecordType SOA = new DnsRecordType(6, "SOA");
public static final DnsRecordType PTR = new DnsRecordType(12, "PTR");
public static final DnsRecordType MX = new DnsRecordType(15, "MX");
public static final DnsRecordType TXT = new DnsRecordType(16, "TXT");
...因?yàn)轭?lèi)型比較多,我們挑選幾個(gè)常用的進(jìn)行講解。
- A類(lèi)型,是address的縮寫(xiě),用來(lái)指定主機(jī)名或者域名對(duì)應(yīng)的ip地址.
- NS類(lèi)型,是name server的縮寫(xiě),是域名服務(wù)器記錄,用來(lái)指定域名由哪個(gè)DNS服務(wù)器來(lái)進(jìn)行解析。
- MX類(lèi)型,是mail exchanger的縮寫(xiě),是一個(gè)郵件交換記錄,用來(lái)根據(jù)郵箱的后綴來(lái)定位郵件服務(wù)器。
- CNAME類(lèi)型,是canonical name的縮寫(xiě),可以將多個(gè)名字映射到同一個(gè)主機(jī).
- TXT類(lèi)型,用來(lái)表示主機(jī)或者域名的說(shuō)明信息。
以上幾個(gè)是我們經(jīng)常會(huì)用到的dns record類(lèi)型。
這里我們選擇使用A,用來(lái)查詢域名對(duì)應(yīng)的主機(jī)IP地址。
構(gòu)建好query之后,我們就可以使用netty client發(fā)送query指令到dns服務(wù)器了,具體的代碼如下:
DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
ch.writeAndFlush(query).sync();DNS查詢的消息處理
DNS的查詢消息我們已經(jīng)發(fā)送出去了,接下來(lái)就是對(duì)消息的處理和解析了。
還記得我們自定義的Do53ChannelInitializer嗎?看一下它的實(shí)現(xiàn):
class Do53ChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new TcpDnsQueryEncoder())
.addLast(new TcpDnsResponseDecoder())
.addLast(new Do53ChannelInboundHandler());
}
}我們向pipline中添加了兩個(gè)netty自帶的編碼解碼器TcpDnsQueryEncoder和TcpDnsResponseDecoder,還有一個(gè)自定義用來(lái)做消息解析的Do53ChannelInboundHandler。
因?yàn)槲覀兿騝hannel中寫(xiě)入的是DnsQuery,所以需要一個(gè)encoder將DnsQuery編碼為ByteBuf,這里使用的是netty提供的TcpDnsQueryEncoder:
public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery>
TcpDnsQueryEncoder繼承自MessageToByteEncoder,表示將DnsQuery編碼為ByteBuf。
看下他的encode方法:
protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception {
out.writerIndex(out.writerIndex() + 2);
this.encoder.encode(msg, out);
out.setShort(0, out.readableBytes() - 2);
}可以看到TcpDnsQueryEncoder在msg編碼之前存儲(chǔ)了msg的長(zhǎng)度信息,所以是一個(gè)基于長(zhǎng)度的對(duì)象編碼器。
這里的encoder是一個(gè)DnsQueryEncoder對(duì)象。
看一下它的encoder方法:
void encode(DnsQuery query, ByteBuf out) throws Exception {
encodeHeader(query, out);
this.encodeQuestions(query, out);
this.encodeRecords(query, DnsSection.ADDITIONAL, out);
}DnsQueryEncoder會(huì)依次編碼header、questions和records。
完成編碼之后,我們還需要從DNS server的返回中decode出DnsResponse,這里使用的是netty自帶的TcpDnsResponseDecoder:
public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder
TcpDnsResponseDecoder繼承自LengthFieldBasedFrameDecoder,表示數(shù)據(jù)是以字段長(zhǎng)度來(lái)進(jìn)行分割的,這和我們剛剛將的encoder的格式類(lèi)似。
來(lái)看下他的decode方法:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf)super.decode(ctx, in);
if (frame == null) {
return null;
} else {
DnsResponse var4;
try {
var4 = this.responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice());
} finally {
frame.release();
}
return var4;
}
}decode方法先調(diào)用LengthFieldBasedFrameDecoder的decode方法將要解碼的內(nèi)容提取出來(lái),然后調(diào)用responseDecoder的decode方法,最終返回DnsResponse。
這里的responseDecoder是一個(gè)DnsResponseDecoder。具體decoder的細(xì)節(jié)這里就不過(guò)多闡述了。感興趣的同學(xué)可以自行查閱代碼文檔。
最后,我們得到了DnsResponse對(duì)象。
接下來(lái)就是自定義的InboundHandler對(duì)消息進(jìn)行解析了:
class Do53ChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse>
在它的channelRead0方法中,我們調(diào)用了readMsg方法對(duì)消息進(jìn)行處理:
private static void readMsg(DefaultDnsResponse msg) {
if (msg.count(DnsSection.QUESTION) > 0) {
DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0);
log.info("question is :{}",question);
}
int i = 0, count = msg.count(DnsSection.ANSWER);
while (i < count) {
DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
//A記錄用來(lái)指定主機(jī)名或者域名對(duì)應(yīng)的IP地址
if (record.type() == DnsRecordType.A) {
DnsRawRecord raw = (DnsRawRecord) record;
log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
}
i++;
}
}DefaultDnsResponse是DnsResponse的一個(gè)實(shí)現(xiàn),首先判斷msg中的QUESTION個(gè)數(shù)是否大于零。
如果大于零,則打印出question的信息。
然后再解析出msg中的ANSWER并打印出來(lái)。
最后,我們可能得到這樣的輸出:
INFO c.f.dnstcp.Do53ChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO c.f.dnstcp.Do53ChannelInboundHandler - ip address is: 47.107.98.187
總結(jié)
以上就是使用netty創(chuàng)建DNS client進(jìn)行TCP查詢的講解。
本文的代碼,大家可以參考:
到此這篇關(guān)于手把手教你在netty中使用TCP協(xié)議請(qǐng)求DNS服務(wù)器的文章就介紹到這了,更多相關(guān)netty請(qǐng)求DNS服務(wù)器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
github的使用_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了github使用的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
dell r710 服務(wù)器配置RAID5(3塊硬盤(pán)做RAID5)
這篇文章主要介紹了dell r710 服務(wù)器配置RAID5圖文教程,需要的朋友可以參考下2014-08-08
在Windows下利用Squid開(kāi)設(shè)代理服務(wù)器
利用Squid在Windows下開(kāi)設(shè)代理服務(wù)器2010-07-07
基于HTTP協(xié)議實(shí)現(xiàn)的小型web服務(wù)器的方法
這篇文章主要介紹了基于HTTP協(xié)議實(shí)現(xiàn)的小型web服務(wù)器的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2007-08-08
SparkGraphx計(jì)算指定節(jié)點(diǎn)的N度關(guān)系節(jié)點(diǎn)源碼
這篇文章主要介紹了SparkGraphx計(jì)算指定節(jié)點(diǎn)的N度關(guān)系節(jié)點(diǎn)源碼,小編覺(jué)得挺不錯(cuò)的,這里分享給大家,希望給各位一個(gè)參考。2017-10-10
如何設(shè)置5臺(tái)SSH互免的虛擬機(jī)服務(wù)器配置
搭建一套集群虛擬機(jī),往往都需要互免設(shè)置,過(guò)程很簡(jiǎn)單,避免以后再搭建還得網(wǎng)上搜索,我直接將這一個(gè)步驟寫(xiě)成筆記,對(duì)SSH虛擬機(jī)服務(wù)器配置感興趣的朋友跟隨小編一起看看吧2024-01-01
DELL服務(wù)器 Dell PowerEdge服務(wù)器RAID卡驅(qū)動(dòng)大全
這篇文章主要介紹了DELL服務(wù)器 Dell PowerEdge服務(wù)器RAID卡驅(qū)動(dòng),因?yàn)楣俜降暮芏嘞到y(tǒng)都不帶陣列卡驅(qū)動(dòng),需要安裝驅(qū)動(dòng)才可以,一般情況下2003用集成raid驅(qū)動(dòng)的系統(tǒng)才可以安裝或者按F6加載驅(qū)動(dòng)2016-04-04
ubuntu 服務(wù)器中文亂碼問(wèn)題的解決方法

