Netty的介紹與簡單使用
有道無術(shù),術(shù)尚可求也!有術(shù)無道,止于術(shù)!
一、Netty的優(yōu)勢
盡管我們前面學(xué)習(xí)NIO的時候,我已經(jīng)盡可能的簡化代碼,但是我們依舊會發(fā)現(xiàn),JDK NIO的開發(fā)依舊是極為復(fù)雜,在業(yè)務(wù)開發(fā)中我們還要考慮到業(yè)務(wù)的處理流程、業(yè)務(wù)的復(fù)用、請求的并發(fā)量、請求過程中的編解碼問題、網(wǎng)絡(luò)傳輸中的半包粘包問題等等,會進(jìn)一步增加NIO開發(fā)的難度!歡迎關(guān)注公眾號【源碼學(xué)徒】
Netty是基于上述難題提供了一個統(tǒng)一的解決方案,Netty提供了一個對于Socket編程的統(tǒng)一模板,即使是一個沒有網(wǎng)絡(luò)編程基礎(chǔ)的開發(fā)人員,也能夠很輕松的開發(fā)出一個高并發(fā)、低延時的程序!
Netty提供了很多默認(rèn)的編解碼功能,能夠很輕松的實(shí)現(xiàn)一些市面上常見的協(xié)議比如FTP、HTTP、SMTP、WebSocket等,能夠通過很少的幾行代碼很輕松的解決網(wǎng)絡(luò)通訊過程中的半包、粘包問題!
定制能力強(qiáng),Netty優(yōu)秀的代碼風(fēng)格和強(qiáng)大的擴(kuò)展能力,可以讓我們通過ChannelHandler對通信框架進(jìn)行靈活的擴(kuò)展,以及通過管道流(PipLine)實(shí)現(xiàn)業(yè)務(wù)功能的相互隔離與復(fù)用!
成熟、穩(wěn)定,Netty修復(fù)了現(xiàn)在JDK已經(jīng)發(fā)現(xiàn)了的,所有的JDK NIO BUG,其中最著名的就是臭名昭著的JDK空輪訓(xùn)BUG!
社區(qū)活躍,版本迭代周期短,發(fā)現(xiàn)的BUG可以被及時修復(fù),同時,更多的新功能會加入;
二、Netty的架構(gòu)設(shè)計(jì)

