概述
在本文,我們將編寫一個基於 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!