netty 之 telnet HelloWorld 詳解

穆書偉發表於2018-10-04

netty 之 telnet HelloWorld 詳解

前言

Netty是 一個非同步事件驅動的網路應用程式框架, 用於快速開發可維護的高效能協議伺服器和客戶端。

Netty是一個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年以來,作者一直在編寫類似的框架,他仍然覺得你的反饋很珍貴!

netty 之 telnet HelloWorld 詳解
參考連結: netty.io/

依賴工具

  • Maven
  • Git
  • JDK
  • IntelliJ IDEA

原始碼拉取

從官方倉庫 github.com/netty/netty Fork 出屬於自己的倉庫。為什麼要 Fork ?既然開始閱讀、除錯原始碼,我們可能會寫一些註釋,有了自己的倉庫,可以進行自由的提交。?

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

本文使用的 Netty 版本為 4.1.26.Final-SNAPSHOT

Maven Profile

開啟 IDEA 的 Maven Projects ,選擇對應的 Profiles 。如下圖所示:

netty 之 telnet HelloWorld 詳解

  • 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;
複製程式碼
  • 具體如下圖所示:

netty 之 telnet HelloWorld 詳解

解決方式如下:

cd common;
mvn clean compile;
複製程式碼
  • 跳轉到 common 模組中,編譯生成對應的類。為什麼可以通過編譯生成對應的類呢,原因參見 common 模組的 src/java/templates/io/netty/util/collection 目錄下的 .template 檔案。

在 Github 上,也有多個針對這個情況討論的 issue :

example 模組

example 模組裡,官網提供了多個 Netty 的使用示例。 本文以 telnet 包下來作為示例。哈哈哈,因為最簡單且完整。

netty 之 telnet HelloWorld 詳解

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 -- 服務端初始化類
複製程式碼

服務端

首先是編寫服務端的啟動類。

程式碼如下:

public final class Server {
    public  static void main(String[] args) throws Exception {
        //Configure the server
        //建立兩個EventLoopGroup物件
        //建立boss執行緒組 用於服務端接受客戶端的連線
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 建立 worker 執行緒組 用於進行 SocketChannel 的資料讀寫
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 建立 ServerBootstrap 物件
            ServerBootstrap b = new ServerBootstrap();
            //設定使用的EventLoopGroup
            b.group(bossGroup,workerGroup)
                //設定要被例項化的為 NioServerSocketChannel 類
                    .channel(NioServerSocketChannel.class)
                // 設定 NioServerSocketChannel 的處理器
                    .handler(new LoggingHandler(LogLevel.INFO))
                 // 設定連入服務端的 Client 的 SocketChannel 的處理器
                    .childHandler(new ServerInitializer());
            // 繫結埠,並同步等待成功,即啟動服務端
            ChannelFuture f = b.bind(8888);
            // 監聽服務端關閉,並阻塞等待
            f.channel().closeFuture().sync();
        } finally {
            // 優雅關閉兩個 EventLoopGroup 物件
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
複製程式碼
  • 第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該方法,進行新增相應的設定,傳輸協議設定,以及相應的業務實現類。 程式碼如下:

public class ServerInitializer extends ChannelInitializer<SocketChannel> {
    private static final StringDecoder DECODER = new StringDecoder();
    private static final StringEncoder ENCODER = new StringEncoder();

    private static final ServerHandler SERVER_HANDLER = new ServerHandler();


    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        // 新增幀限定符來防止粘包現象
        pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        // 解碼和編碼,應和客戶端一致
        pipeline.addLast(DECODER);
        pipeline.addLast(ENCODER);

        // 業務邏輯實現類
        pipeline.addLast(SERVER_HANDLER);
    }
}
複製程式碼

服務相關的設定的程式碼寫完之後,我們再來編寫主要的業務程式碼。 使用Netty編寫業務層的程式碼,我們需要繼承ChannelInboundHandlerAdapterSimpleChannelInboundHandler類,在這裡順便說下它們兩的區別吧。 繼承SimpleChannelInboundHandler類之後,會在接收到資料後會自動release掉資料佔用的Bytebuffer資源。並且繼承該類需要指定資料格式。 而繼承ChannelInboundHandlerAdapter則不會自動釋放,需要手動呼叫ReferenceCountUtil.release()等方法進行釋放。繼承該類不需要指定資料格式。 所以在這裡,個人推薦服務端繼承ChannelInboundHandlerAdapter,手動進行釋放,防止資料未處理完就自動釋放了。而且服務端可能有多個客戶端進行連線,並且每一個客戶端請求的資料格式都不一致,這時便可以進行相應的處理。 客戶端根據情況可以繼承SimpleChannelInboundHandler類。好處是直接指定好傳輸的資料格式,就不需要再進行格式的轉換了。

程式碼如下:

@Sharable
public class ServerHandler extends SimpleChannelInboundHandler<String> {
    /**
     * 建立連線時,傳送一條慶祝訊息
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 為新連線傳送慶祝
        ctx.write("Welcome to " + InetAddress.getLocalHost().getHostName() + "!\r\n");
        ctx.write("It is " + new Date() + " now.\r\n");
        ctx.flush();
    }

    //業務邏輯處理
    @Override
    public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception {
        // Generate and write a response.
        String response;
        boolean close = false;
        if (request.isEmpty()) {
            response = "Please type something.\r\n";
        } else if ("bye".equals(request.toLowerCase())) {
            response = "Have a good day!\r\n";
            close = true;
        } else {
            response = "Did you say '" + request + "'?\r\n";
        }

        ChannelFuture future = ctx.write(response);

        if (close) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    //異常處理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
複製程式碼

到這裡服務端相應的程式碼就編寫完畢了:rocket: 。

客戶端

客戶端這邊的程式碼和服務端的很多地方都類似,我就不再過多細說了,主要將一些不同的程式碼拿出來簡單的講述下。 首先是客戶端的主類,基本和服務端的差不多。 主要實現的程式碼邏輯如下:

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();
    }
}

複製程式碼

那麼到這裡客戶端的程式碼也編寫完畢了:rocket: 。

功能測試

首先啟動服務端,然後再啟動客戶端。

我們來看看結果是否如上述所說。

服務端輸出結果:

十月 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 詳解到這裡就結束了。

netty 之 telnet HelloWorld 詳解專案工程地址:
github.com/sanshengshu…

對了,也有不使用springBoot整合的Netty專案工程地址:
github.com/sanshengshu…

原創不易,如果感覺不錯,希望給個推薦!您的支援是我寫作的最大動力!

版權宣告:
作者:穆書偉
部落格園出處:www.cnblogs.com/sanshengshu…
github出處:github.com/sanshengshu…
個人部落格出處:sanshengshui.github.io/

相關文章