netty 之 telnet HelloWorld 詳解

 

前言

Netty是 一個異步事件驅動的網絡應用程序框架, 用於快速開發可維護的高性能協議服務器和客戶端。html

etty是一個NIO客戶端服務器框架,能夠快速輕鬆地開發協議服務器和客戶端等網絡應用程序。它極大地簡化並簡化了TCP和UDP套接字服務器等網絡編程。java

「快速簡便」並不意味着最終的應用程序會受到可維護性或性能問題的影響。Netty通過精心設計,具備豐富的協議,如FTP,SMTP,HTTP以及各類二進制和基於文本的傳統協議。所以,Netty成功地找到了一種在不妥協的狀況下實現易於開發,性能,穩定性和靈活性的方法。linux

特徵

設計

  • 適用於各類傳輸類型的統一API - 阻塞和非阻塞套接字git

  • 基於靈活且可擴展的事件模型,能夠清晰地分離關注點github

  • 高度可定製的線程模型 - 單線程,一個或多個線程池,如SEDAredis

  • 真正的無鏈接數據報套接字支持(自3.1起)spring

使用方便

  • 詳細記錄的Javadoc,用戶指南和示例編程

  • 沒有其餘依賴項,JDK 5(Netty 3.x)或6(Netty 4.x)就足夠了windows

  • 注意:某些組件(如HTTP / 2)可能有更多要求。 有關更多信息,請參閱 「要求」頁面安全

性能

  • 吞吐量更高,延遲更低

  • 減小資源消耗

  • 最小化沒必要要的內存複製

安全

  • 完整的SSL / TLS和StartTLS支持

社區

  • 早發佈,常常發佈

  • 自2003年以來,做者一直在編寫相似的框架,他仍然以爲你的反饋很珍貴!

參考連接:  https://netty.io/

依賴工具

  • Maven

  • Git

  • JDK

  • IntelliJ IDEA

源碼拉取

從官方倉庫 https://github.com/netty/netty Fork 出屬於本身的倉庫。爲何要 Fork ?既然開始閱讀、調試源碼,咱們可能會寫一些註釋,有了本身的倉庫,能夠進行自由的提交。😈

使用 IntelliJ IDEAFork 出來的倉庫拉取代碼。

本文使用的 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 :

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編寫業務層的代碼,咱們須要繼承ChannelInboundHandlerAdapterSimpleChannelInboundHandler類,在這裏順便說下它們兩的區別吧。 繼承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/

相關文章
相關標籤/搜索