netty系列之:来,手把手教你使用netty搭建一个DNS tcp服务器

netty,系列,手把手,使用,搭建,一个,dns,tcp,服务器 · 浏览次数 : 580

小编点评

**DNS Server 实现原理:** 1. 创建两个EventLoopGroup,用于处理客户端和服务器之间的通信。 2. 创建ServerBootstrap,配置服务器信息,并指定channel是NioServerSocketChannel。 3. 创建Do53ServerChannelInitializer,包含消息处理handler。 4. 在Do53ServerChannelInitializer中添加TcpDnsQueryDecoder和TcpDnsResponseEncoder编码解码器,用于解析和编码DNS查询和响应消息。 5. 在channelRead0方法中,从DnsQuery中解析question信息,根据question的内容返回响应给客户端。 **DNS客户端消息请求总结:** 1. 使用netty创建DNS客户端,设置域名、端口和客户端地址。 2. 发送DNS查询请求,包含查询域名和查询类型。 3. 客户端接收服务器的响应,包括查询结果和响应消息。 **代码示例:** ```java // DNS Server public class Do53Server implements ChannelInitializer { @Override public void initChannel(SocketChannel socket) throws IOException { // 初始化服务器端线程 serverThread.start(); } private void serverThread() { // 处理客户端连接 // 创建DNS服务器对象 // 创建Do53ServerChannelInitializer // 配置Do53ServerChannelInitializer // 启动Do53ServerChannelInitializer } } // DNS Client public class Do53TcpClient { private String dnsServer; private int dnsPort; private String queryDomain; public Do53TcpClient(String dnsServer, int dnsPort, String queryDomain) { this.dnsServer = dnsServer; this.dnsPort = dnsPort; this.queryDomain = queryDomain; } public void startDnsClient(String dnsServer, int dnsPort, String queryDomain) { // 创建DNS客户端对象 // 设置DNS服务器地址和端口 // 发送DNS查询请求 // 接收服务器响应 } } ```

正文

简介

在前面的文章中,我们提到了使用netty构建tcp和udp的客户端向已经公布的DNS服务器进行域名请求服务。基本的流程是借助于netty本身的NIO通道,将要查询的信息封装成为DNSMessage,通过netty搭建的channel发送到服务器端,然后从服务器端接受返回数据,将其编码为DNSResponse,进行消息的处理。

那么DNS Server是否可以用netty实现呢?

答案当然是肯定的,但是之前也讲过了DNS中有很多DnsRecordType,所以如果想实现全部的支持类型可能并现实,这里我们就以最简单和最常用的A类型为例,用netty来实现一下DNS的TCP服务器。

搭建netty服务器

因为是TCP请求,所以这里使用基于NIO的netty server服务,也就是NioEventLoopGroup和NioServerSocketChannel,netty服务器的代码如下:

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup,
                        workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new Do53ServerChannelInitializer());
        final Channel channel = bootstrap.bind(dnsServerPort).channel();
        channel.closeFuture().sync();

因为是服务器,所以我们需要两个EventLoopGroup,一个是bossGroup,一个是workerGroup。

将这两个group传递给ServerBootstrap,并指定channel是NioServerSocketChannel,然后添加自定义的Do53ServerChannelInitializer即可。

Do53ServerChannelInitializer中包含了netty自带的tcp编码解码器和自定义的服务器端消息处理方式。

这里dnsServerPort=53,也是默认的DNS服务器的端口值。

DNS服务器的消息处理

Do53ServerChannelInitializer是我们自定义的initializer,里面为pipline添加了消息的处理handler:

class Do53ServerChannelInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new TcpDnsQueryDecoder(),
                new TcpDnsResponseEncoder(),
                new Do53ServerInboundHandler());
    }
}

这里我们添加了两个netty自带的编码解码器,分别是TcpDnsQueryDecoder和TcpDnsResponseEncoder。

对于netty服务器来说,接收到的是ByteBuf消息,为了方便服务器端的消息读取,需要将ByteBuf解码为DnsQuery,这也就是TcpDnsQueryDecoder在做的事情。

public final class TcpDnsQueryDecoder extends LengthFieldBasedFrameDecoder 

TcpDnsQueryDecoder继承自LengthFieldBasedFrameDecoder,也就是以字段长度来区分对象的起始位置。这和TCP查询传过来的数据结构是一致的。