這是來自官網(wǎng)的一張架構(gòu)圖,我們可以大致的了解Netty的模塊!
Core核心層,是Netty的最主要的實(shí)現(xiàn),后續(xù)的所有擴(kuò)展都建立在Core之上,他提供了事件的可擴(kuò)展模型、網(wǎng)絡(luò)通訊編程的通用API、數(shù)據(jù)零拷貝和數(shù)據(jù)載體的封裝和復(fù)用! Transport Service服務(wù)傳輸層,Netty提供了底層網(wǎng)絡(luò)通訊的能力,它抽象了底層網(wǎng)絡(luò)TCP、UDP等協(xié)議的網(wǎng)絡(luò)通信,使得用戶不在為一個網(wǎng)絡(luò)通信開發(fā)的底層技術(shù)所頭疼,似的注意力能夠更加專注于業(yè)務(wù)!也正是因?yàn)檫@一層的封裝,使得NIO/BIO之間能夠無縫切換! Protocol Support協(xié)議層,Netty幾乎實(shí)現(xiàn)類市面上的大部分主流協(xié)議、包括HTTP、SSL、Protobuf、壓縮、大文件傳輸、WebSocket、文本、二進(jìn)制等主流協(xié)議, 而且Netty支持自定義擴(kuò)展協(xié)議。Netty豐富的協(xié)議使得用戶的開發(fā)成本大大降低,使用內(nèi)置的協(xié)議可以很輕松的開發(fā)一個類似于Tomcat的Http服務(wù)器!
三、Netty的基本使用和介紹
經(jīng)過上面的介紹,我們大概了解了Netty的基本架構(gòu),下面我們會看一下Netty的基本使用,然后我會逐行解析,希望能夠通過這一節(jié)課幫助大家入門Netty編程,也為后面的源碼學(xué)習(xí)更好的鋪墊一下!
1. 開發(fā)一個服務(wù)端
我們使用Netty開發(fā)一個簡單的服務(wù)端代碼:
服務(wù)端接收到客戶端的消息,打印出來,然后主動中斷連接!
啟動服務(wù)端源代碼:
/**
* 服務(wù)端
*
* @author 源碼學(xué)徒
* @date 2021年4月19日12:39:21
*/
public class EchoServer {
public static void main(String[] args) {
//設(shè)置事件循環(huán)組
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
try {
//設(shè)置服務(wù)啟動器
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.localAddress(8989)
//設(shè)置服務(wù)管道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
//同步阻塞 直到綁定完成
ChannelFuture channelFuture = serverBootstrap.bind().sync();
//監(jiān)聽通道關(guān)閉方法 等待服務(wù)端通道關(guān)閉完成
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//優(yōu)雅的關(guān)閉線程組
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
服務(wù)端讀取數(shù)據(jù)處理器源代碼 :EchoServerHandler
/**
* 服務(wù)端打印處理的handler
*
* @author huangfu
* @date 2021年4月19日12:42:34
*/
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
try {
//轉(zhuǎn)換為字符串
String message = byteBuf.toString(StandardCharsets.UTF_8);
System.out.println(message);
}finally {
ReferenceCountUtil.release(byteBuf);
ctx.channel().close();
}
}
}
2. 服務(wù)端源代碼介紹
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
定義兩個事件循環(huán)組,還記得我們第五章將的我們對NIO的優(yōu)化版本嗎?現(xiàn)在暫時你可以把它理解為兩個selector組:
boss內(nèi)部只包含一個selector選擇器,用于接收新連接!worker內(nèi)部默認(rèn)是CPU*2個selector選擇器,用于處理我們的業(yè)務(wù)邏輯!
boss接收到新連接后,將新連接產(chǎn)生的SocketChannel交給worker線程組處理!用一個現(xiàn)在比較流行的比喻:boss相當(dāng)于老板,worker相當(dāng)于工人,boss不處理工作,只負(fù)責(zé)在外面跑業(yè)務(wù)拉活談合同。接到新活之后,就把這個活交給worker來干,自己再去接下一個活!
這兩行代碼建議參考上一章的NIO的優(yōu)化版本學(xué)習(xí)!
ServerBootstrap serverBootstrap = new ServerBootstrap();
ServerBootstrap是Netty為我們提供的一個快速啟動配置類,為什么Netty相較于NIO開發(fā)比較簡單,就是因?yàn)镹etty為我們提供了一個網(wǎng)絡(luò)編程的代碼的模板,那么想要使用這些模板,就必須要告訴模板一些必要的參數(shù),供模板讀取,而ServerBootstrap正式這個作用,通過初始化的時候,我們設(shè)置各種參數(shù),我們將之保存到ServerBootstrap中,后續(xù)Netty服務(wù)啟動的時候,會讀取我們初始化的時候配置的各種參數(shù),完成自己的初始化!
serverBootstrap.group(boss, worker)
將我們前面初始化的兩個事件循環(huán)組綁定起來,保存到serverBootstrap中,供后續(xù)讀取!
.channel(NioServerSocketChannel.class)
我們這里開發(fā)的是一個服務(wù)端程序,所以我們使用的是NioServerSocketChannel,在Netty中分為兩種SocketChannel,一種是NioServerSocketChannel一種是NioSocketChannel,兩者都繼承與JDK的ServerSocketChannel和SocketChannel,是Netty官方對于通訊管道的擴(kuò)展:

