Netty 是什么文判?
Netty 是一個(gè)?基于 NIO?的 client-server(客戶端服務(wù)器)框架戏仓,使用它可以快速簡(jiǎn)單地開(kāi)發(fā)網(wǎng)絡(luò)應(yīng)用程序亡鼠。它極大地簡(jiǎn)化并優(yōu)化了 TCP 和 UDP 套接字服務(wù)器等網(wǎng)絡(luò)編程,并且性能以及安全性等很多方面甚至都要更好间涵。支持多種協(xié)議?如 FTP勾哩,SMTP根盒,HTTP 以及各種二進(jìn)制和基于文本的傳統(tǒng)協(xié)議炎滞。用官方的總結(jié)就是:Netty 成功地找到了一種在不妥協(xié)可維護(hù)性和性能的情況下實(shí)現(xiàn)易于開(kāi)發(fā)册赛,性能震嫉,穩(wěn)定性和靈活性的方法票堵。
除了上面介紹的之外悴势,很多開(kāi)源項(xiàng)目比如我們常用的 Dubbo、RocketMQ军俊、Elasticsearch、gRPC 等等都用到了 Netty昔穴。
網(wǎng)絡(luò)編程我愿意稱 Netty 為王 吗货。
為什么要用 Netty卿操?
因?yàn)?Netty 具有下面這些優(yōu)點(diǎn)害淤,并且相比于直接使用 JDK 自帶的 NIO 相關(guān)的 API 來(lái)說(shuō)更加易用窥摄。
統(tǒng)一的 API础淤,支持多種傳輸類型,阻塞和非阻塞的建峭。簡(jiǎn)單而強(qiáng)大的線程模型亿蒸。自帶編解碼器解決 TCP 粘包/拆包問(wèn)題边锁。自帶各種協(xié)議棧茅坛。真正的無(wú)連接數(shù)據(jù)包套接字支持贡蓖。比直接使用 Java 核心 API 有更高的吞吐量摩梧、更低的延遲仅父、更低的資源消耗和更少的內(nèi)存復(fù)制笙纤。安全性不錯(cuò)省容,有完整的 SSL/TLS 以及 StartTLS 支持腥椒。社區(qū)活躍成熟穩(wěn)定笼蛛,經(jīng)歷了大型項(xiàng)目的使用和考驗(yàn)滨砍,而且很多開(kāi)源項(xiàng)目都使用到了 Netty惋戏, 比如我們經(jīng)常接觸的 Dubbo响逢、RocketMQ 等等。......Netty 應(yīng)用場(chǎng)景了解么回论?
Netty 可以做什么事情傀蓉?
理論上來(lái)說(shuō)葬燎,NIO 可以做的事情 谱净,使用 Netty 都可以做并且更好郊丛。Netty 主要用來(lái)做網(wǎng)絡(luò)通信?:
作為 RPC 框架的網(wǎng)絡(luò)通信工具?:我們?cè)诜植际较到y(tǒng)中厉熟,不同服務(wù)節(jié)點(diǎn)之間經(jīng)常需要相互調(diào)用揍瑟,這個(gè)時(shí)候就需要 RPC 框架了绢片。不同服務(wù)節(jié)點(diǎn)之間的通信是如何做的呢?可以使用 Netty 來(lái)做。比如我調(diào)用另外一個(gè)節(jié)點(diǎn)的方法的話纯续,至少是要讓對(duì)方知道我調(diào)用的是哪個(gè)類中的哪個(gè)方法以及相關(guān)參數(shù)吧灭袁!實(shí)現(xiàn)一個(gè)自己的 HTTP 服務(wù)器?:通過(guò) Netty 我們可以自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 HTTP 服務(wù)器茸歧,這個(gè)大家應(yīng)該不陌生软瞎。說(shuō)到 HTTP 服務(wù)器的話涤浇,作為 Java 后端開(kāi)發(fā),我們一般使用 Tomcat 比較多只锭。一個(gè)最基本的 HTTP 服務(wù)器可要以處理常見(jiàn)的 HTTP Method 的請(qǐng)求著恩,比如 POST 請(qǐng)求、GET 請(qǐng)求等等蜻展。實(shí)現(xiàn)一個(gè)即時(shí)通訊系統(tǒng)?:使用 Netty 我們可以實(shí)現(xiàn)一個(gè)可以聊天類似微信的即時(shí)通訊系統(tǒng)喉誊,這方面的開(kāi)源項(xiàng)目還蠻多的,可以自行去 Github 找一找纵顾。實(shí)現(xiàn)消息推送系統(tǒng)?:市面上有很多消息推送系統(tǒng)都是基于 Netty 來(lái)做的伍茄。......Netty 核心組件有哪些施逾?分別有什么作用敷矫?
Netty 核心組件有哪些?分別有什么作用汉额?
1.Channel
Channel 接口是 Netty 對(duì)網(wǎng)絡(luò)操作抽象類沪饺,它除了包括基本的 I/O 操作,如 bind()闷愤、connect()整葡、read()、write() 等讥脐。
比較常用的Channel接口實(shí)現(xiàn)類是NioServerSocketChannel(服務(wù)端)和NioSocketChannel(客戶端)遭居,這兩個(gè) Channel 可以和 BIO 編程模型中的ServerSocket以及Socket兩個(gè)概念對(duì)應(yīng)上。Netty 的 Channel 接口所提供的 API旬渠,大大地降低了直接使用 Socket 類的復(fù)雜性俱萍。
2.EventLoop
這么說(shuō)吧!EventLoop(事件循環(huán))接口可以說(shuō)是 Netty 中最核心的概念了告丢!
《Netty 實(shí)戰(zhàn)》這本書(shū)是這樣介紹它的:
“EventLoop 定義了 Netty 的核心抽象枪蘑,用于處理連接的生命周期中所發(fā)生的事件。
是不是很難理解?說(shuō)實(shí)話岳颇,我學(xué)習(xí) Netty 的時(shí)候看到這句話是沒(méi)太能理解的照捡。
說(shuō)白了,EventLoop 的主要作用實(shí)際就是負(fù)責(zé)監(jiān)聽(tīng)網(wǎng)絡(luò)事件并調(diào)用事件處理器進(jìn)行相關(guān) I/O 操作的處理话侧。
那 Channel 和 EventLoop 直接有啥聯(lián)系呢栗精?
Channel 為 Netty 網(wǎng)絡(luò)操作(讀寫(xiě)等操作)抽象類,EventLoop 負(fù)責(zé)處理注冊(cè)到其上的Channel 處理 I/O 操作,兩者配合參與 I/O 操作。
3.ChannelFuture
Netty 是異步非阻塞的喉刘,所有的 I/O 操作都為異步的。
因此薪夕,我們不能立刻得到操作是否執(zhí)行成功,但是赫悄,你可以通過(guò) ChannelFuture 接口的 addListener() 方法注冊(cè)一個(gè) ChannelFutureListener寥殖,當(dāng)操作執(zhí)行成功或者失敗時(shí),監(jiān)聽(tīng)就會(huì)自動(dòng)觸發(fā)返回結(jié)果涩蜘。
并且嚼贡,你還可以通過(guò)ChannelFuture 的 channel() 方法獲取關(guān)聯(lián)的Channel
public interface ChannelFuture extends Future<Void> { Channel channel(); ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1); ...... ChannelFuture sync() throws InterruptedException;}
另外,我們還可以通過(guò) ChannelFuture 接口的 sync()方法讓異步的操作變成同步的同诫。
4.ChannelHandler 和 ChannelPipeline
下面這段代碼使用過(guò) Netty 的小伙伴應(yīng)該不會(huì)陌生粤策,我們指定了序列化編解碼器以及自定義的 ChannelHandler 處理消息。
b.group(eventLoopGroup) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class)); ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class)); ch.pipeline().addLast(new KryoClientHandler()); } });
ChannelHandler 是消息的具體處理器误窖。他負(fù)責(zé)處理讀寫(xiě)操作叮盘、客戶端連接等事情。
ChannelPipeline 為 ChannelHandler 的鏈霹俺,提供了一個(gè)容器并定義了用于沿著鏈傳播入站和出站事件流的 API 柔吼。當(dāng) Channel 被創(chuàng)建時(shí),它會(huì)被自動(dòng)地分配到它專屬的 ChannelPipeline丙唧。
我們可以在 ChannelPipeline 上通過(guò) addLast() 方法添加一個(gè)或者多個(gè)ChannelHandler 愈魏,因?yàn)橐粋€(gè)數(shù)據(jù)或者事件可能會(huì)被多個(gè) Handler 處理。當(dāng)一個(gè) ChannelHandler 處理完之后就將數(shù)據(jù)交給下一個(gè) ChannelHandler 想际。
EventloopGroup 了解么?和 EventLoop 啥關(guān)系?
EventLoopGroup 包含多個(gè) EventLoop(每一個(gè) EventLoop 通常內(nèi)部包含一個(gè)線程)培漏,上面我們已經(jīng)說(shuō)了 EventLoop 的主要作用實(shí)際就是負(fù)責(zé)監(jiān)聽(tīng)網(wǎng)絡(luò)事件并調(diào)用事件處理器進(jìn)行相關(guān) I/O 操作的處理。
并且 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理胡本,即 Thread 和 EventLoop 屬于 1 : 1 的關(guān)系牌柄,從而保證線程安全。
上圖是一個(gè)服務(wù)端對(duì) EventLoopGroup 使用的大致模塊圖侧甫,其中 Boss EventloopGroup 用于接收連接珊佣,Worker EventloopGroup 用于具體的處理(消息的讀寫(xiě)以及其他邏輯處理)蹋宦。
從上圖可以看出:當(dāng)客戶端通過(guò) connect 方法連接服務(wù)端時(shí),bossGroup 處理客戶端連接請(qǐng)求咒锻。當(dāng)客戶端處理完成后冷冗,會(huì)將這個(gè)連接提交給 workerGroup 來(lái)處理,然后 workerGroup 負(fù)責(zé)處理其 IO 相關(guān)操作虫碉。
對(duì) Bootstrap 和 ServerBootstrap 的了解
Bootstrap 是客戶端的啟動(dòng)引導(dǎo)類/輔助類贾惦,具體使用方法如下:
EventLoopGroup group = new NioEventLoopGroup(); try { //創(chuàng)建客戶端啟動(dòng)引導(dǎo)/輔助類:Bootstrap Bootstrap b = new Bootstrap(); //指定線程模型 b.group(group). ...... // 嘗試建立連接 ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { // 優(yōu)雅關(guān)閉相關(guān)線程組資源 group.shutdownGracefully(); }
ServerBootstrap 客戶端的啟動(dòng)引導(dǎo)類/輔助類胸梆,具體使用方法如下:
// 1.bossGroup 用于接收連接敦捧,workerGroup 用于具體的處理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.創(chuàng)建服務(wù)端啟動(dòng)引導(dǎo)/輔助類:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.給引導(dǎo)類配置兩大線程組,確定了線程模型 b.group(bossGroup, workerGroup). ...... // 6.綁定端口 ChannelFuture f = b.bind(port).sync(); // 等待連接關(guān)閉 f.channel().closeFuture().sync(); } finally { //7.優(yōu)雅關(guān)閉相關(guān)線程組資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
從上面的示例中,我們可以看出:
Bootstrap 通常使用 connet() 方法連接到遠(yuǎn)程的主機(jī)和端口碰镜,作為一個(gè) Netty TCP 協(xié)議通信中的客戶端兢卵。另外,Bootstrap 也可以通過(guò) bind() 方法綁定本地的一個(gè)端口绪颖,作為 UDP 協(xié)議通信中的一端秽荤。ServerBootstrap通常使用 bind() 方法綁定本地的端口上,然后等待客戶端的連接柠横。Bootstrap 只需要配置一個(gè)線程組— EventLoopGroup ,而 ServerBootstrap需要配置兩個(gè)線程組— EventLoopGroup 窃款,一個(gè)用于接收連接,一個(gè)用于具體的處理牍氛。
NioEventLoopGroup 默認(rèn)的構(gòu)造函數(shù)會(huì)起多少線程呢晨继?
回顧我們?cè)谏厦鎸?xiě)的服務(wù)器端的代碼:
// 1.bossGroup 用于接收連接,workerGroup 用于具體的處理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();
為了搞清楚NioEventLoopGroup 默認(rèn)的構(gòu)造函數(shù) 到底創(chuàng)建了多少個(gè)線程搬俊,我們來(lái)看一下它的源碼紊扬。
/** * 無(wú)參構(gòu)造函數(shù)。 * nThreads:0 */ public NioEventLoopGroup() { //調(diào)用下一個(gè)構(gòu)造方法 this(0); } /** * Executor:null */ public NioEventLoopGroup(int nThreads) { //繼續(xù)調(diào)用下一個(gè)構(gòu)造方法 this(nThreads, (Executor) null); } //中間省略部分構(gòu)造函數(shù) /** * RejectedExecutionHandler():RejectedExecutionHandlers.reject() */ public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) { //開(kāi)始調(diào)用父類的構(gòu)造函數(shù) super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); }
一直向下走下去的話唉擂,你會(huì)發(fā)現(xiàn)在 MultithreadEventLoopGroup 類中有相關(guān)的指定線程數(shù)的代碼餐屎,如下:
// 從1,系統(tǒng)屬性玩祟,CPU核心數(shù)*2 這三個(gè)值中取出一個(gè)最大的 //可以得出 DEFAULT_EVENT_LOOP_THREADS 的值為CPU核心數(shù)*2 private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); // 被調(diào)用的父類構(gòu)造函數(shù)腹缩,NioEventLoopGroup 默認(rèn)的構(gòu)造函數(shù)會(huì)起多少線程的秘密所在 // 當(dāng)指定的線程數(shù)nThreads為0時(shí),使用默認(rèn)的線程數(shù)DEFAULT_EVENT_LOOP_THREADS protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args); }
綜上空扎,我們發(fā)現(xiàn) NioEventLoopGroup 默認(rèn)的構(gòu)造函數(shù)實(shí)際會(huì)起的線程數(shù)為?CPU核心數(shù)*2庆聘。
另外,如果你繼續(xù)深入下去看構(gòu)造函數(shù)的話勺卢,你會(huì)發(fā)現(xiàn)每個(gè)NioEventLoopGroup對(duì)象內(nèi)部都會(huì)分配一組NioEventLoop伙判,其大小是 nThreads, 這樣就構(gòu)成了一個(gè)線程池, 一個(gè)NIOEventLoop 和一個(gè)線程相對(duì)應(yīng)黑忱,這和我們上面說(shuō)的 EventloopGroup 和 EventLoop關(guān)系這部分內(nèi)容相對(duì)應(yīng)宴抚。
Netty 線程模型了解么勒魔?
大部分網(wǎng)絡(luò)框架都是基于 Reactor 模式設(shè)計(jì)開(kāi)發(fā)的。
“Reactor 模式基于事件驅(qū)動(dòng)菇曲,采用多路復(fù)用將事件分發(fā)給相應(yīng)的 Handler 處理冠绢,非常適合處理海量 IO 的場(chǎng)景。
在 Netty 主要靠 NioEventLoopGroup 線程池來(lái)實(shí)現(xiàn)具體的線程模型的 常潮。
我們實(shí)現(xiàn)服務(wù)端的時(shí)候弟胀,一般會(huì)初始化兩個(gè)線程組:
bossGroup?:接收連接。workerGroup?:負(fù)責(zé)具體的處理喊式,交由對(duì)應(yīng)的 Handler 處理孵户。下面我們來(lái)詳細(xì)看一下 Netty 中的線程模型吧!
1.單線程模型:
一個(gè)線程需要執(zhí)行處理所有的 accept岔留、read夏哭、decode、process献联、encode竖配、send 事件。對(duì)于高負(fù)載里逆、高并發(fā)进胯,并且對(duì)性能要求比較高的場(chǎng)景不適用。
對(duì)應(yīng)到 Netty 代碼是下面這樣的
“使用 NioEventLoopGroup 類的無(wú)參構(gòu)造函數(shù)設(shè)置線程數(shù)量的默認(rèn)值就是?CPU 核心數(shù) *2原押。
//1.eventGroup既用于處理客戶端連接胁镐,又負(fù)責(zé)具體的處理。 EventLoopGroup eventGroup = new NioEventLoopGroup(1); //2.創(chuàng)建服務(wù)端啟動(dòng)引導(dǎo)/輔助類:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); boobtstrap.group(eventGroup, eventGroup) //......
2.多線程模型
一個(gè) Acceptor 線程只負(fù)責(zé)監(jiān)聽(tīng)客戶端的連接班眯,一個(gè) NIO 線程池負(fù)責(zé)具體處理:accept希停、read、decode署隘、process宠能、encode、send 事件磁餐。滿足絕大部分應(yīng)用場(chǎng)景违崇,并發(fā)連接量不大的時(shí)候沒(méi)啥問(wèn)題,但是遇到并發(fā)連接大的時(shí)候就可能會(huì)出現(xiàn)問(wèn)題诊霹,成為性能瓶頸羞延。
對(duì)應(yīng)到 Netty 代碼是下面這樣的:
// 1.bossGroup 用于接收連接,workerGroup 用于具體的處理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try { //2.創(chuàng)建服務(wù)端啟動(dòng)引導(dǎo)/輔助類:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.給引導(dǎo)類配置兩大線程組,確定了線程模型 b.group(bossGroup, workerGroup) //......
3.主從多線程模型
從一個(gè) 主線程 NIO 線程池中選擇一個(gè)線程作為 Acceptor 線程脾还,綁定監(jiān)聽(tīng)端口伴箩,接收客戶端連接的連接,其他線程負(fù)責(zé)后續(xù)的接入認(rèn)證等工作鄙漏。連接建立完成后嗤谚,Sub NIO 線程池負(fù)責(zé)具體處理 I/O 讀寫(xiě)棺蛛。如果多線程模型無(wú)法滿足你的需求的時(shí)候,可以考慮使用主從多線程模型 巩步。
// 1.bossGroup 用于接收連接旁赊,workerGroup 用于具體的處理EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try { //2.創(chuàng)建服務(wù)端啟動(dòng)引導(dǎo)/輔助類:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.給引導(dǎo)類配置兩大線程組,確定了線程模型 b.group(bossGroup, workerGroup) //......
Netty 服務(wù)端和客戶端的啟動(dòng)過(guò)程
服務(wù)端
// 1.bossGroup 用于接收連接,workerGroup 用于具體的處理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.創(chuàng)建服務(wù)端啟動(dòng)引導(dǎo)/輔助類:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.給引導(dǎo)類配置兩大線程組,確定了線程模型 b.group(bossGroup, workerGroup) // (非必備)打印日志 .handler(new LoggingHandler(LogLevel.INFO)) // 4.指定 IO 模型 .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); //5.可以自定義客戶端消息的業(yè)務(wù)處理邏輯 p.addLast(new HelloServerHandler()); } }); // 6.綁定端口,調(diào)用 sync 方法阻塞知道綁定完成 ChannelFuture f = b.bind(port).sync(); // 7.阻塞等待直到服務(wù)器Channel關(guān)閉(closeFuture()方法獲取Channel 的CloseFuture對(duì)象,然后調(diào)用sync()方法) f.channel().closeFuture().sync(); } finally { //8.優(yōu)雅關(guān)閉相關(guān)線程組資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }
簡(jiǎn)單解析一下服務(wù)端的創(chuàng)建過(guò)程具體是怎樣的:
1.首先你創(chuàng)建了兩個(gè) NioEventLoopGroup 對(duì)象實(shí)例:bossGroup 和 workerGroup椅野。
bossGroup : 用于處理客戶端的 TCP 連接請(qǐng)求终畅。workerGroup :負(fù)責(zé)每一條連接的具體讀寫(xiě)數(shù)據(jù)的處理邏輯,真正負(fù)責(zé) I/O 讀寫(xiě)操作竟闪,交由對(duì)應(yīng)的 Handler 處理离福。舉個(gè)例子:我們把公司的老板當(dāng)做 bossGroup,員工當(dāng)做 workerGroup瘫怜,bossGroup 在外面接完活之后术徊,扔給 workerGroup 去處理本刽。一般情況下我們會(huì)指定 bossGroup 的 線程數(shù)為 1(并發(fā)連接量不大的時(shí)候) 鲸湃,workGroup 的線程數(shù)量為?CPU 核心數(shù) *2?。另外子寓,根據(jù)源碼來(lái)看暗挑,使用 NioEventLoopGroup 類的無(wú)參構(gòu)造函數(shù)設(shè)置線程數(shù)量的默認(rèn)值就是?CPU 核心數(shù) *2?。
2.接下來(lái) 我們創(chuàng)建了一個(gè)服務(wù)端啟動(dòng)引導(dǎo)/輔助類:ServerBootstrap斜友,這個(gè)類將引導(dǎo)我們進(jìn)行服務(wù)端的啟動(dòng)工作炸裆。
3.通過(guò) .group() 方法給引導(dǎo)類 ServerBootstrap 配置兩大線程組,確定了線程模型鲜屏。
通過(guò)下面的代碼烹看,我們實(shí)際配置的是多線程模型,這個(gè)在上面提到過(guò)洛史。
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup();
4.通過(guò)channel()方法給引導(dǎo)類 ServerBootstrap指定了 IO 模型為NIO
NioServerSocketChannel :指定服務(wù)端的 IO 模型為 NIO惯殊,與 BIO 編程模型中的ServerSocket對(duì)應(yīng)NioSocketChannel : 指定客戶端的 IO 模型為 NIO, 與 BIO 編程模型中的Socket對(duì)應(yīng)5.通過(guò) .childHandler()給引導(dǎo)類創(chuàng)建一個(gè)ChannelInitializer 也殖,然后制定了服務(wù)端消息的業(yè)務(wù)處理邏輯 HelloServerHandler 對(duì)象6.調(diào)用 ServerBootstrap 類的 bind()方法綁定端口客戶端
//1.創(chuàng)建一個(gè) NioEventLoopGroup 對(duì)象實(shí)例 EventLoopGroup group = new NioEventLoopGroup(); try { //2.創(chuàng)建客戶端啟動(dòng)引導(dǎo)/輔助類:Bootstrap Bootstrap b = new Bootstrap(); //3.指定線程組 b.group(group) //4.指定 IO 模型 .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); // 5.這里可以自定義消息的業(yè)務(wù)處理邏輯 p.addLast(new HelloClientHandler(message)); } }); // 6.嘗試建立連接 ChannelFuture f = b.connect(host, port).sync(); // 7.等待連接關(guān)閉(阻塞土思,直到Channel關(guān)閉) f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); }
繼續(xù)分析一下客戶端的創(chuàng)建流程:
1.創(chuàng)建一個(gè) NioEventLoopGroup 對(duì)象實(shí)例
2.創(chuàng)建客戶端啟動(dòng)的引導(dǎo)類是 Bootstrap
3.通過(guò) .group() 方法給引導(dǎo)類 Bootstrap 配置一個(gè)線程組
4.通過(guò)channel()方法給引導(dǎo)類 Bootstrap指定了 IO 模型為NIO
5.通過(guò) .childHandler()給引導(dǎo)類創(chuàng)建一個(gè)ChannelInitializer ,然后制定了客戶端消息的業(yè)務(wù)處理邏輯 HelloClientHandler 對(duì)象
6.調(diào)用 Bootstrap 類的 connect()方法進(jìn)行連接忆嗜,這個(gè)方法需要指定兩個(gè)參數(shù):
inetHost : ip 地址inetPort : 端口號(hào)public ChannelFuture connect(String inetHost, int inetPort) { return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort)); } public ChannelFuture connect(SocketAddress remoteAddress) { ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); this.validate(); return this.doResolveAndConnect(remoteAddress, this.config.localAddress()); }
connect 方法返回的是一個(gè) Future 類型的對(duì)象
public interface ChannelFuture extends Future<Void> { ......}
也就是說(shuō)這個(gè)方是異步的己儒,我們通過(guò) addListener 方法可以監(jiān)聽(tīng)到連接是否成功,進(jìn)而打印出連接信息捆毫。具體做法很簡(jiǎn)單闪湾,只需要對(duì)代碼進(jìn)行以下改動(dòng):
ChannelFuture f = b.connect(host, port).addListener(future -> { if (future.isSuccess()) { System.out.println("連接成功!"); } else { System.err.println("連接失敗!"); }}).sync();
什么是 TCP 粘包/拆包?
我:TCP 粘包/拆包 就是你基于 TCP 發(fā)送數(shù)據(jù)的時(shí)候,出現(xiàn)了多個(gè)字符串“粘”在了一起或者一個(gè)字符串被“拆”開(kāi)的問(wèn)題绩卤。比如你多次發(fā)送:“你好,你真帥巴狙损合!哥哥!”娘纷,但是客戶端接收到的可能是下面這樣的:
1.使用 Netty 自帶的解碼器
LineBasedFrameDecoder?: 發(fā)送端發(fā)送數(shù)據(jù)包的時(shí)候嫁审,每個(gè)數(shù)據(jù)包之間以換行符作為分隔,LineBasedFrameDecoder 的工作原理是它依次遍歷 ByteBuf 中的可讀字節(jié)赖晶,判斷是否有換行符律适,然后進(jìn)行相應(yīng)的截取。DelimiterBasedFrameDecoder?: 可以自定義分隔符解碼器遏插,LineBasedFrameDecoder?實(shí)際上是一種特殊的 DelimiterBasedFrameDecoder 解碼器捂贿。FixedLengthFrameDecoder: 固定長(zhǎng)度解碼器,它能夠按照指定的長(zhǎng)度對(duì)消息進(jìn)行相應(yīng)的拆包胳嘲。LengthFieldBasedFrameDecoder:2.自定義序列化編解碼器
在 Java 中自帶的有實(shí)現(xiàn) Serializable 接口來(lái)實(shí)現(xiàn)序列化厂僧,但由于它性能、安全性等原因一般情況下是不會(huì)被使用到的了牛。
通常情況下颜屠,我們使用 Protostuff、Hessian2鹰祸、json 序列方式比較多甫窟,另外還有一些序列化性能非常好的序列化方式也是很好的選擇:
專門(mén)針對(duì) Java 語(yǔ)言的:Kryo,F(xiàn)ST 等等跨語(yǔ)言的:Protostuff(基于 protobuf 發(fā)展而來(lái))蛙婴,ProtoBuf粗井,Thrift,Avro街图,MsgPack 等等“由于篇幅問(wèn)題浇衬,這部分內(nèi)容會(huì)在后續(xù)的文章中詳細(xì)分析介紹~~~
Netty 長(zhǎng)連接、心跳機(jī)制餐济?
我們知道 TCP 在進(jìn)行讀寫(xiě)之前耘擂,server 與 client 之間必須提前建立一個(gè)連接。建立連接的過(guò)程颤介,需要我們常說(shuō)的三次握手梳星,釋放/關(guān)閉連接的話需要四次揮手。這個(gè)過(guò)程是比較消耗網(wǎng)絡(luò)資源并且有時(shí)間延遲的滚朵。
所謂冤灾,短連接說(shuō)的就是 server 端 與 client 端建立連接之后,讀寫(xiě)完成之后就關(guān)閉掉連接辕近,如果下一次再要互相發(fā)送消息韵吨,就要重新連接。短連接的有點(diǎn)很明顯移宅,就是管理和實(shí)現(xiàn)都比較簡(jiǎn)單归粉,缺點(diǎn)也很明顯椿疗,每一次的讀寫(xiě)都要建立連接必然會(huì)帶來(lái)大量網(wǎng)絡(luò)資源的消耗,并且連接的建立也需要耗費(fèi)時(shí)間糠悼。
長(zhǎng)連接說(shuō)的就是 client 向 server 雙方建立連接之后届榄,即使 client 與 server 完成一次讀寫(xiě),它們之間的連接并不會(huì)主動(dòng)關(guān)閉倔喂,后續(xù)的讀寫(xiě)操作會(huì)繼續(xù)使用這個(gè)連接铝条。長(zhǎng)連接的可以省去較多的 TCP 建立和關(guān)閉的操作,降低對(duì)網(wǎng)絡(luò)資源的依賴席噩,節(jié)約時(shí)間班缰。對(duì)于頻繁請(qǐng)求資源的客戶來(lái)說(shuō),非常適用長(zhǎng)連接悼枢。
為什么需要心跳機(jī)制埠忘?Netty 中心跳機(jī)制了解么?
在 TCP 保持長(zhǎng)連接的過(guò)程中馒索,可能會(huì)出現(xiàn)斷網(wǎng)等網(wǎng)絡(luò)異常出現(xiàn)莹妒,異常發(fā)生的時(shí)候, client 與 server 之間如果沒(méi)有交互的話双揪,它們是無(wú)法發(fā)現(xiàn)對(duì)方已經(jīng)掉線的动羽。為了解決這個(gè)問(wèn)題, 我們就需要引入?心跳機(jī)制包帚。
心跳機(jī)制的工作原理是: 在 client 與 server 之間在一定時(shí)間內(nèi)沒(méi)有數(shù)據(jù)交互時(shí), 即處于 idle 狀態(tài)時(shí), 客戶端或服務(wù)器就會(huì)發(fā)送一個(gè)特殊的數(shù)據(jù)包給對(duì)方, 當(dāng)接收方收到這個(gè)數(shù)據(jù)報(bào)文后, 也立即發(fā)送一個(gè)特殊的數(shù)據(jù)報(bào)文, 回應(yīng)發(fā)送方, 此即一個(gè) PING-PONG 交互渔期。所以, 當(dāng)某一端收到心跳消息后, 就知道了對(duì)方仍然在線, 這就確保 TCP 連接的有效性.
TCP 實(shí)際上自帶的就有長(zhǎng)連接選項(xiàng),本身是也有心跳包機(jī)制渴邦,也就是 TCP 的選項(xiàng):SO_KEEPALIVE疯趟。但是,TCP 協(xié)議層面的長(zhǎng)連接靈活性不夠谋梭。所以信峻,一般情況下我們都是在應(yīng)用層協(xié)議上實(shí)現(xiàn)自定義心跳機(jī)制的,也就是在 Netty 層面通過(guò)編碼實(shí)現(xiàn)瓮床。通過(guò) Netty 實(shí)現(xiàn)心跳機(jī)制的話盹舞,核心類是 IdleStateHandler 。
Netty 的零拷貝隘庄?
維基百科是這樣介紹零拷貝的:
“零復(fù)制(英語(yǔ):Zero-copy踢步;也譯零拷貝)技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),CPU 不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域丑掺。這種技術(shù)通常用于通過(guò)網(wǎng)絡(luò)傳輸文件時(shí)節(jié)省 CPU 周期和內(nèi)存帶寬获印。
在 OS 層面上的 Zero-copy 通常指避免在 用戶態(tài)(User-space) 與 內(nèi)核態(tài)(Kernel-space) 之間來(lái)回拷貝數(shù)據(jù)。而在 Netty 層面 街州,零拷貝主要體現(xiàn)在對(duì)于數(shù)據(jù)操作的優(yōu)化兼丰。
Netty 中的零拷貝體現(xiàn)在以下幾個(gè)方面:
使用 Netty 提供的 CompositeByteBuf 類, 可以將多個(gè)ByteBuf 合并為一個(gè)邏輯上的 ByteBuf, 避免了各個(gè) ByteBuf 之間的拷貝玻孟。
ByteBuf 支持 slice 操作, 因此可以將 ByteBuf 分解為多個(gè)共享同一個(gè)存儲(chǔ)區(qū)域的 ByteBuf, 避免了內(nèi)存的拷貝。
通過(guò) FileRegion 包裝的FileChannel.tranferTo 實(shí)現(xiàn)文件傳輸, 可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo) Channel, 避免了傳統(tǒng)通過(guò)循環(huán) write 方式導(dǎo)致的內(nèi)存拷貝問(wèn)題.(JavaNIO中的transferTo鳍征,操作系統(tǒng)中的sendFile)