Netty 入門案例
1. 前言
本節主要是使用 Netty 來開發服務端和客戶端,Netty 的開發模式基本上都是主啟動類 + 自定義業務 Handler,Netty 是基于責任鏈的模式來管理自定義部分的 Handler,本節帶大家感受一下 Netty 的開發。
需求: 本節主要通過 Netty 來實現我們的第一個 Demo,主要功能是分別建立兩個項目(客戶端和服務端),客戶端向服務端發送 Hello World
,服務端接受到數據之后打印到控制臺,并且給客戶端響應。
2. 環境搭建
第一步: 使用 Maven 構建工程,項目結構如下:
第二步: netty-demo-client
和 netty-demo-server
兩個工程的 pom.xml 導入 Netty 坐標,Netty 主流有三個版本,分別是 3.x、4.x、5.x,一般主流都是使用 4.x 版本。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
第三步: netty-demo-client 工程相關類
建立兩個類,分別是客戶端啟動類 NettyClient.java
和業務處理類 NettyClientHandler.java
。
第四步: netty-demo-server 工程相關類
建立兩個類,分別是服務端啟動類 NettyServer.java
和業務處理類 NettyServerHandler.java
3. 核心流程
客戶端和服務端通信流程圖如下圖所示:
核心步驟說明:
- 在 NettyClientHandler 的 channelActive () 方法往服務端發送消息;
- 在 NettyServerHandler 的 channelRead () 方法接受消息,并且響應消息給客戶端;
- 在 NettyClientHandler 的 channelRead () 方法接受服務端的響應消息。
4. 如何自定義 Handler
在 Netty 的開發當中,最核心就是自定義 Handler,通常根據不同的業務定義不同的 Handler。自定義 Handler 一般分為三個核心步驟:
- 需要繼承
ChannelInboundHandlerAdapter
類;
實例:
public class TestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//業務處理
}
}
- 重寫幾個核心的方法,其中 channelRead () 是業務邏輯編寫,使用最多;
方法名稱 | 觸發時機 | 常見業務場景 |
---|---|---|
channelActive | 連接就緒時觸發 | 連接時進行登錄認證 |
channelRead | 通道有數據可讀取時觸發 | 讀取數據并且做處理,這個是用的最多的方法 |
channelInactive | 連接斷開時觸發 | 連接斷開,刪除服務端對于的 Session 關系;也可以在這里實現斷開重新 |
exceptionCaught | 發生異常時觸發 | 發生日常時,做日志記錄 |
- 把 Handler 添加到 Pipeline 管道里面
實例:
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
//自定義業務 Handler
ch.pipeline().addLast(new TestHandler());
}
});
5. 服務端實現
5.1 服務端啟動類
public class NettyServer {
public static void main(String[] args) {
//線程組-主要是監聽客戶端請求
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
//線程組-主要是處理具體業務
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//啟動類
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
//指定線程組
.group(bossGroup, workerGroup)
//指定 NIO 模式
.channel(NioServerSocketChannel.class)
//雙向鏈表管理
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//責任鏈,指定自定義處理業務的 Handler
ch.pipeline().addLast(new NettyServerHandler());
}
});
//綁定端口號
serverBootstrap.bind(80);
}
}
代碼說明:
- 以上都是模板代碼,需要變動的是根據不同的業務自定義對應的 Handler,并且在 initChannel () 添加邏輯處理器;
- 根據實際情況指定 bind () 方法的端口號,注意的是端口號不能和其它端口號沖突;
- 這里大家先熟練掌握模板代碼的編寫,后面章節會講解 NioEventLoopGroup、Pipeline 等核心組件的原理。
5.2 自定義 Handler
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//接受客戶端端響應時觸發該事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//轉換為 ByteBuf 緩沖區(底層是 byte[] 數組)
ByteBuf buffer=(ByteBuf)msg;
//定義一個 byte[] 數組
byte[] bytes=new byte[buffer.readableBytes()];
//緩沖區把數據寫到 byte[] 數組
buffer.readBytes(bytes);
//把 byte[] 轉換字符串
String req=new String(bytes,"UTF-8");
System.out.println("客戶端請求:"+req);
//給客戶端響應信息>>>>>>>>>>>>>>>>>>>>>>>>>>>>
String res="Hello World>>>>Client";
//把字符串轉換 ByteBuf
ByteBuf buf=getByteBuf(ctx,res);
//把 ByteBuf 寫到通道并且刷新
ctx.writeAndFlush(buf);
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx,String str) {
// 1. 獲取二進制抽象 ByteBuf
ByteBuf buffer = ctx.alloc().buffer();
// 2. 準備數據,指定字符串的字符集為 utf-8
byte[] bytes = str.getBytes(Charset.forName("utf-8"));
// 3. 填充數據到 ByteBuf
buffer.writeBytes(bytes);
return buffer;
}
}
代碼說明:
-
這個邏輯處理器繼承自 ChannelInboundHandlerAdapter,然后覆蓋了 channelRead () 方法;
-
channelRead () 方法,接受客戶端請求數據時會觸發該方法,一般是用來處理具體的業務;
-
Netty 是面向 ByteBuf 通訊的,接受數據和響應數據都需要轉換 ByteBuf,ByteBuf 的 API 后面再詳細講解,這里我們需要知道的是常見創建 ByteBuf 有常見兩種方式,①通過 Unpooled 非池化工具類來操作;②通過
ctx.alloc().buffer()
來獲取。最后我們調用ctx.channel().writeAndFlush()
把數據寫到服務端。
6. 客戶端實現
6.1 客戶端啟動類
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
// 1.指定線程模型
.group(workerGroup)
// 2.指定 IO 類型為 NIO
.channel(NioSocketChannel.class)
// 3.IO 處理邏輯
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
//自定義業務 Handler
ch.pipeline().addLast(new NettyClientHandler());
}
});
// 4.建立連接
ChannelFuture future=bootstrap.connect("127.0.0.1", 80).sync();
}
}
代碼說明:
- 以上都是模板代碼,需要變動的是根據不同的業務自定義對應的 Handler,并且在 initChannel () 添加邏輯處理器;
- connect () 方法,指定對應服務端 ip 和 port。
6.2 自定義 Handler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//客戶端連接成功之后觸發該事件,只會觸發一次
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World".getBytes()));
}
//接受服務端響應時觸發該事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer=(ByteBuf)msg;
byte[] bytes=new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
String res=new String(bytes,"UTF-8";
System.out.println("服務端響應:"+res);
}
}
代碼說明:
- 這個邏輯處理器繼承自 ChannelInboundHandlerAdapter,然后覆蓋了 channelActive () 和 channelRead () 方法;
- channelActive (),這個方法會在客戶端連接建立成功之后被調用,可以在這個方法里面,做一些初始化的工作,該方法僅被調用一次;
- channelRead 方法,在接受客戶端響應時觸發,會觸發多次。
7. 測試效果
服務端打?。?br>
客戶端打?。?br>
8. 視頻演示
9. 小結
通過以上的代碼,我們主要實現了客戶端和服務端之間的通訊,需要掌握以下關鍵點:
- 客戶端和服務端的啟動類代碼,這個基本上是固定寫法;
- 掌握 Handler 的作用以及如何自定義,幾個核心方法的觸發時機以及常見的應用場景;
- 和傳統的 socket 編程不同的是,Netty 里面數據是以 ByteBuf 為單位的。