Netty1:初識Netty

五月的倉頡發表於2018-04-05

為什麼使用Netty

Netty是業界最流行的NIO框架之一,它的健壯性、功能、效能、可定製性、可擴充套件性在同類框架中都是首屈一指的,它已經得到了成百上千的商用專案的證明。對於為什麼使用Netty這個話題,我們先看一下使用原生的NIO有什麼缺點:

  • NIO的類庫和API繁雜,使用麻煩,需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等,這就像我們會使用Hibernate、MyBatis這些ORM框架而不會直接使用Connection、Statement一樣
  • 需要其他額外技能作為鋪墊,必須對多執行緒和網路程式設計非常熟悉才能寫出高質量的NIO程式
  • 可靠效能力補齊,工作量和難度都非常大,例如客戶端面臨斷線重連、網路閃斷、半包讀寫、失敗快取、網路擁塞、異常碼流等問題的處理
  • JDK NIO的BUG,例如著名的epoll bug,該問題會導致Selector空輪訓,最終導致CPU 100%

也正是因為有種種缺點,因此不建議使用原生的NIO而是建議使用一些比較成熟的NIO框架例如Netty、Mina,這一系列文章講的是Netty,Netty作為一款高效能NIO框架,其優點總結有:

  • API使用簡單、開發門檻低
  • 功能強大,預置了多種編碼解碼功能,支援多種主流協議
  • 定製能力強,可以通過ChannelHandler對通訊框架進行靈活擴充套件
  • 效能高,與業界其他主流NIO框架對比,Netty效能最優
  • 成熟、穩定,Netty修復了已經發現的所有JDK NIO的BUG,業務開發人員不需要再為NIO的BUG而煩惱
  • 社群活躍、版本迭代週期短,發現的BUG可以被及時修復,同時,更多的新功能會被加入
  • 經歷了大規模的商業應用考驗,質量得到驗證

正因為這些優點,Netty逐漸成為了Java NIO變成的首選框架。

 

Netty入門Demo

下面演示一下Netty的Demo(注:Demo來自Netty權威指南第三章),本文只寫程式碼與演示結果,不做講解,對Netty的使用基本講解放在下一篇文章中,循序漸進,先感性地認識Netty,再理性地認識Netty中的東西。

提一下,本文及之後的文章Netty基於5.0.0.Alpha1這個版本,貼一下我自己的Maven配置吧:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>

      <groupId>org.xrq.netty</groupId>
      <artifactId>netty-test</artifactId>
      <version>1.0.0</version>
      <packaging>jar</packaging>


      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>

      <dependencies>
        <dependency>
              <groupId>junit</groupId>
              <artifactId>junit</artifactId>
              <version>4.11</version>
              <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>5.0.0.Alpha1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
      </dependencies>
      
</project>

首先從服務端程式碼開始,定義一個TimeServer:

 1 public class TimeServer {
 2 
 3     public void bind(int port) throws Exception {
 4         // NIO執行緒組
 5         EventLoopGroup bossGroup = new NioEventLoopGroup();
 6         EventLoopGroup workerGroup = new NioEventLoopGroup();
 7         
 8         try {
 9             ServerBootstrap b = new ServerBootstrap();
10             b.group(bossGroup, workerGroup)
11                 .channel(NioServerSocketChannel.class)
12                 .option(ChannelOption.SO_BACKLOG, 1024)
13                 .childHandler(new ChildChannelHandler());
14             
15             // 繫結埠,同步等待成功
16             ChannelFuture f = b.bind(port).sync();
17             // 等待服務端監聽埠關閉
18             f.channel().closeFuture().sync();
19         } finally {
20             // 優雅退出,釋放執行緒池資源
21             bossGroup.shutdownGracefully();
22             workerGroup.shutdownGracefully();
23         }
24     }
25     
26     private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
27         @Override
28         protected void initChannel(SocketChannel arg0) throws Exception {
29             arg0.pipeline().addLast(new TimeServerHandler());
30         }
31     }
32     
33 }

