亲自教你在netty中使用TCP协议请求DNS服务器的详细过程

目录
  • 简介
  • DNS传输协议简介
  • DNS的IP地址
  • Do53/TCP在netty中的使用
    • 搭建DNS netty client
    • 发送DNS查询消息
    • DNS查询的消息处理
  • 总结

简介

DNS的全称domain name system,既然是一个系统就有客户端和服务器之分。一般情况来说我们并不需要感知这个DNS客户端的存在,因为我们在浏览器访问某个域名的时候,浏览器作为客户端已经实现了这个工作。

但是有时候我们没有使用浏览器,比如在netty环境中,如何构建一个DNS请求呢?

DNS传输协议简介

在RFC的规范中,DNS传输协议有很多种,如下所示:

  • DNS-over-UDP/53简称"Do53",是使用UDP进行DNS查询传输的协议。
  • DNS-over-TCP/53简称"Do53/TCP",是使用TCP进行DNS查询传输的协议。
  • DNSCrypt,对DNS传输协议进行加密的方法。
  • DNS-over-TLS简称"DoT",使用TLS进行DNS协议传输。
  • DNS-over-HTTPS简称"DoH",使用HTTPS进行DNS协议传输。
  • DNS-over-TOR,使用VPN或者tunnels连接DNS。

这些协议都有对应的实现方式,我们先来看下Do53/TCP,也就是使用TCP进行DNS协议传输。

DNS的IP地址

先来考虑一下如何在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

Do53/TCP在netty中的使用

有了DNS Server的IP地址,接下来我们需要做的就是搭建netty client,然后向DNS server端发送DNS查询消息。

搭建DNS netty client

因为我们进行的是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的实现我们在后面再进行详细讲解。

发送DNS查询消息

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");
    ...

因为类型比较多,我们挑选几个常用的进行讲解。

  • A类型,是address的缩写,用来指定主机名或者域名对应的ip地址.
  • NS类型,是name server的缩写,是域名服务器记录,用来指定域名由哪个DNS服务器来进行解析。
  • MX类型,是mail exchanger的缩写,是一个邮件交换记录,用来根据邮箱的后缀来定位邮件服务器。
  • CNAME类型,是canonical name的缩写,可以将多个名字映射到同一个主机.
  • 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查询的消息处理

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查询的讲解。

本文的代码,大家可以参考:

learn-netty4