NioServerSocketChannel: 服務(wù)端通道 NioSocketChannel: 客戶端通道
有關(guān)于兩個通道,我們后面會詳細(xì)分析,這里你們先記著,NioServerSocketChannel代表著服務(wù)端通道!NioSocketChannel代表著客戶端通道,不要搞混了!
.localAddress(8989)
設(shè)置一個端口號,服務(wù)端對外暴露的端口號!
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
這個代碼及其重要,后面也會重點(diǎn)提及,這里主要是做服務(wù)編排的,想客戶端Socket綁定一個通道,并添加我們的業(yè)務(wù)處理類 xxxxHandler,當(dāng)一個客戶端連接上服務(wù)端后,后續(xù)所有的業(yè)務(wù)處理,都會由這里注冊的各種Handler來處理,這就是Netty提供的能夠讓開發(fā)人員專注于業(yè)務(wù)開發(fā)的主要邏輯所在,后面會對這一塊代碼進(jìn)行一個重點(diǎn)的講解,這里也只需要記住,我們注冊這些Handler后,客戶端發(fā)來的數(shù)據(jù)都會在這些Handler中流轉(zhuǎn)處理!
ChannelFuture channelFuture = serverBootstrap.bind().sync();
上面的初始化完成了,開始進(jìn)行服務(wù)器啟動和端口綁定,根據(jù)上面設(shè)置的端口號綁定,細(xì)心的同學(xué)可能會發(fā)現(xiàn)我還調(diào)用了一個sync方法,Netty是基于異步事件來開發(fā)的,這里我們進(jìn)行bind調(diào)用之后,因?yàn)槭钱惒綀?zhí)行,所以我們并不知道什么時候完成,所以這里調(diào)用了一個阻塞的方法(sync),一直阻塞等待綁定完成后才繼續(xù)往下走!
channelFuture.channel().closeFuture().sync();
獲取一個服務(wù)端的通道對象,并且對服務(wù)端的通道對象添加一個關(guān)閉的監(jiān)聽,并調(diào)用阻塞方法(sync),調(diào)用后,程序會一直阻塞再這里,等待服務(wù)端管道關(guān)閉,才會繼續(xù)往下走!一般來說,服務(wù)端管道除非我們主動停機(jī)活因?yàn)楫惓1罎ⅲ駝t服務(wù)端管道會一直存活,那么改程序?qū)恢弊枞谶@里,形成一個服務(wù)!
boss.shutdownGracefully();
worker.shutdownGracefully();
優(yōu)雅停機(jī),該段程序會將通道標(biāo)記為不可用狀態(tài),等待程序處理完畢后,釋放Netty所創(chuàng)建的所有資源!
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {....}
當(dāng)服務(wù)端察覺到客戶端發(fā)來數(shù)據(jù)時,回調(diào)該邏輯!
3. 開發(fā)一個客戶端
/**
* 打印程序客戶端
*
* @author huangfu
* @date 2021年4月19日13:58:16
*/
public class EchoClient {
public static void main(String[] args) {
EventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.remoteAddress("127.0.0.1", 8989)
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect().sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}
客戶端處理Handler
/**
* @author huangfu
* @date
*/
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes("Hello Netty".getBytes(StandardCharsets.UTF_8));
ctx.channel().writeAndFlush(buffer);
}
}
4. 客戶端源代碼介紹
客戶端的代碼和服務(wù)端的代碼基本一致,所以這里不做全部講解,只做不一樣的講解:
EventLoopGroup worker = new NioEventLoopGroup();
這里客戶端只有一個事件循環(huán)組,為什么?因?yàn)榭蛻舳瞬淮嬖谛逻B接嘛!
Bootstrap bootstrap = new Bootstrap();
這個里和服務(wù)端的ServerBootstrap基本一致,是為了保存客戶端的配置所設(shè)置的一個類!
ChannelFuture channelFuture = bootstrap.connect().sync();
連接服務(wù)端,并等待鏈接完成!
Handler的重載方法也發(fā)生了變化:
public void channelActive(ChannelHandlerContext ctx) throws Exception {....}
這里的含義是,當(dāng)客戶端被激活后既鏈接到服務(wù)端后,回調(diào)該邏輯!
細(xì)心的同學(xué)在練習(xí)的時候可能會發(fā)現(xiàn)一點(diǎn)問題,我們發(fā)現(xiàn)客戶端會有 handler和childHandler兩種方法
.handler()
//設(shè)置服務(wù)管道
.childHandler()
handler: 是綁定在 ServerSocketChannel之上的,負(fù)責(zé)服務(wù)端的邏輯處理! childHandler: 是綁定在SockerChannel之上的,當(dāng)客戶端綁定成功后,會產(chǎn)生一個SocketChannel對象會調(diào)用該handler的綁定!
總結(jié)
本節(jié)課比較簡單,主要是對Netty的基本使用有一個比較簡單的認(rèn)知,希望大家課下多練習(xí),爭取會簡單的使用Netty!
才疏學(xué)淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關(guān)注作者的公眾號,一起進(jìn)步,一起學(xué)習(xí)!
??「轉(zhuǎn)發(fā)」和「在看」,是對我最大的支持??
