《Netty NIO 入门篇》
Netty简单介绍
为什么选择Netty?开发高质量的NIO程序并不是一件简单的事情,出去NIO的复杂性和BUG不谈,作为一个NIO服务器,要能处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写情况,如果没有足够的NIO编程经验累积,一个NIO框架的稳定往往需要半年甚至更长的实际。并且从维护性角度而言,NIO采用了异步非阻塞编程模型,而且是一个I/O线性处理多条链路,调试和跟踪非常麻烦,我们无法进行有效的调试和跟踪,定位难度很大。同时NIO编程设计到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
而Netty具有以下优点:1、API使用简单,开发门槛低。2、预置了多种编解码功能。3、定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展。4、Netty修复了已经发现的NIO的BUG。Netyy在大数据、互联网等众多行业进行了成功的商用,正因这些优点,Netty成为了 Java NIO编程的首选框架。
Netty相比原生的NIO做了哪些改变?
搭建Netty入门应用
从官网上 https://netty.io/downloads.html /maven repostory下载压缩包:新建java项目,将all in one中的netty-all .jar拷贝至工程lib下,并添加至构建路径即可。
服务端时间服务器:
- 创建ServerBootstrap实例来引导绑定和启动服务器
- 创建NioEventLoopGroup对象来处理事件,如接受新连接、接收数据、写数据等等
- 指定InetSocketAddress,服务器监听此端口
- 设置childHandler执行所有的连接请求
- 都设置完毕了,最后调用ServerBootstrap.bind()
1、先创建两个NioEventLoopGroup线程组(Reactor线程组),一个NioEventLoopGroup包含了一组NIO线程,一个线程组用于服务端接收客户端的连接,另一个用于进行SocketChannel的网络读写。
EventLoopGroup bossGroup = new NioEventLoopGroup();// 用户服务端接收客户端的链接
EventLoopGroup workerGroup = new NioEventLoopGroup();// 用户进行SocketChannel的网络读写
2、创建ServerBootstrap对象,用于辅助NIO服务端启动,目的是降低服务端开发的复杂度。
ServerBootstrap b = new ServerBootstrap();
3、调用ServerBootstrap的group方法,将上面的两个NIO线程组传入该方法,同时要配置NioServerSocketChannel的TCP参数,还要绑定I/O事件的处理类ChildChannelHandler,事件处理类ChildChannelHandler类似Reactor模式中Handler类,处理网络I/O事件,例如记录日志、对消息进行编码等。ChildChannelHandler需要继承ChannelInitializer抽象类,实现initChannel方法。
b.group(bossGroup, workerGroup)// 将两个NIO线程组当做入参传递到ServerBootstrap中
.channel(NioServerSocketChannel.class)
//创建NioServerSocketChannel 对应JDK NIO库中的ServerSocketChannel类 , 指定通道类型为NioServerSocketChannel
.option(ChannelOption.SO_BACKLOG, 1024)
//然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024
//最后绑定IO事件的处理类ChildChannelHandler,类似于Reactor模式中的Handler类,主要用于处理网络IO事件,例如记录日志,消息编解码等
.childHandler(new ChildChannelHandler());
4、服务器启动辅助类配置完成后,调用它的bind方法绑定监听端口,然后调用它的同步阻塞方法sync等待绑定完成,之后Netty返回一个ChannelFuture,功能类似java.util.concurrent.Future,用于异步操作的通知回调
ChannelFuture f = b.bind(port).sync();
5、使用f.channel().closeFuture().sync()方法进行阻塞,等待服务端链路关闭之后main函数才退出。
f.channel().closeFuture().sync();
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class TimeServer {
public void bind(int port) throws Exception {
// 配置服务端的NIO线程组 ,专门用于网络事件的处理,实际上他们就是Reactor线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();// 用户服务端接收客户端的链接
EventLoopGroup workerGroup = new NioEventLoopGroup();// 用户进行SocketChannel的网络读写
try {
// ServerBootstrap对象实际上是netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)// 将两个NIO线程组当做入参传递到ServerBootstrap中
.channel(NioServerSocketChannel.class)//创建NioServerSocketChannel 对应JDK NIO库中的ServerSocketChannel类 , 指定通道类型为NioServerSocketChannel
.option(ChannelOption.SO_BACKLOG, 1024)//然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024
//最后绑定IO事件的处理类ChildChannelHandler,类似于Reactor模式中的Handler类,主要用于处理网络IO事件,例如记录日志,对消息进行编解码等
.childHandler(new ChildChannelHandler());
// 绑定端口,同步方法等待绑定操作完成 ,返回ChannelFuture主要用于异步操作的通知回调.
ChannelFuture f = b.bind(port).sync();
// 等待阻塞,等待服务端链路关闭之后main函数才退出
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 调用childHandler 来指定连接后调用的ChannelHandler,这个方法传ChannelInitalizer类型的参数
// ChannelInitalizer是个抽象类,所以要实现initChannel方法, 这个方法及时用来设置ChannelHandler的
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new TimeServerHandler());
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 30000;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new TimeServer().bind(port);
}
}
- ChannelHandler : IO事件处理类 处理网络IO事件,例如记录日志,对消息进行编解码等…
- ChannelHandlerAdapter : IO事件适配器类 ,进行链接、读、写、等事件的处理及扩展
TimeServerHandler 实现业务器业务逻辑,继承了ChannelHandlerAdapter :Netty使用futures和回调概念,它的设计允许你处理不同的事件类型。你的channelhandler必须继承ChannelInboundHandlerAdapter并且重写channelRead方法,这个方法在任何时候都会被调用来接收数据,在这个例子中接收的是字节。
1、首先将接收到的数据做类型转换,将msg消息转换成Netty的ByteBuf对象,即相当于jdk中的java.nio.ByteBuffer对象,通过ByteBuf的readableBytes方法可以获取缓冲区可读的字节数,根据字节数创建byte数组,通过ByteBuf的readBytes方法将缓冲区的字节数组复制到新建的byte数组中,最后通过NewString构造字符串。由于没有加StringDecoder、StringEncoder因此,此处需要类型转换。
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
2、在读完后,会调用channelReadComplete方法,由于在读方法中有异步发送应答操作,因此channelReadComplete方法中调用ChannelHandlerContext的flush方法,将消息发送队列中的消息写入到SocketChannel中发送给对象,为了防止频繁的换下Selector进行消息发送,Netty的write方法并不直接将消息写入SocketChannel中,调用write方法只是把待发送的消息放到发送缓冲数组中,再次调用flush方法,将缓冲区中的消息全部写入到SocketChannel。
ctx.write(resp);// 异步发送应答消息给客户端
// 再通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中
ctx.flush();
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import java.util.logging.Logger;
public class TimeServerHandler extends ChannelHandlerAdapter {
private static final Logger logger = Logger
.getLogger(TimeServerHandler.class.getName());
@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("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 {
// 将发送缓冲区中的消息全部写到SocketChannel中发送给对方。
// 从性能角度考虑,为了防止频繁地唤醒Selector进行消息发送,Netty的write方法并不直接将消息写入SocketChannel中
// 调用write方法只是把待发送的消息放到发送缓冲区数中,
// 再通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.warning("Unexpected exception from downstream : "
+ cause.getMessage());
/**
* 当发生异常时,关闭ChannelHandlerContext,释放ChannelHandlerContext相关联的句柄等资源
*/
ctx.close();
}
}
客户端类:
- 创建Bootstrap对象用来引导启动客户端
- 创建EventLoopGroup对象并设置到Bootstrap中,EventLoopGroup可以理解为是一个线程池,这个线程池用来处理连接、接受数据、发送数据
- 创建InetSocketAddress并设置到Bootstrap中,InetSocketAddress是指定连接的服务器地址
- 添加一个ChannelHandler,客户端成功连接服务器后就会被执行
- 调用Bootstrap.connect()来连接服务器
- 最后关闭EventLoopGroup来释放资源
1、客户端首先创建I/O读写的NioEventLoopGroup线程组,然后继续创建客户端辅助启动类Bootstrap,随后对其配置,与服务端不同的是,它的Channel需要设置为NioSocketChannel,然后为其添加Handler,此处为了简单直接创建了匿名内部类,实现initChannel方法,作用是当创建NioSocketChannel,成功之后,进行初始化时,将ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();// 客户端辅助启动类,然后对其进行配置
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
// 作用是当创建NioSocketChannel成功之后,在进行初始化时,
// 将ChannelHandler设置到ChannelPipeline中,用于处理网络IO事件
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class TimeClient {
public void connect(int port, String host) throws Exception {
// 配置客户端 NIO 线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();// 客户端辅助启动类,然后对其进行配置
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
// 作用是当创建NioSocketChannel成功之后,在进行初始化时,
// 将ChannelHandler设置到ChannelPipeline中,用于处理网络IO事件
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 当代客户端链路关闭