到此这篇关于手把手教你在netty中使用TCP协议请求DNS服务器的文章就介绍到这了,更多相关netty请求DNS服务器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • spring+netty服务器搭建的方法

    游戏一般是长连接,自定义协议,不用http协议,BIO,NIO,AIO这些我就不说了,自己查资料 我现在用spring+netty搭起简单的游戏服 思路:1自定义协议和协议包:2spring+netty整合:3半包粘包处理,心跳机制等:4请求分发(目前自己搞的都是单例模式) 下个是测试用的,结构如下 首先自定义包头 Header.java package com.test.netty.message; /** * Header.java * 自定义协议包头 * @author janehuang

  • Netty框架实现TCP/IP通信的完美过程

    项目中需要使用到TCP/IP协议完成数据的发送与接收.如果只是用以前写的简单的socket套接字方法,每次接收发送消息都会创建新的socket再关闭socket,造成资源浪费.于是使用netty框架完成java网络通信.       Netty框架的内容很多,这里只是代码展示其中的一个功能. 代码仓库 这里使用的是Springboot+Netty框架,使用maven搭建项目.这里是在一个项目中搭建服务端与客户端,所以端口一样.还可以使用TCP/UTP工具自己搭建服务端和客户端,只要在yml文件中

  • 使用Netty解决TCP粘包和拆包问题过程详解

    前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络转播的,当tcp三次握手简历通信后,客户端服务端之间就建立了一种通讯管道,我们可以想象成自来水管道,流出来的水是连城一片的,是没有分界线的. TCP底层并不了解上层的业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分. 所以对于我们应用层而言.我们直观是发送一个个连续完整TCP数据包的,

  • 如何用Netty实现高效的HTTP服务器

    1 概述 HTTP 是基于请求/响应模式的:客户端向服务器发送一个 HTTP 请求,然后服务器将会返回一个 HTTP 响应.Netty 提供了多种编码器和解码器以简化对这个协议的使用.一个HTTP 请求/响应可能由多个数据部分组成,FullHttpRequest 和FullHttpResponse 消息是特殊的子类型,分别代表了完整的请求和响应.所有类型的 HTTP 消息(FullHttpRequest.LastHttpContent 等等)都实现了 HttpObject 接口. (1) Htt

  • Netty解决 TCP 粘包拆包的方法

    什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在.处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要. 我们都知道TCP是基于字节流的传输协议. 那么数据在通信层传播其实就像河水一样并没有明显的分界线,而数据具体表示什么意思什么地方有句

  • 亲自教你在netty中使用TCP协议请求DNS服务器的详细过程

    目录 简介 DNS传输协议简介 DNS的IP地址 Do53/TCP在netty中的使用 搭建DNS netty client 发送DNS查询消息 DNS查询的消息处理 总结 简介 DNS的全称domain name system,既然是一个系统就有客户端和服务器之分.一般情况来说我们并不需要感知这个DNS客户端的存在,因为我们在浏览器访问某个域名的时候,浏览器作为客户端已经实现了这个工作. 但是有时候我们没有使用浏览器,比如在netty环境中,如何构建一个DNS请求呢? DNS传输协议简介 在R

  • 对python中基于tcp协议的通信(数据传输)实例讲解

    阅读目录 tcp协议:流式协议(以数据流的形式通信传输).安全协议(收发信息都需收到确认信息才能完成收发,是一种双向通道的通信) tcp协议在OSI七层协议中属于传输层,它上承用户层的数据收发,下启网络层.数据链路层.物理层.可以说很多安全数据的传输通信都是基于tcp协议进行的. 为了让tcp通信更加方便需要引入一个socket模块(将网络层.数据链路层.物理层封装的模块),我们只要调用模块中的相关接口就能实现传输层下面的繁琐操作. 简单的tcp协议通信模板:(需要一个服务端和一个客户端) 服务

  • python3实现TCP协议的简单服务器和客户端案例(分享)

    利用python3来实现TCP协议,和UDP类似.UDP应用于及时通信,而TCP协议用来传送文件.命令等操作,因为这些数据不允许丢失,否则会造成文件错误或命令混乱.下面代码就是模拟客户端通过命令行操作服务器.客户端输入命令,服务器执行并且返回结果. TCP(Transmission Control Protocol 传输控制协议):是一种面向连接的.可靠的.基于字节流的传输层通信协议,由IETF的RFC 793定义. TCP客户端 from socket import * host = '192

  • 教你创建springcloud微服务的基础子服务的超详细过程

    目录 一.创建父项目 1. 选择Spring Initializr 2. Type选择为Maven POM,Java Version选择为8 3. 勾选一些基本的依赖,如lombok和spring-boot-starter-web 4. 创建好了的父项目结构如下: 二.创建二级项目 1. 选择新建Module 2. 选择Maven 3. 填写Name 4. 修改openfeign pom.xml文件,并删除掉该项目下的src文件 5. 创建完成的项目结构如下 三.创建子项目feign-provi

  • 在netty中使用native传输协议的方法

    目录 简介 native传输协议的依赖 netty本地传输协议的使用 总结 简介 对于IO来说,除了传统的block IO,使用最多的就是NIO了,通常我们在netty程序中最常用到的就是NIO,比如NioEventLoopGroup,NioServerSocketChannel等. 我们也知道在IO中有比NIO更快的IO方式,比如kqueue和epoll,但是这两种方式需要native方法的支持,也就是说需要在操作系统层面提供服务. 如果我们在支持Kqueue或者epoll的服务器上,nett

  • Android 通过TCP协议上传指定目录文件的方法

    为了方便客户抓取Log,现通过TCP协议连接指定服务器,传输指定内容,定义指定目录,IP,PORT字段接收参数.直接上代码 public static void uploadLog(final String dirPath, final String IP, final int port ) { JSONArray fileList = new JSONArray(); final JSONArray allFiles = getAllFiles(fileList,dirPath); if(al

  • Golang中基于HTTP协议的网络服务

    目录 一.HTTP协议的网络服务 1.1 使用http.Get函数访问HTTP协议的网络服务 1.2 使用缺省客户端DefaultClient(类型为*http.Client ) 1.3 使用http.Client访问HTTP协议的网络服务 二.http.Client中的Transport字段 (1)http.Transport类型中的DialContext字段 (2)http.Transport类型中的其它字段 三.为什么会出现空闲的连接 3.1 空闲连接的产生 3.2 杜绝空闲连接的产生 四

  • 基于python中的TCP及UDP(详解)

    python中是通过套接字即socket来实现UDP及TCP通信的.有两种套接字面向连接的及无连接的,也就是TCP套接字及UDP套接字. TCP通信模型 创建TCP服务器 伪代码: ss = socket() # 创建服务器套接字 ss.bind() # 套接字与地址绑定 ss.listen() # 监听连接 inf_loop: # 服务器无限循环 cs = ss.accept() # 接受客户端连接 comm_loop: # 通信循环 cs.recv()/cs.send() # 对话(接收/发

  • .Net WInform开发笔记(二)Winform程序运行结构图及TCP协议在Winform中的应用

    中午没事,把去年刚毕业那会画的几张图翻出来了,大概介绍Winform应用程序运行的过程,以及TCP协议在Winform中的应用.如果有Windows消息机制等基础,很好理解这两张图. (1)Winform应用程序运行结构图 (2)TCP通讯协议在Winform程序中的应用示意图 熟悉整个程序的来龙去脉,编程的时候就会很轻松,不会云里雾里. 另附公司招聘面试题一份,用了几次,发现效果不好,不知啥原因 1.简述接口.抽象类的区别. 2.简述重载(overload)与重写(override)的区别.

随机推荐