上一節中介紹的java NIO的開發,回顧下NIO開發的步驟:
1、創建ServerSocketChannel并設置為非阻塞模式
2、綁定監聽端口
3、創建多路服務器Selector,將創建的ServerSocketChannel注冊到Selector,監聽SelectKey.Accept事件
4、創建IO線程輪詢Selector.select
5、如果輪詢到就緒的Channel如果是OP_ACCEPT狀態說明是新的客戶端接入則調用ServerSocketChannel.accept方法接受新的客戶端
6、設置客戶端鏈路SocketChannel為非阻塞模式,并且將其注冊到Selector上,監聽SelectKey.OP_READ操作位
7、如果輪詢到Channel為OP_READ,則說明SocketChannel中有新的就緒的數據包需要讀取,則通過ByteBuffer讀取數據
由上來看,直接通過Java原生的類進行NIO編程非常復雜繁瑣,而Netty正好解決了這個難題
下面通過一個簡單的Netty程序了解Netty開發
netty服務端代碼:
public class TimeServer {
public void bind(int port) throws Exception {
//創建兩個線程組 一個用于服務端接收客戶端的連接
EventLoopGroup bossGroup = new NioEventLoopGroup();
//一個用于網絡讀寫
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHander());
ChannelFuture future = b.bind(port).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHander extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
try {
new TimeServer().bind(port);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception{
ByteBuf byteBuf = (ByteBuf)msg;
byte[] req = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println("the time server receive order:"+body);
String currentTIme = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()
).toString():"BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTIme.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
程序剛開始創建了兩個EventLoopGroup線程,一個專門監聽服務端接收客戶端請求,一個專門處理業務邏輯,之后創建一個ServerBootstrap類,并將線程組傳遞到ServerBootstrap,設置的Channel為NioServerSocketChannel并將事件處理類設置為ChildChannelHander,最后調用bind方法綁定監聽端口,最后future.channel().closeFuture().sync()會阻塞直到服務端斷開連接
客戶端代碼:
public class TimeClient {
public void connect(int port,String host) throws Exception {
//創建讀寫io線程組
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
System.out.println("1");
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
System.out.println("2");
ChannelFuture f = b.connect(host,port).sync();
System.out.println("3");
f.channel().closeFuture().sync();
System.out.println("4");
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length >0) {
try {
port = Integer.valueOf(args[0]);
}catch (NumberFormatException e) {
}
}
try {
new TimeClient().connect(port,"127.0.0.1");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Netty客戶端代碼更為簡單,第一步創建NioEventLoopGroup線程組,跟服務端一樣再創建客戶端輔助類Bootstrap并將線程組和渠道作為參數出傳入,并且指定handler實現initChannel方法,其作用就是當創建NioSocketChannel之后回調initChannel方法處理網絡IO事件
下面是TimeChientHander代碼
public class TimeClientHandler extends ChannelHandlerAdapter {
private final ByteBuf firstMsg;
public TimeClientHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMsg = Unpooled.buffer(req.length);
firstMsg.writeBytes(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMsg);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println("Now is : "+body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("Unexpected exception from downstream :"+cause.getMessage());
ctx.close();
}
}
TimeChientHander實現了channelActive,channelRead,exceptionCaught方法,
channelActive為客戶端或者服務端TCP鏈路建立連接之后回調,當服務端寫數據到客戶端時channelRead回調,當發生異常時會回調exceptionCaught方法。
從上面代碼可以看出Netty將IO事件的處理抽象為ChannelHandler接口,開發者可以繼承ChannelHandler接口來自定義事件處理邏輯。
ChannelHandler接口主要有以下方法:
channelRegistered(ChannelHandlerContext ctx) 注冊到EventLoop成功回調
channelActive(ChannelHandlerContext ctx) 當建立連接后回調
channelRead(ChannelHandlerContext ctx, Object msg) 當前channel讀到數據時回調
channelReadComplete(ChannelHandlerContext ctx) 讀取數據完成后回調
exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 發生異常時回調
在實際Netty開發中基本都是繼承ChannelHandler實現業務邏輯
在初始化Channel時需要將自定義的ChannelHandler添加到該Channel上,Netty實現是ChannelPipeline.addLast
源碼如下
public interface ChannelPipeline extends Iterable<Entry<String, ChannelHandler>> {
……
}
其中繼承Iterable迭代器接口,可以看出其hannelPipeline是一系列ChannelHandler的集合(可以理解為鏈表)自定義的ChannelHandler可以調用它的addFirst、addLast添加到IO處理pipeLine的頭或者尾
主要方法有以下:
ChannelPipeline addFirst(String name, ChannelHandler handler) 將handler插入到handler鏈表頭部
ChannelPipeline addLast(ChannelHandler... handlers) 將handler插入到handler鏈表尾部
自動以的ChannelHandler一般通過上述方法添加到事件處理鏈表中