TimeServerHandler這麼定義:

 1 public class TimeServerHandler extends ChannelHandlerAdapter {
 2 
 3     @Override
 4     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
 5         ByteBuf buf = (ByteBuf)msg;
 6         byte[] req = new byte[buf.readableBytes()];
 7         buf.readBytes(req);
 8         
 9         String body = new String(req, "UTF-8");
10         System.out.println("The time server receive order:" + body);
11         String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
12         
13         ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
14         ctx.write(resp);
15     }
16     
17     @Override
18     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
19         ctx.flush();
20     }
21     
22     @Override
23     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
24         ctx.close();
25     }
26     
27 }

即讀取來自客戶端的資料,如果是"QUERY TIME ORDER",則把當前時間寫到Channel中去。至此,Netty服務端程式碼已經開發完畢。接下來是Netty客戶端程式碼,首先還是TimeClient:

 1 public class TimeClient {
 2 
 3     public void connect(int port, String host) throws Exception {
 4         EventLoopGroup group = new NioEventLoopGroup();
 5         try {
 6             Bootstrap b = new Bootstrap();
 7             
 8             b.group(group)
 9                 .channel(NioSocketChannel.class)
10                 .option(ChannelOption.TCP_NODELAY, true)
11                 .handler(new ChannelInitializer<SocketChannel>() {
12                     protected void initChannel(SocketChannel ch) throws Exception {
13                         ch.pipeline().addLast(new TimeClientHandler());
14                     };
15                 });
16             
17             // 發起非同步連線操作
18             ChannelFuture f = b.connect(host, port).sync();
19             // 等待客戶端連線關閉
20             f.channel().closeFuture().sync();
21         } finally {
22             // 優雅退出,釋放NIO執行緒組
23             group.shutdownGracefully();
24         }
25     }
26     
27 }

同樣的,定義一個TimeClientHandler:

 1 public class TimeClientHandler extends ChannelHandlerAdapter {
 2 
 3     private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class);
 4     
 5     private final ByteBuf firstMessage;
 6     
 7     public TimeClientHandler() {
 8         byte[] req = "QUERY TIME ORDER".getBytes();
 9         firstMessage = Unpooled.buffer(req.length);
10         firstMessage.writeBytes(req);
11     }
12     
13     @Override
14     public void channelActive(ChannelHandlerContext ctx) throws Exception {
15         ctx.writeAndFlush(firstMessage);
16     }
17     
18     @Override
19     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
20         ByteBuf buf = (ByteBuf)msg;
21         byte[] req = new byte[buf.readableBytes()];
22         buf.readBytes(req);
23         
24         String body = new String(req, "UTF-8");
25         System.out.println("Now is:" + body);
26     }
27     
28     @Override
29     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
30         LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage());
31         ctx.close();
32     }
33     
34 }

客戶端的操作為列印來自服務端的資料,這樣,整個Netty Demo程式碼就寫完了,結構比較清楚,都是一個Server+一個Handler的模式,Handler用於處理讀取到的資訊。

 

執行Demo

上面寫完了Demo,接著寫一下測試程式碼,很簡單,分別執行bind方法和connect方法即可:

 1 public class CoreTest {
 2 
 3     @Test
 4     public void timeServerTest() throws Exception {
 5         new TimeServer().bind(8080);
 6     }
 7     
 8     @Test
 9     public void timeClientTest() throws Exception {
10         new TimeClient().connect(8080, "127.0.0.1");
11     }
12     
13 }

先執行timeServerTest讓服務端先啟動,再執行timeClientServer讓客戶端後啟動,執行結果服務端的列印為:

The time server receive order:QUERY TIME ORDER

結合程式碼可以看到,服務端讀取到了來自客戶端的資料,資料內容為"QUERY TIME ORDER",接著服務端取自己的時間,傳輸給客戶端,看一下客戶端的列印:

Now is:Thu Apr 05 21:07:39 CST 2018

列印了來自服務端的時間,這樣,利用Netty進行服務端+客戶端的相互通訊的Demo完成,有了這個Demo,對Netty有了感性上的認識,接著我們一點一點深入去學習Netty。