[自己做個游戲服務器二] 游戲服務器的基石-Netty全解析,有例子,多圖解釋
Netty的大名我想做java 的基本都知道,因為他實在太出名了,現在很多著名的軟件都是使用netty作為通訊基礎,今天就聊聊Netty,希望能講清楚,如果懶得看理論,可以直接拉到后面看Hello world。把代碼抄下來,運行一下看看。
1、Netty 是什么
Netty是一個高性能、異步事件驅動的NIO框架,基于JAVA NIO提供的API實現。它提供了對TCP、UDP和文件傳輸的支持
作為一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。
作為當前最流行的NIO框架,Netty在互聯網領域、大數據分布式計算領域、游戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基于Netty的NIO框架構建。
Netty的官網 :https://netty.io/
2、Netty的優點
Netty的缺點就不說了,Netty的優點有很多:
統一的 API,支持多種傳輸類型,阻塞和非阻塞的。
功能強大,內置了多種解碼編碼器,支持多種協議,比如上圖中的右側黃色區域,通用的文本,二進制協議,google protobuf等。
性能高,對比其他主流的NIO框架,Netty的性能最優。
社區活躍,發現BUG會及時修復,迭代版本周期短,不斷加入新的功能。
簡單而強大的線程模型。
自帶編解碼器解決 TCP 粘包/拆包問題。
自帶各種協議棧,比如 SSL 。
比直接使用 Java 核心 API 有更高的吞吐量、更低的延遲、更低的資源消耗和更少的內存復制,Zero-Copy Byte buffer。
安全性不錯,有完整的 SSL/TLS 以及 StartTLS 支持。
成熟穩定,經歷了大型項目的使用和考驗,而且很多開源項目都使用到了 Netty, 比如我們經常接觸的 Dubbo、RocketMQ ,Elasticsearch等等。
3、核心組件
3.1 Netty的線程模型
Netty的線程模型是比較重要的,理解了Netty的線程模型才能很好地使用Netty,Netty常見的線程模型有三種:
1.單線程模型
單線程模型,是指所有的 I/O 操作都在同一個 NIO 線程上面完成的,此時NIO線程職責包括:接收新建連接請求、讀寫操作等,在游戲開發中不會使用,也不合理,不展開。
2.Reactor多線程模型
第一種不合理,升級一下,一個接受連接的線程, 所有的 I/O 操作都在同一個 NIO 線程池上面完成,這種線程模型可以滿足大部分情況,但是如果在連接的時候需要做一些驗證,就會阻塞線程。性能會出問題,服務器
3.Reactor主從多線程模型
服務端用于接收客戶端連接的不再是一個單獨的 NIO 線程,而是一個獨立的 NIO 線程池。Acceptor 接收到客戶端 TCP連接請求并處理完成后(可能包含接入認證等),將新創建的 SocketChannel注 冊 到 I/O 線 程 池(sub reactor 線 程 池)的某個I/O線程上, 由它負責SocketChannel 的讀寫和編解碼工作。Acceptor 線程池僅僅用于客戶端的登錄、握手和安全認證,一旦鏈路建立成功,就將鏈路注冊到后端 subReactor 線程池的 I/O 線程上,由 I/O 線程負責后續的 I/O 操作。
這也是在游戲開發中最常用的線程模型,需要掌握,下面這張圖將核心技術都做了展示
3.2 EventLoopGroup
NioEventLoopGroup 核心實際上就是個線程池,是為了處理IO事件而存在的一個線程池。
一個 EventLoopGroup 包含一個或者多個 EventLoop;一個 EventLoop 在它的生命周期內只和一個 Thread 綁定;所有有 EnventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理;一個 Channel 在它的生命周期內只注冊于一個 EventLoop;每一個 EventLoop 負責處理一個或多個 Channel;
我們實現服務端的時候,一般會初始化兩個線程組:
bossGroup :接收連接。
workerGroup :負責具體的處理,交由對應的 Handler 處理
BossEventLoop 只負責處理連接,開銷非常小,連接到來,馬上將 SocketChannel 轉發給 WorkerEventLoopGroup,WorkerEventLoopGroup 會由 next 選擇其中一個 EventLoop 來將這 個SocketChannel 注冊到其維護的 Selector 并對其后續的 IO 事件進行處理。
注:默認的線程數量 是當前cpu 數量 *2
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup { private static final InternalLogger logger = InternalLoggerFactory.getInstance(MultithreadEventLoopGroup.class); private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); }
3.3 Channel
Channel 表示一個和客戶端建立的連接,相當于電話建立了連接,Channel是雙向的通道。
通道(Channel)是雙向的,可讀可寫。在 Java NIO 中,Buffer 是一個頂層接口,它的常用子類有:
FileChannel:用于文件讀寫
DatagramChannel:用于 UDP 數據包收發
ServerSocketChannel:用于服務端 TCP 數據包收發
SocketChannel:用于客戶端 TCP 數據包收發
游戲中常用的通道類型有以下:
NioSocketChannel:異步非阻塞的客戶端 TCP Socket 連接。
NioServerSocketChannel:異步非阻塞的服務器端 TCP Socket 連接。
常用的就是這兩個通道類型,因為是異步非阻塞的。所以是首選。
3.4 ?option()與childOption()
首先說一下這兩個的區別。
option()設置的是服務端用于接收進來的連接,也就是boosGroup線程。
childOption()是提供給父管道接收到的連接,也就是workerGroup線程。
搞清楚了之后,我們看一下常用的一些設置有哪些:
SocketChannel參數,也就是childOption()常用的參數:
SO_RCVBUF Socket參數,TCP數據接收緩沖區大小。TCP_NODELAY TCP參數,立即發送數據,默認值為Ture。SO_KEEPALIVE Socket參數,連接保活,默認值為False。啟用該功能時,TCP會主動探測空閑連接的有效性。
ServerSocketChannel參數,也就是option()常用參數:
SO_BACKLOG Socket參數,服務端接受連接的隊列長度,如果隊列已滿,客戶端連接將被拒絕。默認值,Windows為200,其他為128。
3.5 ?inbound 和 outbound
inbound ?表示 消息進入到服務器的路徑,可以理解為輸入
outBound 表示 消息輸出到客戶端的路徑,可以理解為輸出
ChannelPipeline p = ...; p.addLast("1", new InboundHandlerA()); p.addLast("2", new InboundHandlerB()); p.addLast("3", new OutboundHandlerA()); p.addLast("4", new OutboundHandlerB()); p.addLast("5", new InboundOutboundHandlerX());
當一個輸入事件來了之后,事件處理器的調用順序為1,2,5
當一個輸出事件來了之后,事件處理器的處理順序為5,4,3。(注意輸出事件的處理器發揮作用的順序與定義的順序是相反的)
可以理解為對handler 進行壓棧操作。
ChannelInboundHandlerAdapter處理器常用的事件有:
注冊事件 fireChannelRegistered。
連接建立事件 fireChannelActive。
讀事件和讀完成事件 fireChannelRead、fireChannelReadComplete。
異常通知事件 fireExceptionCaught。
用戶自定義事件 fireUserEventTriggered。
Channel 可寫狀態變化事件 fireChannelWritabilityChanged。
連接關閉事件 fireChannelInactive。
ChannelOutboundHandler處理器常用的事件有:
端口綁定 bind。
連接服務端 connect。
寫事件 write。
刷新時間 flush。
讀事件 read。
主動斷開連接 disconnect。
關閉 channel 事件 close。
還有一個類似的handler(),主要用于裝配parent通道,也就是bossGroup線程。一般情況下,都用不上這個方法。
3.6 ByteBuf
ByteBuff有三種類型:
堆內存緩沖區(HeapByteBuf)
數據存儲在堆中,可以認為就是我們常用的內存緩沖區
直接內存緩沖區(DirectByteBuf)
數據存儲在內核中。由于數據本身就存儲在內核中,因此使用網卡傳輸數據的時候直接可以傳輸,不需要多余的拷貝。因此,這也被稱為零拷貝。
從硬盤中讀取數據使用網卡發送出去,一般步驟如下:
數據從磁盤讀取到內核的read buffer數據從內核緩沖區拷貝到用戶緩沖區數據從用戶緩沖區拷貝到內核的socket buffer
數據從內核的socket buffer拷貝到網卡接口(硬件)的緩沖區使用內存緩沖區只需要兩步:
調用transferTo,數據從文件由DMA引擎拷貝到內核read buffer接著DMA從內核read buffer將數據拷貝到網卡接口buffer
復合緩沖區(CompositeByteBuf)
復合緩沖區可以將多個ByteBuff組合
注:即內核功能模塊運行在內核空間,而應用程序運行在用戶空間
ByteBuf有讀readerIndex和寫writerIndex兩個指針,用來標記“可讀”、“可寫”、“可丟棄”的字節
調用write*方法寫入數據后,寫指針將會向后移動
調用read*方法讀取數據后,讀指針將會向后移動
寫入數據或讀取數據時會檢查是否有足夠多的空間可以寫入和是否有數據可以讀取
寫入數據之前,會進行容量檢查,當剩余可寫的容量小于需要寫入的容量時,需要執行擴容操作
擴容時有一個4MB的閾值,需要擴容的容量小于閾值或大于閾值所對應的擴容邏輯不同
clear等修改讀寫指針的方法,只會更改讀寫指針位置的值,并不會影響ByteBuf中已有的內容
setZero等修改字節值的方法,只會修改對應字節的值,不會影響讀寫指針的值以及字節的可讀寫狀態
Netty又為我們提供了兩個工具類:Pooled、Unpooled,分類用來分配池化的和未池化的ByteBuf,進一步簡化了創建ByteBuf的步驟,只需要調用這兩個工具類的靜態方法即可。
3.7 使用 Netty 自帶的解碼器
LineBasedFrameDecoder : 發送端發送數據包的時候,每個數據包之間以換行符作為分隔,LineBasedFrameDecoder 的工作原理是它依次遍歷 ByteBuf 中的可讀字節,判斷是否有換行符,然后進行相應的截取。
DelimiterBasedFrameDecoder : 可以自定義分隔符解碼器,LineBasedFrameDecoder 實際上是一種特殊的 DelimiterBasedFrameDecoder 解碼器。
FixedLengthFrameDecoder: 固定長度解碼器,它能夠按照指定的長度對消息進行相應的拆包,每個數據包的長度都是固定的。
LengthFieldBasedFrameDecoder:這個是后面服務器將要使用的解碼器,下期會有實例
3.8 Netty 版本
netty5 中使用了 ForkJoinPool,增加了代碼的復雜度,但是對性能的改善卻不明顯
多個分支的代碼同步工作量很大
在發布版本之前,還有更多問題需要調查一下,比如是否應該廢棄 exceptionCaught, 是否暴露EventExecutorChooser等等。
當前最新版本:4.1.68.Final
4、Hello World
4.1 官方的demo
官方的demo下載源碼就可以在example下看到所有的demo,
gitHub 地址:https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example
4.2 idea 建立maven項目
我這里使用了idea,所以下面的截圖也是用Idea。
4.2.1 File ->New 進入下面的界面
4.2.2 ?next 如下圖填入自己的信息
4.2.3 等待一下,知道maven加載項目完成,如下結構
4.3 服務端代碼
為了盡可能的僅僅展示Netty的代碼,去掉那些花里胡哨的技術,只是簡單的程序
package com.xiangcai; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; /** * 服務端代碼 * @author 香菜 */ public class GameServer { /** * 啟動 */ public static void start() throws InterruptedException { EventLoopGroup boss = new NioEventLoopGroup(1); EventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) //服務端可連接隊列數,對應TCP/IP協議listen函數中backlog參數 .option(ChannelOption.SO_BACKLOG, 1024) //設置TCP長連接,一般如果兩個小時內沒有數據的通信時,TCP會自動發送一個活動探測數據報文 .childOption(ChannelOption.SO_KEEPALIVE, true) //將小的數據包包裝成更大的幀進行傳送,提高網絡的負載,即TCP延遲傳輸 .childOption(ChannelOption.TCP_NODELAY, true) .childHandler(new NettyServerHandlerInitializer()); ChannelFuture channelFuture = serverBootstrap.bind(8088).sync(); System.out.println("服務器啟動了"); channelFuture.channel().closeFuture().sync(); } finally { // 關閉線程 boss.shutdownGracefully(); worker.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { start(); } }
下面看下Channel的初始化代碼:
使用了2個解碼器
package com.xiangcai; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; /** * 服務端代碼 * @author 香菜 */ public class NettyServerHandlerInitializer extends ChannelInitializer
下面是業務的代碼展示:
這里只是展示了簡單的收發消息
package com.xiangcai; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; /** * 服務端代碼 * @author 香菜 */ public class NettyServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); } /** * 超時處理 如果5秒沒有接受客戶端的心跳,就觸發; 如果超過兩次,則直接關閉; */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception { if (obj instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) obj; if (IdleState.READER_IDLE.equals(event.state())) { // 如果讀通道處于空閑狀態,說明沒有接收到心跳命令 System.out.println("已經5秒沒有接收到客戶端的信息了"); } } else { super.userEventTriggered(ctx, obj); } } /** * 業務邏輯處理 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("服務端收到信息:" + msg); String respStr = "收到了 " + System.getProperty("line.separator"); ByteBuf resp = Unpooled.copiedBuffer(respStr.getBytes()); ctx.writeAndFlush(resp); } /** * 異常處理 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
4.4 客戶端代碼
客戶端啟動代碼
package com.xiangcai; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import org.junit.Test; /** * 客戶端代碼 * @author 香菜 */ public class TestClient { public static void clientStart() { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer
客戶端業務處理:
也是簡單的收發消息
package com.xiangcai; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import java.nio.charset.StandardCharsets; /** * 客戶端代碼 * @author 香菜 */ public class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) { System.out.println("建立連接"); byte[] bytes = ("連接上了,開始說話" + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8); ByteBuf message = Unpooled.buffer(bytes.length); message.writeBytes(bytes); ctx.writeAndFlush(message); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String body = (String) msg; System.out.println("收到信息 " + body); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { System.out.println("發生異常"); ctx.close(); } }
5、總結
現在越來越多的游戲公司使用Java進行開發,Netty是繞不開的網絡基礎,搞懂Netty 很重要,Netty也很簡單,只要記得線程模型,編解碼,其他的都是細節問題,在開發的過程中進行學習也不遲,希望這篇文章能幫助你理解,如果你有疑問可以留言給我,一起學習交流。
完整項目源碼-:https://download.csdn.net/download/perfect2011/29665428
最后一張圖結尾
寫的好累,希望大佬們能點個贊,點個在看。
5G游戲 Java 任務調度 容器 游戲開發
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。