Netty 框架學習 —— 第一個 Netty 應用
概述
在本文,我們將編寫一個基於 Netty 實現的客戶端和服務端應用程序,相信通過學習該示例,一定能更全面的理解 Netty API
該圖展示的是多個客戶端同時連接到一台服務器。客戶端建立一個連接後,會向服務器發送一個或多個消息,反過來,服務器又會將每個消息回送給客戶端
編寫 Echo 服務器
所有 Netty 服務器都需要以下兩部分:
-
至少一個 CHannelHandler
該組件實現了服務器對從客戶端接收的數據的處理,即它的業務邏輯
-
引導
配置服務器的啟動代碼,將服務器綁定到它要監聽連接請求的端口上
1. ChannelHandler 和業務邏輯
ChannelHandler 是一個接口族的父接口,它的實現負責接收並響應事件通知,即要包含數據的處理邏輯。我們的 Echo 服務器需要響應傳入的消息,所以需要實現 ChannelHandler 接口,用來定義響應入站事件的方法,又因為只需要用到少量的方法,所以繼承 ChannelHandlerAdapter 類就足夠了,它提供了 ChannelHandler 的默認實現
我們感興趣的方法有:
-
channelRead()
對於每個傳入的消息都要調用
-
channelReadComplete()
通知 ChannelHandler 最後一次對 channelRead() 的調用是當前批量讀取的最後一條消息
-
exceptionCaught
在讀取操作期間,有異常拋出時會調用
Echo 服務器的 ChannelHandler 實現 EchoServerHandler 如下
@ChannelHandler.Sharable // 標識一個 ChannelHandler 可以被多個 Channel 安全的共享
public class EchoServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server receiver: " + in.toString(CharsetUtil.UTF_8));
// 將接收到的消息寫給發送者
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
// 將剩餘的消息全部沖刷到遠程結點,並關閉 CHannel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
應用程序通過實現或者擴展 ChannelHandler 來掛鈎到事件的生命周期,並且提供自定義的應用程序邏輯。ChannelHandler 有助於保持業務邏輯與網絡處理代碼的分離,簡化了開發過程
2. 引導服務器
編寫完 EchoServerHandler 實現的核心業務邏輯之後,我們現在探討引導服務器的過程,具體涉及內容如下:
- 綁定服務器將在其上監聽並接收傳入連接請求的接口
- 配置 Channel,將入站消息交給 EchoServerHandler 實例
EchoServer 類完整代碼如下
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: " + EchoServer.class.getSimpleName() + " <port>");
return;
}
int port = Integer.parseInt(args[0]);
new EchoServer(port).start();
}
public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
// 指定所使用的 NIO 傳輸 Channel
.channel(NioServerSocketChannel.class)
// 使用指定的端口設置套接字地址
.localAddress(new InetSocketAddress(port))
// 添加一個 EchoServerHandler 到子 Handler 的 ChannelPipeline
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(serverHandler);
}
});
// 異步地綁定服務器,調用 sync() 方法阻塞等待直到綁定完成
ChannelFuture f = b.bind().sync();
// 獲取 Channel 的 CloseFuture,並且阻塞當前線程直到它完成
f.channel().closeFuture().sync();
} finally {
// 關閉 EventLoopGroup 釋放所有資源
group.shutdownGracefully().sync();
}
}
}
到此為止,我們回顧一下服務器實現中的幾個重要步驟:
- EchoServerHandler 實現業務邏輯
- main() 方法引導服務器
引導服務器過程的重要步驟如下:
- 創建一個 ServerBootstrap 的實例以引導和綁定服務器
- 創建並分配一個 NioEventLoopGroup 實例以進行事件的處理,如接受新連接以及讀寫數據
- 指定服務器綁定的本地的 InetSocketAddress
- 使用一個 EchoServerHandler 實例初始化每一個新的 Channel
- 調用 ServerBootstrap.bind() 方法以綁定服務器
編寫 Echo 客戶端
Echo 客戶端的作用:
- 連接到服務器
- 發送一個或多個消息
- 對於每個消息,等待並接收從服務器返回的響應
- 關閉連接
和服務器一樣,編寫客戶端所涉及的主要代碼部分也是業務邏輯和引導
1. ChannelHandler 和客戶端邏輯
客戶端也要有一個用來處理數據的 ChannelHandler,這裡選擇 SimpleChannelInboundHandler 類處理所有必需的任務,要求重寫下面的方法:
-
channelActive()
當與服務器的連接建立之後被調用
-
messageReceived()
當從服務器接收到一條消息時被調用
-
exceptionCaught()
在處理過程中引發異常時被調用
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 當一個連接建立時被調用,發送一條消息
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) {
// 記錄已接收消息的轉儲
System.out.println("Client received: " + msg.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 發生異常時,記錄錯誤並關閉 Channel
cause.printStackTrace();
ctx.close();
}
}
2. 引導客戶端
引導客戶端類似於服務器,不同的是,客戶端是使用主機和端口參數來連接遠程地址
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: " + EchoClient.class.getSimpleName() + "<host> <port>");
return;
}
String host = args[0];
int port = Integer.parseInt(args[1]);
new EchoClient(host, port).start();
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
// 創建 Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
// 連接到遠程節點,阻塞等待直到連接完成
ChannelFuture future = bootstrap.connect().sync();
// 阻塞,直到 Channel 關閉
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
到此為止,我們回顧一下客戶端實現中的幾個重要步驟:
- 創建一個 Bootstrap 實例
- 創建並分配一個 NioEventLoopGroup 實例以進行事件的處理,其中事件處理包括創建新的連接以及處理入站和出站數據
- 為服務器連接創建一個 InetSocketAddress 實例
- 當連接建立時,一個 EchoClientHandler 實例會被安裝到該 Channel 的 ChannelPipeline 中
- 調用 Bootstrap.connect() 方法連接遠程節點
運行客戶端和服務端
本文的項目使用 maven 構建,先啟動服務端並準備好接受連接。然後啟動客戶端,一旦客戶端建立連接,就會發送消息。服務器接收消息,控制台會打印如下信息:
Server receiver: Netty rocks!
同時將其回送給客戶端,客戶端的控制台也會打印如下消息,隨後退出:
Client received: Netty rocks!