netty系列之:來(lái),手把手教你使用netty搭建一個(gè)DNS tcp服務(wù)器

簡(jiǎn)介

在前面的文章中刹悴,我們提到了使用netty構(gòu)建tcp和udp的客戶端向已經(jīng)公布的DNS服務(wù)器進(jìn)行域名請(qǐng)求服務(wù)咐鹤≌城眩基本的流程是借助于netty本身的NIO通道,將要查詢的信息封裝成為DNSMessage,通過(guò)netty搭建的channel發(fā)送到服務(wù)器端贷帮,然后從服務(wù)器端接受返回?cái)?shù)據(jù)戚揭,將其編碼為DNSResponse,進(jìn)行消息的處理撵枢。

那么DNS Server是否可以用netty實(shí)現(xiàn)呢民晒?

答案當(dāng)然是肯定的,但是之前也講過(guò)了DNS中有很多DnsRecordType锄禽,所以如果想實(shí)現(xiàn)全部的支持類型可能并現(xiàn)實(shí)潜必,這里我們就以最簡(jiǎn)單和最常用的A類型為例,用netty來(lái)實(shí)現(xiàn)一下DNS的TCP服務(wù)器沃但。

搭建netty服務(wù)器

因?yàn)槭荰CP請(qǐng)求磁滚,所以這里使用基于NIO的netty server服務(wù),也就是NioEventLoopGroup和NioServerSocketChannel,netty服務(wù)器的代碼如下:

        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();

因?yàn)槭欠?wù)器宵晚,所以我們需要兩個(gè)EventLoopGroup垂攘,一個(gè)是bossGroup,一個(gè)是workerGroup淤刃。

將這兩個(gè)group傳遞給ServerBootstrap晒他,并指定channel是NioServerSocketChannel,然后添加自定義的Do53ServerChannelInitializer即可逸贾。

Do53ServerChannelInitializer中包含了netty自帶的tcp編碼解碼器和自定義的服務(wù)器端消息處理方式陨仅。

這里dnsServerPort=53,也是默認(rèn)的DNS服務(wù)器的端口值铝侵。

DNS服務(wù)器的消息處理

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());
    }
}

這里我們添加了兩個(gè)netty自帶的編碼解碼器灼伤,分別是TcpDnsQueryDecoder和TcpDnsResponseEncoder。

對(duì)于netty服務(wù)器來(lái)說(shuō)咪鲜,接收到的是ByteBuf消息狐赡,為了方便服務(wù)器端的消息讀取,需要將ByteBuf解碼為DnsQuery,這也就是TcpDnsQueryDecoder在做的事情嗜诀。

public final class TcpDnsQueryDecoder extends LengthFieldBasedFrameDecoder 

TcpDnsQueryDecoder繼承自LengthFieldBasedFrameDecoder猾警,也就是以字段長(zhǎng)度來(lái)區(qū)分對(duì)象的起始位置。這和TCP查詢傳過(guò)來(lái)的數(shù)據(jù)結(jié)構(gòu)是一致的隆敢。

下面是它的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接受一個(gè)ByteBuf對(duì)象发皿,首先調(diào)用LengthFieldBasedFrameDecoder的decode方法,將真正需要解析的內(nèi)容解析出來(lái)拂蝎,然后再調(diào)用DnsMessageUtil的decodeDnsQuery方法將真正的ByteBuf內(nèi)容解碼成為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的內(nèi)容松捉,根據(jù)DnsQuestion的內(nèi)容返回一個(gè)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;
    }

上面的代碼封裝了一個(gè)新的DefaultDnsResponse對(duì)象馆里,并使用query的id作為DefaultDnsResponse的id隘世。并將question作為response的QUESEION section。

除了QUESTION section,response中還需要ANSWER section,這個(gè)ANSWER section需要填充一個(gè)DnsRecord鸠踪。

這里構(gòu)造了一個(gè)DefaultDnsRawRecord,傳入了record的name丙者,type,ttl和具體內(nèi)容营密。

最后將構(gòu)建好的DefaultDnsResponse返回械媒。

因?yàn)榭蛻舳瞬樵兊氖茿 address,按道理我們需要通過(guò)QUESTION中傳入的domain名字评汰,然后根據(jù)DNS服務(wù)器中存儲(chǔ)的記錄進(jìn)行查找纷捞,最終返回對(duì)應(yīng)域名的IP地址。

但是因?yàn)槲覀冎皇悄M的DNS服務(wù)器被去,所以并沒(méi)有真實(shí)的域名IP記錄兰绣,所以這里我們偽造了一個(gè)ip地址:

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

然后調(diào)用Unpooled的wrappedBuffer方法,將byte數(shù)組轉(zhuǎn)換成為ByteBuf,傳入DefaultDnsRawRecord的構(gòu)造函數(shù)中编振。

這樣我們的DNS服務(wù)器就搭建好了缀辩。

DNS客戶端消息請(qǐng)求

上面我們搭建好了DNS服務(wù)器,接下來(lái)就可以使用DNS客戶端來(lái)請(qǐng)求DNS服務(wù)器了踪央。

這里我們使用之前創(chuàng)建好的netty DNS客戶端臀玄,只不過(guò)進(jìn)行少許改動(dòng),將DNS服務(wù)器的域名和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就填本機(jī)的IP地址畅蹂,dnsPort就是我們剛剛創(chuàng)建的默認(rèn)端口53健无。

首先運(yùn)行DNS服務(wù)器:

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服務(wù)器已經(jīng)準(zhǔn)備好了,綁定的端口是53液斜。

然后運(yùn)行上面的客戶端累贤,在客戶端可以得到下面的結(jié)果:

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查詢成功叠穆,并且返回了我們?cè)诜?wù)器中預(yù)設(shè)的值。

然后再看一下服務(wù)器端的輸出:

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)

可以看到服務(wù)器端成功和客戶端建立了連接臼膏,并成功接收到了客戶端的查詢請(qǐng)求硼被。

總結(jié)

以上就是使用netty默認(rèn)DNS服務(wù)器端的實(shí)現(xiàn)原理和例子。因?yàn)槠邢奚酰@里只是默認(rèn)了type為A address的情況嚷硫,對(duì)其他type感興趣的朋友可以自行探索。

本文的代碼始鱼,大家可以參考:

learn-netty4

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仔掸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子医清,更是在濱河造成了極大的恐慌起暮,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会烙,死亡現(xiàn)場(chǎng)離奇詭異鞋怀,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)持搜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)密似,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人葫盼,你說(shuō)我怎么就攤上這事残腌。” “怎么了贫导?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵抛猫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我孩灯,道長(zhǎng)闺金,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任峰档,我火速辦了婚禮败匹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘讥巡。我一直安慰自己掀亩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布欢顷。 她就那樣靜靜地躺著槽棍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炼七,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天缆巧,我揣著相機(jī)與錄音,去河邊找鬼豌拙。 笑死陕悬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的姆蘸。 我是一名探鬼主播墩莫,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼芙委,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼逞敷!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起灌侣,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤推捐,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后侧啼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體牛柒,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年痊乾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了皮壁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哪审,死狀恐怖蛾魄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情湿滓,我是刑警寧澤滴须,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站叽奥,受9級(jí)特大地震影響扔水,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朝氓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一魔市、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赵哲,春花似錦嘹狞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至筷屡,卻和暖如春涧偷,著一層夾襖步出監(jiān)牢的瞬間簸喂,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工燎潮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喻鳄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓确封,卻偏偏與公主長(zhǎng)得像除呵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子爪喘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容