<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
DNS的全稱domain name system,既然是一個系統就有使用者端和伺服器之分。一般情況來說我們並不需要感知這個DNS使用者端的存在,因為我們在瀏覽器存取某個域名的時候,瀏覽器作為使用者端已經實現了這個工作。
但是有時候我們沒有使用瀏覽器,比如在netty環境中,如何構建一個DNS請求呢?
在RFC的規範中,DNS傳輸協定有很多種,如下所示:
這些協定都有對應的實現方式,我們先來看下Do53/TCP,也就是使用TCP進行DNS協定傳輸。
先來考慮一下如何在netty中使用Do53/TCP協定,進行DNS查詢。
因為DNS是使用者端和伺服器的模式,我們需要做的是構建一個DNS使用者端,向已知的DNS伺服器端進行查詢。
已知的DNS伺服器地址有哪些呢?
除了13個root DNS IP地址以外,還出現了很多免費的公共DNS伺服器地址,比如我們常用的阿里DNS,同時提供了IPv4/IPv6 DNS和DoT/DoH服務。
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
當然還有很多其他的公共免費DNS,這裡我選擇使用阿里的IPv4:223.5.5.5為例。
有了IP地址,我們還需要指定netty的連線埠號,這裡預設的是53。
然後就是我們要查詢的域名了,這裡以www.flydean.com為例。
你也可以使用你係統中設定的DNS解析地址,以mac為例,可以通過nslookup進行檢視原生的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
有了DNS Server的IP地址,接下來我們需要做的就是搭建netty client,然後向DNS server端傳送DNS查詢訊息。
因為我們進行的是TCP連線,所以可以藉助於netty中的NIO操作來實現,也就是說我們需要使用NioEventLoopGroup和NioSocketChannel來搭建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協定,所以我們只需要像常用的netty使用者端服務一樣構建使用者端即可。
然後呼叫Bootstrap的connect方法連線到DNS伺服器,就建立好了channel連線。
這裡我們在handler中傳入了自定義的Do53ChannelInitializer,我們知道handler的作用是對訊息進行編碼、解碼和對訊息進行讀取。因為目前我們並不知道使用者端查詢的訊息格式,所以Do53ChannelInitializer的實現我們在後面再進行詳細講解。
netty提供了DNS訊息的封裝,所有的DNS訊息,包括查詢和響應都是DnsMessage的子類。
每個DnsMessage都有一個唯一標記的ID,還有代表這個message型別的DnsOpCode。
對於DNS來說,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");
因為每個DnsMessage都可能包含4個sections,每個section都以DnsSection來表示。因為有4個section,所以在DnsSection定義了4個section型別:
QUESTION, ANSWER, AUTHORITY, ADDITIONAL;
每個section裡面又包含了多個DnsRecord, DnsRecord代表的就是Resource record,簡稱為RR,RR中有一個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訊息的統一表示,對於查詢來說,netty中提供了一個專門的查詢類叫做DefaultDnsQuery。
先來看下DefaultDnsQuery的定義和建構函式:
public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery { public DefaultDnsQuery(int id) { super(id); } public DefaultDnsQuery(int id, DnsOpCode opCode) { super(id, opCode); }
DefaultDnsQuery的建構函式需要傳入id和opCode。
我們可以這樣定義一個DNS查詢:
int randomID = (int) (System.currentTimeMillis() / 1000); DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
既然是QEURY,那麼還需要設定4個sections中的查詢section:
query.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
這裡呼叫的是setRecord方法向section中插入RR資料。
這裡的RR資料使用的是DefaultDnsQuestion。DefaultDnsQuestion的建構函式有兩個,一個是要查詢的domain name,這裡就是"www.flydean.com",另外一個引數是dns記錄的型別。
dns記錄的型別有很多種,在netty中有一個專門的類DnsRecordType表示,DnsRecordType中定義了很多個型別,如下所示:
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"); ...
因為型別比較多,我們挑選幾個常用的進行講解。
以上幾個是我們經常會用到的dns record型別。
這裡我們選擇使用A,用來查詢域名對應的主機IP地址。
構建好query之後,我們就可以使用netty client傳送query指令到dns伺服器了,具體的程式碼如下:
DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A)); ch.writeAndFlush(query).sync();
DNS的查詢訊息我們已經傳送出去了,接下來就是對訊息的處理和解析了。
還記得我們自定義的Do53ChannelInitializer嗎?看一下它的實現:
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中新增了兩個netty自帶的編碼解碼器TcpDnsQueryEncoder和TcpDnsResponseDecoder,還有一個自定義用來做訊息解析的Do53ChannelInboundHandler。
因為我們向channel中寫入的是DnsQuery,所以需要一個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編碼之前儲存了msg的長度資訊,所以是一個基於長度的物件編碼器。
這裡的encoder是一個DnsQueryEncoder物件。
看一下它的encoder方法:
void encode(DnsQuery query, ByteBuf out) throws Exception { encodeHeader(query, out); this.encodeQuestions(query, out); this.encodeRecords(query, DnsSection.ADDITIONAL, out); }
DnsQueryEncoder會依次編碼header、questions和records。
完成編碼之後,我們還需要從DNS server的返回中decode出DnsResponse,這裡使用的是netty自帶的TcpDnsResponseDecoder:
public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder
TcpDnsResponseDecoder繼承自LengthFieldBasedFrameDecoder,表示資料是以欄位長度來進行分割的,這和我們剛剛將的encoder的格式類似。
來看下他的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方法先呼叫LengthFieldBasedFrameDecoder的decode方法將要解碼的內容提取出來,然後呼叫responseDecoder的decode方法,最終返回DnsResponse。
這裡的responseDecoder是一個DnsResponseDecoder。具體decoder的細節這裡就不過多闡述了。感興趣的同學可以自行查閱程式碼檔案。
最後,我們得到了DnsResponse物件。
接下來就是自定義的InboundHandler對訊息進行解析了:
class Do53ChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse>
在它的channelRead0方法中,我們呼叫了readMsg方法對訊息進行處理:
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記錄用來指定主機名或者域名對應的IP地址 if (record.type() == DnsRecordType.A) { DnsRawRecord raw = (DnsRawRecord) record; log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content()))); } i++; } }
DefaultDnsResponse是DnsResponse的一個實現,首先判斷msg中的QUESTION個數是否大於零。
如果大於零,則列印出question的資訊。
然後再解析出msg中的ANSWER並列印出來。
最後,我們可能得到這樣的輸出:
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
以上就是使用netty建立DNS client進行TCP查詢的講解。
本文的程式碼,大家可以參考:
到此這篇關於手把手教你在netty中使用TCP協定請求DNS伺服器的文章就介紹到這了,更多相關netty請求DNS伺服器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45