前言
Netty是 一個異步事件驅動的網絡應用程序框架, 用於快速開發可維護的高性能協議服務器和客戶端。
etty是一個NIO客戶端服務器框架,可以快速輕松地開發協議服務器和客戶端等網絡應用程序。它極大地簡化並簡化了TCP和UDP套接字服務器等網絡編程。
“快速簡便”並不意味着最終的應用程序會受到可維護性或性能問題的影響。Netty經過精心設計,具有豐富的協議,如FTP,SMTP,HTTP以及各種二進制和基於文本的傳統協議。因此,Netty成功地找到了一種在不妥協的情況下實現易於開發,性能,穩定性和靈活性的方法。
特征
設計
-
適用於各種傳輸類型的統一API - 阻塞和非阻塞套接字
-
基於靈活且可擴展的事件模型,可以清晰地分離關注點
-
高度可定制的線程模型 - 單線程,一個或多個線程池,如SEDA
-
真正的無連接數據報套接字支持(自3.1起)
使用方便
-
詳細記錄的Javadoc,用戶指南和示例
-
沒有其他依賴項,JDK 5(Netty 3.x)或6(Netty 4.x)就足夠了
-
注意:某些組件(如HTTP / 2)可能有更多要求。 有關更多信息,請參閱 “要求”頁面。
性能
-
吞吐量更高,延遲更低
-
減少資源消耗
-
最小化不必要的內存復制
安全
-
完整的SSL / TLS和StartTLS支持
社區
-
早發布,經常發布
-
自2003年以來,作者一直在編寫類似的框架,他仍然覺得你的反饋很珍貴!
參考鏈接: https://netty.io/
-
Maven
-
Git
-
JDK
-
IntelliJ IDEA
源碼拉取
從官方倉庫 https://github.com/netty/netty Fork
出屬於自己的倉庫。為什么要 Fork
?既然開始閱讀、調試源碼,我們可能會寫一些注釋,有了自己的倉庫,可以進行自由的提交。😈
使用 IntelliJ IDEA
從 Fork
出來的倉庫拉取代碼。
本文使用的 Netty 版本為 4.1.26.Final-SNAPSHOT
。
Maven Profile
打開 IDEA 的 Maven Projects ,選擇對應的 Profiles 。如下圖所示:
-
jdk8
:筆者使用的 JDK 版本是 8 ,所以勾選了jdk8
。如果錯誤的選擇,可能會報如下錯誤:java.lang.NoSuchMethodError: java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer
-
linux
: 選擇對應的系統版本。😈 筆者手頭沒有 windows 的電腦,所以不知道該怎么選。
修改完成后,點擊左上角的【刷新】按鈕,進行依賴下載,耐心等待...
解決依賴報錯
在 codec-redis
模塊中,類 FixedRedisMessagePool 會報如下類不存在的問題:
import io.netty.util.collection.LongObjectHashMap; import io.netty.util.collection.LongObjectMap;
-
具體如下圖所示:
解決方式如下:
cd common;
mvn clean compile;
-
跳轉到
common
模塊中,編譯生成對應的類。為什么可以通過編譯生成對應的類呢,原因參見common
模塊的src/java/templates/io/netty/util/collection
目錄下的.template
文件。
在 Github 上,也有多個針對這個情況討論的 issue :
-
《Can not find class io.netty.util.collection.LongObjectHashMap in 4.1.8.final》
-
《io.netty.util.collection.LongObjectHashMap not found at branch 4.1》
example 模塊
在 example
模塊里,官網提供了多個 Netty 的使用示例。 本文以 telnet
包下來作為示例。哈哈哈,因為最簡單且完整。
netty-helloworld
說明: 如果想直接獲取工程那么可以直接跳到底部,通過鏈接下載工程代碼。
開發准備
環境要求
-
JDK: 1.8
-
Netty: 4.0或以上
如果對Netty不熟的話,可以看看之前寫的一些文章。大神請無視☺。
首先還是Maven的相關依賴:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <netty-all.version>4.1.6.Final</netty-all.version> </properties> <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>${netty-all.version}</version> </dependency> </dependencies>
添加了相應的maven依賴之后,配置文件這塊暫時沒有什么可以添加的,因為暫時就一個監聽的端口而已。
代碼編寫
代碼模塊主要分為服務端和客戶端。 主要實現的業務邏輯: 服務端啟動成功之后,客戶端也啟動成功,這時服務端會發送一條信息給客戶端。客戶端或者telnet發送一條信息到服務端,服務端會根據邏輯回復客戶端一條客戶端,當客戶端或者telent發送bye
給服務端,服務端和客戶端斷開鏈接。
項目結構
netty-helloworld
├── client
├── Client.class -- 客戶端啟動類
├── ClientHandler.class -- 客戶端邏輯處理類
├── ClientHandler.class -- 客戶端初始化類
├── server
├── Server.class -- 服務端啟動類
├── ServerHandler -- 服務端邏輯處理類
├── ServerInitializer -- 服務端初始化類
服務端
首先是編寫服務端的啟動類。
代碼如下:
1 public final class Server { 2 public static void main(String[] args) throws Exception { 3 //Configure the server 4 //創建兩個EventLoopGroup對象 5 //創建boss線程組 用於服務端接受客戶端的連接 6 EventLoopGroup bossGroup = new NioEventLoopGroup(1); 7 // 創建 worker 線程組 用於進行 SocketChannel 的數據讀寫 8 EventLoopGroup workerGroup = new NioEventLoopGroup(); 9 try { 10 // 創建 ServerBootstrap 對象 11 ServerBootstrap b = new ServerBootstrap(); 12 //設置使用的EventLoopGroup 13 b.group(bossGroup,workerGroup) 14 //設置要被實例化的為 NioServerSocketChannel 類 15 .channel(NioServerSocketChannel.class) 16 // 設置 NioServerSocketChannel 的處理器 17 .handler(new LoggingHandler(LogLevel.INFO)) 18 // 設置連入服務端的 Client 的 SocketChannel 的處理器 19 .childHandler(new ServerInitializer()); 20 // 綁定端口,並同步等待成功,即啟動服務端 21 ChannelFuture f = b.bind(8888); 22 // 監聽服務端關閉,並阻塞等待 23 f.channel().closeFuture().sync(); 24 } finally { 25 // 優雅關閉兩個 EventLoopGroup 對象 26 bossGroup.shutdownGracefully(); 27 workerGroup.shutdownGracefully(); 28 } 29 } 30 }
-
第6到8行: 創建兩個EventLoopGroup對象。
-
boss 線程組: 用於服務端接受客戶端的連接。
-
worker 線程組: 用於進行客戶端的SocketChannel的數據讀寫。
-
關於為什么是兩個EventLoopGroup對象,請了解文章NIO系列之Reactro模型。
-
-
第11行: 創建 ServerBootstrap 對象,用於設置服務端的啟動配置。
-
第13行: 調用
#group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
方法,設置使用的 EventLoopGroup 。 -
第15行: 調用
#channel(Class<? extends C> channelClass)
方法,設置要被實例化的 Channel 為 NioServerSocketChannel 類。在下文中,我們會看到該 Channel 內嵌了java.nio.channels.ServerSocketChannel
對象。是不是很熟悉 😈 ? -
第17行: 調用
#handler(ChannelHandler handler)
方法,設置 NioServerSocketChannel 的處理器。在本示例中,使用了io.netty.handler.logging.LoggingHandler
類,用於打印服務端的每個事件。 -
第19行: 調用
#childHandler(ChannelHandler handler)
方法,設置連入服務端的 Client 的 SocketChannel 的處理器。在本實例中,使用 ServerInitializer() 來初始化連入服務端的 Client 的 SocketChannel 的處理器。
-
-
第21行: 先調用
#bind(int port)
方法,綁定端口,后調用ChannelFuture#sync()
方法,阻塞等待成功。這個過程,就是“啟動服務端”。
-
第23行: 先調用
#closeFuture()
方法,監聽服務器關閉,后調用ChannelFuture#sync()
方法,阻塞等待成功。😈 注意,此處不是關閉服務器,而是“監聽”關閉。
-
第26到27行: 執行到此處,說明服務端已經關閉,所以調用
EventLoopGroup#shutdownGracefully()
方法,分別關閉兩個 EventLoopGroup 對象。
服務端主類編寫完畢之后,我們再來設置下相應的過濾條件。 這里需要繼承Netty中ChannelInitializer類,然后重寫initChannel該方法,進行添加相應的設置,傳輸協議設置,以及相應的業務實現類。 代碼如下:
1 public class ServerInitializer extends ChannelInitializer<SocketChannel> { 2 private static final StringDecoder DECODER = new StringDecoder(); 3 private static final StringEncoder ENCODER = new StringEncoder(); 4 5 private static final ServerHandler SERVER_HANDLER = new ServerHandler(); 6 7 8 @Override 9 public void initChannel(SocketChannel ch) throws Exception { 10 ChannelPipeline pipeline = ch.pipeline(); 11 12 // 添加幀限定符來防止粘包現象 13 pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); 14 // 解碼和編碼,應和客戶端一致 15 pipeline.addLast(DECODER); 16 pipeline.addLast(ENCODER); 17 18 // 業務邏輯實現類 19 pipeline.addLast(SERVER_HANDLER); 20 } 21 }
服務相關的設置的代碼寫完之后,我們再來編寫主要的業務代碼。 使用Netty編寫業務層的代碼,我們需要繼承ChannelInboundHandlerAdapter 或SimpleChannelInboundHandler類,在這里順便說下它們兩的區別吧。 繼承SimpleChannelInboundHandler類之后,會在接收到數據后會自動release掉數據占用的Bytebuffer資源。並且繼承該類需要指定數據格式。 而繼承ChannelInboundHandlerAdapter則不會自動釋放,需要手動調用ReferenceCountUtil.release()等方法進行釋放。繼承該類不需要指定數據格式。 所以在這里,個人推薦服務端繼承ChannelInboundHandlerAdapter,手動進行釋放,防止數據未處理完就自動釋放了。而且服務端可能有多個客戶端進行連接,並且每一個客戶端請求的數據格式都不一致,這時便可以進行相應的處理。 客戶端根據情況可以繼承SimpleChannelInboundHandler類。好處是直接指定好傳輸的數據格式,就不需要再進行格式的轉換了。
代碼如下:
1 @Sharable 2 public class ServerHandler extends SimpleChannelInboundHandler<String> { 3 /** 4 * 建立連接時,發送一條慶祝消息 5 */ 6 @Override 7 public void channelActive(ChannelHandlerContext ctx) throws Exception { 8 // 為新連接發送慶祝 9 ctx.write("Welcome to " + InetAddress.getLocalHost().getHostName() + "!\r\n"); 10 ctx.write("It is " + new Date() + " now.\r\n"); 11 ctx.flush(); 12 } 13 14 //業務邏輯處理 15 @Override 16 public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception { 17 // Generate and write a response. 18 String response; 19 boolean close = false; 20 if (request.isEmpty()) { 21 response = "Please type something.\r\n"; 22 } else if ("bye".equals(request.toLowerCase())) { 23 response = "Have a good day!\r\n"; 24 close = true; 25 } else { 26 response = "Did you say '" + request + "'?\r\n"; 27 } 28 29 ChannelFuture future = ctx.write(response); 30 31 if (close) { 32 future.addListener(ChannelFutureListener.CLOSE); 33 } 34 } 35 36 @Override 37 public void channelReadComplete(ChannelHandlerContext ctx) { 38 ctx.flush(); 39 } 40 41 //異常處理 42 @Override 43 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 44 cause.printStackTrace(); 45 ctx.close(); 46 } 47 }
到這里服務端相應的代碼就編寫完畢了🚀 。
客戶端
客戶端這邊的代碼和服務端的很多地方都類似,我就不再過多細說了,主要將一些不同的代碼拿出來簡單的講述下。 首先是客戶端的主類,基本和服務端的差不多。 主要實現的代碼邏輯如下:
public static void main(String[] args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .handler(new ClientInitializer()); Channel ch = b.connect("127.0.0.1",8888).sync().channel(); ChannelFuture lastWriteFuture = null; BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); for (;;) { String line = in.readLine(); if (line == null) { break; } // Sends the received line to the server. lastWriteFuture = ch.writeAndFlush(line + "\r\n"); // If user typed the 'bye' command, wait until the server closes // the connection. if ("bye".equals(line.toLowerCase())) { ch.closeFuture().sync(); break; } } // Wait until all messages are flushed before closing the channel. if (lastWriteFuture != null) { lastWriteFuture.sync(); } } finally { group.shutdownGracefully(); } }
客戶端過濾其這塊基本和服務端一致。不過需要注意的是,傳輸協議、編碼和解碼應該一致。
代碼如下:
public class ClientInitializer extends ChannelInitializer<SocketChannel> { private static final StringDecoder DECODER = new StringDecoder(); private static final StringEncoder ENCODER = new StringEncoder(); private static final ClientHandler CLIENT_HANDLER = new ClientHandler(); @Override public void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())); pipeline.addLast(DECODER); pipeline.addLast(ENCODER); pipeline.addLast(CLIENT_HANDLER); } }
客戶端的業務代碼邏輯。
主要時打印讀取到的信息。
這里有個注解, 該注解Sharable主要是為了多個handler可以被多個channel安全地共享,也就是保證線程安全。 廢話就不多說了,代碼如下:
@Sharable public class ClientHandler extends SimpleChannelInboundHandler<String> { //打印讀取到的數據 @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.err.println(msg); } //異常數據捕獲 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
那么到這里客戶端的代碼也編寫完畢了🚀 。
功能測試
首先啟動服務端,然后再啟動客戶端。
我們來看看結果是否如上述所說。
服務端輸出結果:
十月 02, 2018 10:03:00 上午 io.netty.handler.logging.LoggingHandler channelRegistered 信息: [id: 0x1c7da838] REGISTERED 十月 02, 2018 10:03:00 上午 io.netty.handler.logging.LoggingHandler bind 信息: [id: 0x1c7da838] BIND: 0.0.0.0/0.0.0.0:8888 十月 02, 2018 10:03:00 上午 io.netty.handler.logging.LoggingHandler channelActive 信息: [id: 0x1c7da838, L:/0:0:0:0:0:0:0:0:8888] ACTIVE 十月 02, 2018 10:03:51 上午 io.netty.handler.logging.LoggingHandler channelRead 信息: [id: 0x1c7da838, L:/0:0:0:0:0:0:0:0:8888] RECEIVED: [id: 0xc033aea8, L:/127.0.0.1:8888 - R:/127.0.0.1:58178]
客戶端輸入結果:
Connected to the target VM, address: '127.0.0.1:37175', transport: 'socket' Welcome to james! It is Tue Oct 02 10:03:51 CST 2018 now. yes Did you say 'yes'? hello world Did you say 'hello world'? bye Have a good day! Disconnected from the target VM, address: '127.0.0.1:37175', transport: 'socket' Process finished with exit code 0
telnet客戶端 和服務端交互結果如下:
通過打印信息可以看出如上述所說。
其它
關於netty 之 telnet HelloWorld 詳解到這里就結束了。
netty 之 telnet HelloWorld 詳解項目工程地址: https://github.com/sanshengshui/netty-learning-example/tree/master/netty-helloworld
對了,也有不使用springBoot整合的Netty項目工程地址: https://github.com/sanshengshui/netty-learning-example
原創不易,如果感覺不錯,希望給個推薦!您的支持是我寫作的最大動力!
版權聲明: 作者:穆書偉 博客園出處:https://www.cnblogs.com/sanshengshui github出處:https://github.com/sanshengshui 個人博客出處:https://sanshengshui.github.io/