目錄
本章我們將展示如何構建一個基於Netty的客戶端和服務器。應用程序非常簡單,客戶端將消息發送給服務端,而服務器再將消息回送給客戶端。開發環境搭建步驟我們直接跳過了,這裏有一點需要注意的是我們下載的是JDK,而不是Java運行環境(JRE),其只可以運行Java應用程序,但是不能夠編譯它們。
下圖展示了我們要編寫的Echo客戶端和服務器應用程序:
Echo客戶端和服務器之間的交互非常簡單的,在客戶端建立一個連接之後,它會向服務器發送一個或多個消息,反過來,服務器又將每個消息回送給客戶端。
1.編寫Echo服務器
所有的Netty服務器都需要以下兩個部分:
- 至少一個ChannelHandler:該組件實現了服務器對從客戶端接收的數據的處理,即它的業務邏輯。
- 引導——這是配置服務器的啓動代碼。至少,它會將服務器綁定到它要監聽連接請求的端口上。
1.1 ChannelHandler和業務邏輯
ChannelHandler是一個接口族的父接口,它的實現負責接收並響應事件通知。在Netty應用程序中,所有的數據處理邏輯都包含在這些核心抽象的實現中。
因爲我們的Echo服務器會響應傳入的消息,所以它需要實現ChannelInboundHandler接口,用來定義響應入站事件的方法。這個簡單的應用程序只需要用到少量的這些方法,所以繼承ChannelInboundHandlerAdapter類就足夠了,它提供了ChannelInboundHandler的默認實現。
我們項目中主要用到了下面的方法:
- channelRead():對於每個傳入的消息都要調用
- channelReadComplete():通知ChannelInboundHandler,最後一次對channelRead調用(讀取最後一次消息)
- exceptionCaught():在讀取操作期間,有異常拋出時會調用
該Echo服務器的ChannelHandler實現是EchoServerHandler,實現代碼如下:
package com.martin.learn.netty.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
/**
* 負責接收並響應時間的通知
*
* @author: martin
* @date: 2018/12/21 20:38
* @description:
*/
/**
* 標識一個ChannelHandler可以被多個Channel安全的共享
*/
@ChannelHandler.Sharable
public class EchoServiceHandler extends ChannelInboundHandlerAdapter {
/**
* 對於每個傳入的消息都要調用
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server received:" + in.toString(CharsetUtil.UTF_8));
//將接收到的消息寫給發送者
ctx.write(in);
}
/**
* 讀取操作完成時調用
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//將未決的消息沖刷到遠程節點,並且關閉該Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
/**
* 在讀取操作期間,有異常拋出時調用
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//關閉該Channel
ctx.close();
}
}
ChannelInboundHandlerAdapter有一個直觀的API,並且它的每個方法都可以被重寫以掛鉤到事件生命週期的恰當點上。因爲需要處理所有接收到的數據,所以我們重寫了channelRead()方法。
重寫exceptionCaught()方法允許我們對Throwable的任何子類型做出處理,在這裏我們記錄了異常並關閉了連接。
如果我們在這裏不捕獲異常,會發生什麼呢?
每個Channel都擁有一個與之相關聯的ChannelPipeline,其持有一個ChannelHandler的實例鏈。在默認的情況下,ChannelHadler會把對它的方法的調用轉發給鏈中的下一個ChannelHandler。因此,如果exceptionCaught()方法沒有被該鏈中的某處實現,那麼所接收的異常將會被傳遞到ChannelPipeline的尾端並被記錄。爲此,我們的應用程序應該提供至少一個實現了exceptionCaught()方法的ChannelHandler。
關鍵點總結:
- 針對不同類型的事件來調用ChannelHandler。
- 應用程序通過實現或者擴展ChannelHandler來掛鉤到事件的生命週期,並且提供自定義的應用程序邏輯。
- 在架構上,ChannelHandler有助於保持業務邏輯與網絡代理代碼的分離,這簡化了開發過程。
1.2 引導服務器
引導服務器涉及以下內容:
- 綁定到服務器將在其上監聽並接受傳入連接請求的端口。
- 配置Channel,以便將有關的入站消息通知給EchoServerHandler實例。
實現代碼如下:
package com.martin.learn.netty.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
/**
* 服務器類
*
* @author: martin
* @date: 2018/12/22 9:54
* @description:
*/
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws InterruptedException {
final EchoServiceHandler serviceHandler = new EchoServiceHandler();
//創建並分配一個NioEventLoopGroup實例以進行事件的處理,如接受新連接以及讀寫數據
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
//指定所使用的NIO傳輸Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口設置套接字的地址
.localAddress(new InetSocketAddress(port))
//添加一個EchoServerHandler到子Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//EchoServerHandler被標註爲@Shareable,所以我們總是使用相同的實例
socketChannel.pipeline().addLast(serviceHandler);
}
});
//異步地綁定服務器,調用sync()方法阻塞等待直到綁定完成
ChannelFuture future = bootstrap.bind().sync();
//獲取Channel的CloseFuture,並且阻塞當前線程直到它完成
future.channel().closeFuture().sync();
} finally {
//關閉EventLoopGroup,釋放所有的資源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("必須指定服務器端口");
}
int port = Integer.parseInt(args[0]);
new EchoServer(port).start();
}
}
這個實力使用了NIO,因爲得益於它的可擴展性和徹底的異步性,它是目前使用最廣泛的傳輸。但是也可以使用一個不同的傳輸實現,比如OIO傳輸,將需要指定OioServerSocketChannel和OioEventLoopGroup。引導的步驟如下:
- 創建一個ServerBootstrap的實例以引導和綁定服務器
- 創建並分配一個NioEventLoopGroup實例以進行事件的處理,如接受新連接以及讀/寫數據。
- 指定服務器綁定的本地的InetSocketAddress
- 使用一個EchoServerHandler的實例初始化每一個新的Channel
- 調用ServerBootstrap.bind()方法以綁定服務器
這個時候,服務器已經初始化,並且就緒已經能被使用了。
2.編寫Echo客戶端
Echo客戶端將會:
- 連接到服務器
- 發送一個或者多個消息
- 對於每個消息,等待並接收從服務器發回的相同的消息
- 關閉連接
2.1 通過ChannelHandler實現客戶端邏輯
客戶端將擁有一個用來處理數據的ChannelInboundHandler,在這裏我們擴展SimpleChannelInboundHandler類以處理所有必須的任務。這要求重寫下面的方法:
- channelActive():在到服務器的連接已經建立之後將被調用
- channelRead():當從服務器接收到一條消息時被調用
- exceptionCaught():在處理過程中引發異常時被調用
實例代碼如下:
package com.martin.learn.netty.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/**
* @author: martin
* @date: 2018/12/22 23:00
* @description:
*/
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 在到服務器的連接已經建立之後將被調用
*
* @param ctx
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
//當被通知Channel是活躍的時候,發送一條消息
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}
/**
* 在處理過程中引發異常的時候被調用
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
//在發生異常的時候,記錄錯誤並關閉Channel
cause.printStackTrace();
ctx.close();
}
/**
* 每當從服務器接收到一條消息時被調用
*
* @param channelHandlerContext
* @param byteBuf
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) {
//記錄已接收消息的轉儲
System.out.println("Client received:" + byteBuf.toString(CharsetUtil.UTF_8));
}
}
首先,我們重寫了channelActive()方法,其將在一個連接建立時被調用,這確保了數據將會被儘可能快地寫入服務器。
接下來,我們重寫了channelRead()方法,每當有數據接收時,都會調用這個方法。需要注意的是,由服務器發送的消息可能會被分塊接收。也就是說,如果服務器發送了5字節,那麼並不能保證這5個字節會被一次性接收。即使是這麼少的數據,channelRead()方法也有可能會被調用兩次,第一次使用一個持有3字節的ByteBuf,第二次使用一個持有2字節的ByteBuf。作爲一個面向流的協議,TCP保證了字節數組將會按照服務器發送它們的順序被接收。
最後,我們重寫了exceptionCaught(),記錄Throwable,關閉Channel,終止到服務器的連接。
2.2 引導客戶端
引導客戶端類似於引導服務器,使用代碼如下:
package com.martin.learn.netty.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* 客戶端連接
*
* @author: martin
* @date: 2018/12/22 23:36
* @description:
*/
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
//指定EventLoopGroup以處理客戶端的事件;適用於NIO的實現
EventLoopGroup group = new NioEventLoopGroup();
try {
//創建Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
//適用於NIO傳輸的Channel類型
.channel(NioSocketChannel.class)
//設置連接服務器的InetSocketAddress
.remoteAddress(new InetSocketAddress(host, port))
//在創建Channel時,向ChannelPipeline中添加一個EchoClientHandler實例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
//連接到遠程節點,阻塞等待直到連接完成
ChannelFuture channelFuture = bootstrap.connect().sync();
//阻塞直到Channel關閉
channelFuture.channel().closeFuture().sync();
} finally {
//關閉線程池並且釋放所有的資源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
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();
}
}
和服務端一樣,使用了NIO傳輸。注意,我們可以在客戶端和服務端上分別使用不同的傳輸。例如,在服務器端使用NIO傳輸,而在客戶端使用OIO傳輸。實現客戶端的關鍵點如下:
- 爲初始化客戶端,創建了一個Bootstrap實例。
- 爲進行事件處理分配了一個NioEventLoopGroup實例,其中事件處理包括創建新的連接以及處理入站和出站數據。
- 爲連接服務器創建了一個InetSocketAddress實例。
- 當連接被建立時,一個EchoClientHandler實例會被安裝到ChannelPipeline中。
- 在一切都設置完成之後,調用Bootsrap.connect()方法連接到遠程節點。
3. 編譯運行
完成客戶端和服務端的編寫之後,先運行服務端,再運行客戶端,輸出結果如下:
Server received:Netty rocks!
Client received:Netty rocks!
4.小結
在本章中,我們開發了一個簡單的Netty客戶端和服務端。雖然這只是一個簡單的應用程序,但是它可以伸縮到支持數千個併發連接,每秒可以比普通的基於套接字的Java應用程序處理多得多的消息。