Netty網絡框架
Netty是一個異步的基於事件驅動的網絡框架。
為什么要使用Netty而不直接使用JAVA中的NIO
1.Netty支持三種IO模型同時支持三種Reactor模式。
2.Netty支持很多應用層的協議,提供了很多decoder和encoder。
3.Netty能夠解決TCP長連接所帶來的缺陷(粘包、半包等)
4.Netty支持應用層的KeepAlive。
5.Netty規避了JAVA NIO中的很多BUG,性能更好。
Netty啟動服務端
1.創建bossGroup和workerGroup(bossGroup負責接收連接,workerGroup負責處理連接的讀寫就緒事件)
2.創建ServerBootstrap服務端啟動對象,並且調用group()方法傳入剛剛創建的bossGroup和workerGroup。
3.調用ServerBootstrap的channel()方法,配置父Channel,一般為NioServerSocketChannel。
4.調用ServerBootstrap的childHandler()方法,配置子Channel與ChannelHandler之間的關系,傳入ChannelInitializer實現類,實現initChannel()方法,方法中獲取Channel的ChannelPileline,然后往ChannelPipeline中添加ChannelHandler。
5.調用ServerBootstrap的option()方法給父Channel配置參數。
6.調用ServerBootstrap的childOption()方法給子Channel配置參數。
7.綁定端口,啟動服務。
private void start() {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // 配置父Channel
.childHandler(new ChannelInitializer<SocketChannel>() { // 配置子Channel與ChannelHandler之間的關系
@Override
protected void initChannel(SocketChannel socketChannel) {
// 往ChannelPipeline中添加ChannelHandler
socketChannel.pipeline().addLast(
new HttpRequestDecoder(),
new HttpObjectAggregator(65535),
new HttpResponseEncoder(),
new HttpServerHandler()
);
}
})
.option(ChannelOption.SO_BACKLOG, 128) // 給父Channel配置參數
.childOption(ChannelOption.SO_KEEPALIVE, true); // 給子Channel配置參數
try {
// 綁定端口,啟動服務
System.out.println("start server and bind 8888 port ...");
serverBootstrap.bind(8888).sync();
} catch (InterruptedException e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
Netty啟動客戶端
1.創建workerGroup,負責處理連接的讀寫就緒事件。
2.創建Bootstrap客戶端啟動對象,並且調用group()方法傳入剛剛創建的workerGroup。
3.調用Bootstrap的channel()方法,配置父Channel,一般為NioSocketChannel。
4.調用Bootstrap的option()方法,給父Channel配置參數。
5.調用Bootstrap的handler()方法,配置父Channel與ChannelHandler之間的關系。
6.連接服務器。
private void start() {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class) // 配置父Channel
.option(ChannelOption.SO_KEEPALIVE, true) // 給父Channel配置參數
.handler(new ChannelInitializer<SocketChannel>() { // 配置父Channel與ChannelHandler之間的關系
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
try {
bootstrap.connect(new InetSocketAddress(8888)).sync(); // 連接服務器
} catch (InterruptedException e) {
workerGroup.shutdownGracefully();
}
}
ChannelInBoundHandler接口聲明了事件的處理方法
channelActive():當接收到一個新的連接時調用該方法
handlerAdd():當往Channel的ChannelPipeline中添加ChannelHandler時調用該方法
handlerRemove():當移除ChannelPipeline中的ChannelHandler時調用該方法
channelRead():當Channel有數據可讀時調用該方法
exceptionCaught():當在處理事件發生異常時調用該方法
ServerSocketChannel每接收到一個新的連接時都會調用ChannelInitializer的initChannel()方法,初始化Channel與ChannelHandler之間的關系,最后調用ChannelHandler的handlerAdd()和channelActive()方法。
關於ChannelPipeline
ChannelPipeline底層使用雙向鏈表。

當Channel有數據可讀時,會沿着鏈表從前往后尋找有IN性質的Handler進行處理。
當Channel寫入數據時,會沿着鏈表從后往前尋找有OUT性質的Handler進行處理。
關於write()和flush()方法
write():將數據寫入到緩沖區
flush():發送緩沖區中的數據並進行清空
writeAndFlush():將數據寫入到緩沖區,同時發送緩沖區中的數據並進行清空
Channel的writeAndFlush()和flush()方法會從鏈表的最后一個節點開始從后往前尋找有OUT性質的Handler進行處理。
ChannelHandlerContext的writeAndFlush()和flush()方法會從當前節點從后往前尋找有OUT性質的Handler進行處理。
關於寫就緒事件
當SocketChannel可以寫入數據時,將會觸發寫就緒事件,所以一般不能隨便監聽,否則將會一直觸發。
當SocketChannel在寫入數據寫不進時(緩沖區已經滿了),此時可以將Channel注冊到Selector當中並且向Selector傳遞要監聽此Channel的寫就緒事件,然后強制發送緩沖區中的數據並進行清空,此時將會觸發寫就緒事件,當處理完寫就緒事件后,應該從Selector當中剔除對此Channel的監聽。
為什么說Netty中的所有操作都是異步的
Channel中的所有任務都會放入到其綁定的EventLoop的任務隊列中,然后等待被EventLoop中的線程處理。
關於ChannelFuture
由於Netty中的所有操作都是異步的,因此一般會返回ChannelFuture對象,用於存儲Channel異步執行的結果。
當創建ChannelFuture實例時,isDone()方法返回false,僅當ChannelFuture被設置成成功或者失敗時,isDone()方法才返回true。
可以往ChannelFuture中添加ChannelFutureListener,當任務被執行完畢后由IO線程自動調用。
Netty中的ByteBuf
ByteBuf有readerIndex和writerIndex兩個指針,默認都為0,當進行寫操作時移動writerIndex指針,讀操作時移動readerIndex指針。
可讀容量 = writerIndex - readerIndex
*只有read()/write()方法才會移動指針,get()/set()方法不會移動指針。
*ByteBuf支持動態擴容。
ByteBuf的創建和管理
使用ByteBufAllocator來創建和管理ByteBuf,其分別提供PooledByteBufAllocator和UnpooledByteBufAllocator實現類,分別代表池化和非池化。
*Netty同時也提供了Pooled和Unpooled工具類來創建和管理ByteBuf。
池化的ByteBuf(Pooled)
每次使用時都從池中取出一個ByteBuf對象,當使用完畢后再放回到池中。
每個ByteBuf都有一個refCount屬性,僅當refCount屬性為0時才將ByteBuf對象放回到池中。
ByteBuf的release()方法可以使refCount屬性減1(一般由最后一個訪問ByteBuf的Handler進行處理)
非池化的ByteBuf(Unpooled)
每次使用時都創建一個新的ByteBuf對象。
使用池化ByteBuf的風險
如果每次使用ByteBuf后卻不進行釋放,那么有可能發生內存泄漏,對象池中會不停的創建ByteBuf對象。
非池化的ByteBuf對象能夠依賴JVM自動進行回收。
關於堆內和堆外的ByteBuf
池化和非池化的ByteBufAllocator中都可以創建堆內和堆外的ByteBuf對象。
堆外的ByteBuf可以避免在進行IO操作時數據從堆內內存復制到操作系統內存的過程,所以對於IO操作來說一般使用堆外的ByteBuf,而對於內部業務數據處理來說使用堆內的ByteBuf。
Netty支持的IO模型
Netty支持BIO、NIO、AIO三種IO模型。

*其中AIO模型只在Netty的5.x版本有提供,但不建議使用,因為Netty不再維護同時也廢除了5.x版本,其原因是在Linux中AIO比NIO強不了多少。
Netty如何切換IO模型
只需要將EventLoopGroup和ServerSocketChannel換成相應IO模型的API即可。
Netty中使用Reactor模式
Reactor單線程模式

EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
Reactor多線程模式

EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
*默認CPU核數 x 2個EventLoop。
主從Reactor多線程模式

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
關於TCP的粘包和半包
粘包(多個數據包被合並成一個進行發送)
半包(一個數據包被拆分成多個進行發送)
發生粘包的原因
1.寫入的數據遠小於緩沖區的大小,TCP協議為了性能的考慮,合並后再進行發送。
發生半包的原因
1.寫入的數據大於緩沖區的大小,因此必須拆包后再進行傳輸(緩沖區已滿,強制flush)
2.寫入的數據大於協議的MTU(最大傳輸單元),因此必須拆包后再進行傳輸。
TCP長連接的缺陷
長連接中可以發送多個請求,同時TCP協議是流式協議,消息無邊界,所以有一個很棘手的問題,接收方怎么去知道一個請求中的數據到底是哪里到哪里,以及一個請求中的數據有可能是粘包后的結果,同時多個請求中的數據有可能是半包后的結果。
解決方案
1.使用短連接,每次發送請求時都建立一個連接。
2.使用固定的長度,每個請求中的數據都使用固定的長度,接收方以接收到固定長度的數據來確定一個完整的請求數據。
3.使用指定的分隔符,每個請求中的數據的末尾都加上一個分隔符,接收方以分隔符來確定一個完整的請求數據。
4.使用特定長度的字段去存儲請求數據的長度,接收方根據請求數據的長度來確定一個完整的請求數據。
Netty對TCP長連接缺陷的解決方案
FixedLengthFrameDecoder:使用固定的長度
DelimiterBasedFrameDecoder:使用指定的分隔符
LengthFieldBasedFrameDecoder:使用特定長度的字段去存儲請求數據的長度
關於TCP的KeepAlive
正常情況下雙方建立連接后是不會斷開的,KeepAlive就是防止連接雙方中的任意一方由於意外斷開而通知不到對方,導致對方一直持有連接,占用資源(發現對方不可用,斷開連接)。
*建立連接需要三次握手、正常斷開連接需要四次揮手。
KeepAlive有三個核心參數
net.ipv4.tcp_keepalive_timeout:連接的超時時間(默認7200s)
net.ipv4.tcp_keepalive_intvl:發送探測包的間隔(默認75s)
tnet.ipv4.cp_keepalive_probes:發送探測包的個數(默認9個)
這三個參數都是系統參數,會影響部署在機器上的所有應用。
KeepAlive的開關是在應用層開啟的,只有當應用層開啟了KeepAlive,KeepAlive才會生效。
java.net.Socket.setKeepAlive(boolean on);
當連接在指定時間內沒有發送請求時,開啟KeepAlive的一端就會向對方發送一個探測包,如果對方沒有回應,則每隔指定時間發送一個探測包,總共發送指定個探測包,如果對方都沒有回應則認為對方不可用,斷開連接。
為什么要做應用層的KeepAlive
1.KeepAlive參數是系統參數,對於應用來說不夠靈活。
2.默認檢測一個不可用的連接所需要的時間太長。
怎么做應用層的KeepAlive
1.定時任務
客戶端定期向所有已經建立連接的服務端發送心跳檢測,如果服務端連續沒有回應指定個心跳檢測,則認為對方不可用,此時客戶端應該重連。
服務端定期向所有已經建立連接的客戶端發送心跳檢測,如果客戶端連續沒有回應指定個心跳檢測,則認為對方不可用,此時應該斷開連接。
2.計時器
連接在指定時間內沒有發送請求則認為對方不可用
Netty對KeepAlive的支持
Netty開啟KeepAlive
Bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
ServerBootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);
Netty提供的KeepAlive機制
Netty提供的IdleStateHandler能夠檢測處於Idle狀態的連接。
Idle狀態類型
reader_idle:SocketChannel在指定時間內都沒有數據可讀
writer_idle:SocketChannel在指定時間內沒有寫入數據
all_idle:SocketChannel在指定時間內沒有數據可讀或者沒有寫入數據
直接將IdleStateHandler添加到ChannelPipeline即可,當Netty檢測到處於Idle狀態的連接時,將會自動調用其Handler的userEventTriggered()方法,用戶只需要在該方法中判斷Idle狀態的類型,然后做出相應的處理。
關於HTTP的KeepAlive
HTTP的KeepAlive是對長連接和短連接的選擇,並不是發現對方不可用,斷開連接。
HTTP是基於請求和響應的,客戶端發送請求給服務端然后等待服務端的響應,當服務端檢測到請求頭中包含Connection:KeepAlive時,表示客戶端使用長連接,此時服務端應該保持連接,當檢測到請求頭中包含Connection:close時,表示客戶端使用短連接,此時服務端應該主動斷開連接。
TCP並不是基於請求和響應的,客戶端可以發送請求給服務端,同時服務端也可以發送請求給客戶端。