下面是它的decode方法:

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf frame = (ByteBuf)super.decode(ctx, in);
        return frame == null ? null : DnsMessageUtil.decodeDnsQuery(this.decoder, frame.slice(), new DnsQueryFactory() {
            public DnsQuery newQuery(int id, DnsOpCode dnsOpCode) {
                return new DefaultDnsQuery(id, dnsOpCode);
            }
        });
    }

decode接受一个ByteBuf对象,首先调用LengthFieldBasedFrameDecoder的decode方法,将真正需要解析的内容解析出来,然后再调用DnsMessageUtil的decodeDnsQuery方法将真正的ByteBuf内容解码成为DnsQuery返回。

这样就可以在自定义的handler中处理DnsQuery消息了。

上面代码中,自定义的handler叫做Do53ServerInboundHandler:

class Do53ServerInboundHandler extends SimpleChannelInboundHandler<DnsQuery> 

从定义看,Do53ServerInboundHandler要处理的消息就是DnsQuery。

看一下它的channelRead0方法:

    protected void channelRead0(ChannelHandlerContext ctx,
                                DnsQuery msg) throws Exception {
        DnsQuestion question = msg.recordAt(DnsSection.QUESTION);
        log.info("Query is: {}", question);
        ctx.writeAndFlush(newResponse(msg, question, 1000, QUERY_RESULT));
    }

我们从DnsQuery的QUESTION section中拿到DnsQuestion,然后解析DnsQuestion的内容,根据DnsQuestion的内容返回一个response给客户端。

这里的respone是我们自定义的:

    private DefaultDnsResponse newResponse(DnsQuery query,
                                           DnsQuestion question,
                                           long ttl, byte[]... addresses) {
        DefaultDnsResponse response = new DefaultDnsResponse(query.id());
        response.addRecord(DnsSection.QUESTION, question);

        for (byte[] address : addresses) {
            DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(
                    question.name(),
                    DnsRecordType.A, ttl, Unpooled.wrappedBuffer(address));
            response.addRecord(DnsSection.ANSWER, queryAnswer);
        }
        return response;
    }

上面的代码封装了一个新的DefaultDnsResponse对象,并使用query的id作为DefaultDnsResponse的id。并将question作为response的QUESEION section。

除了QUESTION section,response中还需要ANSWER section,这个ANSWER section需要填充一个DnsRecord。

这里构造了一个DefaultDnsRawRecord,传入了record的name,type,ttl和具体内容。

最后将构建好的DefaultDnsResponse返回。

因为客户端查询的是A address,按道理我们需要通过QUESTION中传入的domain名字,然后根据DNS服务器中存储的记录进行查找,最终返回对应域名的IP地址。

但是因为我们只是模拟的DNS服务器,所以并没有真实的域名IP记录,所以这里我们伪造了一个ip地址:

    private static final byte[] QUERY_RESULT = new byte[]{46, 53, 107, 110};

然后调用Unpooled的wrappedBuffer方法,将byte数组转换成为ByteBuf,传入DefaultDnsRawRecord的构造函数中。

这样我们的DNS服务器就搭建好了。

DNS客户端消息请求

上面我们搭建好了DNS服务器,接下来就可以使用DNS客户端来请求DNS服务器了。

这里我们使用之前创建好的netty DNS客户端,只不过进行少许改动,将DNS服务器的域名和IP地址替换成下面的值:

        Do53TcpClient client = new Do53TcpClient();
        final String dnsServer = "127.0.0.1";
        final int dnsPort = 53;
        final String queryDomain ="www.flydean.com";
        client.startDnsClient(dnsServer,dnsPort,queryDomain);

dnsServer就填本机的IP地址,dnsPort就是我们刚刚创建的默认端口53。

首先运行DNS服务器:

INFO  i.n.handler.logging.LoggingHandler - [id: 0x021762f2] REGISTERED
INFO  i.n.handler.logging.LoggingHandler - [id: 0x021762f2] BIND: 0.0.0.0/0.0.0.0:53
INFO  i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] ACTIVE

可以看到DNS服务器已经准备好了,绑定的端口是53。

然后运行上面的客户端,在客户端可以得到下面的结果:

INFO  c.f.d.Do53TcpChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO  c.f.d.Do53TcpChannelInboundHandler - ip address is: 46.53.107.110

可以看到DNS查询成功,并且返回了我们在服务器中预设的值。

然后再看一下服务器端的输出:

