前幾節(jié)中,講解了Bio、Nio、Aio實現(xiàn)時間查詢服務的細節(jié),比對其優(yōu)缺點進行了對比。從本節(jié)開始,Nio系列將講解Netty的相關知識,為了比較使用Netty實現(xiàn)服務與直接使用JDK提供的IO操作實現(xiàn)服務的區(qū)別,本節(jié)仍然以實現(xiàn)時間查詢服務為例,講解Netty的使用細節(jié)。
Netty實現(xiàn)服務端
Netty屏蔽了IO底層的操作,即便對IO操作不熟悉,沒有專業(yè)的知識基礎,也可以寫出穩(wěn)定可用的服務。使用Netty編程,基本就是按照特定的步驟進行自定義的設值,啟動服務即可。Netty服務端實現(xiàn)邏輯如下:
- 創(chuàng)建兩個線程池 EventLoopGroup,一個用于處理客戶端的鏈接操作,如果服務只監(jiān)聽了一個端口的話,建議改線程池的大小設置為1,另一個線程池用于處理IO的讀寫操作。
- 配置ServerBootstrap, 該類是服務端配置輔助類,主要配置服務端的一些設置參數(shù)。例如設置使用的線程池、使用的channel類型、連接上限、初始化邏輯操作等等。
- 使用serverBootstrap綁定端口,使用同步方法綁定端口,綁定成功返回ChannelFuture對象,到此服務端就啟動了
- 等待future執(zhí)行完,主要就是為了不讓邏輯執(zhí)行完成后退出
實現(xiàn)代碼如下:
public class NettyServer {
private int port;
public NettyServer(int port) {
this.port = port;
}
public void start(){
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimerServerHandler());
}
});
try {
//同步綁定
ChannelFuture future = serverBootstrap.bind(new InetSocketAddress(port)).sync();
//等待關閉
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
NettyServer server = new NettyServer(8080);
server.start();
}
}
重點講解一下childHandler()方法,該方法中接收一個ChannelInitializer對象,ChannelInitializer是一個抽象類,我們需要繼承該類實現(xiàn)抽象方法initchannel(),在該方法中往pipeline中添加自己的處理邏輯。數(shù)據(jù)的編解碼、數(shù)據(jù)的接收讀取都可以在這里進行擴展。處理邏輯類的實現(xiàn)如下:
public class TimerServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String message = new String(bytes, "utf-8");
System.out.println("獲取到請求:" + message);
String response = null;
if("時間".equals(message)){
response = (new Date()).toString();
}else{
response = "不支持該請求";
}
byte[] responseBytes = response.getBytes();
ByteBuf responseBuf = Unpooled.buffer(responseBytes.length);
responseBuf.writeBytes(responseBytes);
System.out.println("返回響應:" + response);
ctx.write(responseBuf);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
cause.printStackTrace();
}
}
邏輯處理類,需要繼承ChannelHandlerAdapter,其中重要介紹一下channelRead()方法,該方法在接收到數(shù)據(jù)時自動調用(會存在半包問題,此處不考慮),ctx是上下文參數(shù),可以通過ctx讀寫數(shù)據(jù),msg是讀取到的數(shù)據(jù),這個數(shù)據(jù)可以是Buffer,可以是String,可以是任意對象,關鍵是看有沒有在initChannel方法中進行編解碼配置。讀取到數(shù)據(jù)中,解析數(shù)據(jù),然后進行響應。到此服務端代碼就寫完了。
Netty實現(xiàn)客戶端
客戶端的實現(xiàn)與服務端實現(xiàn)類似,不同之處在于:
- 輔助配置類為Bootstrap
- 只需要配置一個線程池即可,因為可以斷不需要處理請求連接操作
實現(xiàn)代碼如下:
public class NettyClient {
private int port;
private String hostname;
public NettyClient(int port, String hostname) {
this.port = port;
this.hostname = hostname;
}
public void start(){
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
try {
ChannelFuture future = bootstrap.connect(new InetSocketAddress(hostname, port)).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
NettyClient client = new NettyClient(8080, "127.0.0.1");
client.start();
}
}
重點看一下客戶端的處理邏輯類TimeClientHandler的實現(xiàn),代碼如下:
public class TimeClientHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String message = new String(bytes, "utf-8");
System.out.println("收到響應:" + message);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String message = "時間";
byte[] bytes = message.getBytes();
ByteBuf byteBuf = Unpooled.buffer(bytes.length);
byteBuf.writeBytes(bytes);
System.out.println("發(fā)送請求:" + message);
ctx.writeAndFlush(byteBuf);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
channelActive()在連接服務器成功時調用,連接成功后發(fā)送請求即可。channelRead()方法在當有響應數(shù)據(jù)時調用(接收數(shù)據(jù)隊列中有數(shù)據(jù)了就會調用,當然也會受到initChannel()方法中配置的影響),到此客戶端也實現(xiàn)完成。
總結
經(jīng)過對Netty的學習,我們發(fā)現(xiàn)在實現(xiàn)邏輯中,我們不在和Channel、socket打交道了,而是而需要和Buffer打交道即可(如果配置了編解碼Buffer也不用打交道,直接處理對象即可)。Netty屏蔽了IO處理的細節(jié),簡化了編程的復雜性。
下一節(jié),將講解Netty對半包/粘包問題的處理。
歡迎掃描下方二維碼,關注公眾號,我們可以進行技術交流,共同成長