INFO  i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] READ: [id: 0x44d4c761, L:/127.0.0.1:53 - R:/127.0.0.1:65471]
INFO  i.n.handler.logging.LoggingHandler - [id: 0x021762f2, L:/0:0:0:0:0:0:0:0:53] READ COMPLETE
INFO  c.f.d.Do53ServerInboundHandler - Query is: DefaultDnsQuestion(www.flydean.com. IN A)

可以看到服务器端成功和客户端建立了连接,并成功接收到了客户端的查询请求。

总结

以上就是使用netty默认DNS服务器端的实现原理和例子。因为篇幅有限,这里只是默认了type为A address的情况,对其他type感兴趣的朋友可以自行探索。

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

learn-netty4

与netty系列之:来,手把手教你使用netty搭建一个DNS tcp服务器相似的内容:

netty系列之:来,手把手教你使用netty搭建一个DNS tcp服务器

简介 在前面的文章中,我们提到了使用netty构建tcp和udp的客户端向已经公布的DNS服务器进行域名请求服务。基本的流程是借助于netty本身的NIO通道,将要查询的信息封装成为DNSMessage,通过netty搭建的channel发送到服务器端,然后从服务器端接受返回数据,将其编码为DNSR

netty系列之: 在netty中使用 tls 协议请求 DNS 服务器

简介 在前面的文章中我们讲过了如何在netty中构造客户端分别使用tcp和udp协议向DNS服务器请求消息。在请求的过程中并没有进行消息的加密,所以这种请求是不安全的。 那么有同学会问了,就是请求解析一个域名的IP地址而已,还需要安全通讯吗? 事实上,不加密的DNS查询消息是很危险的,如果你在访问一

netty系列之:在netty中使用proxy protocol

简介 我们知道proxy protocol是haproxy提出的一个代理协议,通过这个协议,所有实现这个协议的proxy或者LBS,都可以附带真实客户端的IP地址和端口号,这使得proxy protocol在实际应用中非常有用。 这么优秀的协议,没有理由netty不支持。本文将会谈一下netty中对

jdk17下netty导致堆内存疯涨原因排查

# 背景: ### 介绍 天网风控**灵玑**系统是基于内存计算实现的高吞吐低延迟在线计算服务,提供滑动或滚动窗口内的count、distinctCout、max、min、avg、sum、std及区间分布类的在线统计计算服务。客户端和服务端底层通过netty直接进行tcp通信,且服务端也是基于net

美团面试:说说Netty的零拷贝技术?

零拷贝技术(Zero-Copy)是一个大家耳熟能详的技术名词了,它主要用于提升 IO(Input & Output)的传输性能。 那么问题来了,为什么零拷贝技术能提升 IO 性能? 1.零拷贝技术和性能 在传统的 IO 操作中,当我们需要读取并传输数据时,我们需要在用户态(用户空间)和内核态(内核空

面试官:说说Netty对象池的实现原理?

Netty 作为一个高性能的网络通讯框架,它内置了很多恰夺天工的设计,目的都是为了将网络通讯的性能做到极致,其中「对象池技术」也是实现这一目标的重要技术。 1.什么是对象池技术? 对象池技术是一种重用对象以减少对象创建和销毁带来的开销的方法。在对象池中,只有第一次访问时会创建对象,并将其维护在内存中

拼多多面试:Netty如何解决粘包问题?

粘包和拆包问题也叫做粘包和半包问题,它是指在数据传输时,接收方未能正常读取到一条完整数据的情况(只读取了部分数据,或多读取到了另一条数据的情况)就叫做粘包或拆包问题。 从严格意义上来说,粘包问题和拆包问题属于两个不同的问题,接下来我们分别来看。 1.粘包问题 粘包问题是指在网络通信中,发送方连续发送

滴滴面试:谈谈你对Netty线程模型的理解?

Netty 线程模型是指 Netty 框架为了提供高性能、高并发的网络通信,而设计的管理和利用线程的策略和机制。 Netty 线程模型被称为 Reactor(响应式)模型/模式,它是基于 NIO 多路复用模型的一种升级,它的核心思想是将 IO 事件和业务处理进行分离,使用一个或多个线程来执行任务的一

面试官:说说Netty的核心组件?

Netty 核心组件是指 Netty 在执行过程中所涉及到的重要概念,这些核心组件共同组成了 Netty 框架,使 Netty 框架能够正常的运行。 Netty 核心组件包含以下内容: 启动器 Bootstrap/ServerBootstrap 事件循环器 EventLoopGroup/EventL

Springboot集成Netty实现TCP通讯

Netty