Netty是一款用于创建高性能网络应用程序的框架,它相对于原生的Java API更加容易使用,更加高效,性能更好。本文以简单的示例着手,简介Netty的基本概念以及入门使用。
Netty提供异步的(非阻塞I/O),基于事件驱动的,它包含了一组设计模式,将应用程序逻辑从网络层解耦。得益于它的池化与复用,具有更高的性能,更低的资源消耗。
在传统的同步阻塞式I/O模型中,通常由一个独立的Acceptor线程负责监听客户端的请求,并且在服务端为每一个客户端连接创建一个线程来响应对应的时间,完成该响应后,服务端销毁该处理线程。这是缺乏弹性的设计,在并发量较大的情况下,服务端的性能将急剧下降,可能发生线程堆栈溢出,无法创建线程等问题。一种改进的方案是将服务端创建的线程池化,避免不断创建和销毁线程带来的开销,同时避免创建过多的线程而耗尽系统资源。
JDK新引入的NIO提供了非阻塞的I/O模型,将网络数据通过全双工的Channel读取和写入,通过多路复用器Selector不间断地轮询注册在其上的Channel(一个Selector可以同时轮询多个Channel),如果Channel发生读写操作,这个Channel就会处于就绪状态,从而被Selector轮询出来进行后续的I/O操作。NIO为高性能网络编写提供了可能,但是相对于Netty,它使用复杂,开发者需要出来各种可能的异常(如请求重连,半包读写,网络拥塞等),总体使用难度较大,开发效率较低。
Netty核心概念
- Channel:可以把Channel看作是传入(入站)或者传出(出站)数据的载体
- 回调:主要用于在事件发生时,回调相应的方法以响应事件的处理
- Future:Future提供了另一种在操作完成时通知应用程序的方式。
这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。Netty实现ChannelFuture用于在执行异步操作的时候使用,可以向ChannelFuture注册监听实例,当相应的事件发生时,它们会被触发回调。 - 事件和EventHandler: Netty使用事件来通知状态的改变或者操作的状态。
数据的入站和出站都会触发一些不同的事件,例如Channel被激活,数据读取,读取完毕,数据刷写完成,通道关闭等。EventHandler用来响应这些事件并触发相应的方法回调。 - ChannelPipeline:提供了ChannelHandler链的容器,你可以向Channel中注册多个不同的ChannelHandler,在数据入站或出站时,会被ChannelPipeline中的ChannelHandler依次“截获”并处理。
- ByteBuf:Netty传输快也依赖了NIO的一个特性——零拷贝。在netty里面通过ByteBuf可以直接将数据从IO读到内存中(不必经历中间的socket缓冲区),从而加快了传输速度。
- EventLoop及线程模型:在内部,将会为每个Channel分配一个EventLoop,用以处理所有事件。EventLoop有线程驱动,用来处理Channel提交的事件。一个EventLoopGroup包含一个或者多个EventLoop,用以提高并发效率。
示例代码
服务端
//服务端驱动程序
//TimeServer.java
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;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
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 ChildChannelHandler());
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new LineBasedFrameDecoder(1024));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 8085;
new TimeServer().bind(port);
}
}
/*
*服务端事件处理器TimeServerHandler
*服务端收到客户端请求后,返回当前的时间
*/
import java.util.Date;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class TimeServerHandler extends ChannelInboundHandlerAdapter
{
private int counter = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
{
String body = (String) msg;
System.out.println("The time server receive order:" + body + ";the counter is:" + (++counter));
String curTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString():"BAD ORDER";
curTime = curTime + System.getProperty("line.separator");
ByteBuf rsp = Unpooled.copiedBuffer(curTime.getBytes());
ctx.writeAndFlush(rsp);
ReferenceCountUtil.release(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
{
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{
ctx.close();
}
}
客户端
//客户端驱动
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;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class TimeClient
{
public void connect(int port,String host) throws Exception
{
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 sc) throws Exception {
sc.pipeline().addLast(new LineBasedFrameDecoder(1024));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture f = b.connect(host,port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception
{
int port = 8085;
new TimeClient().connect(port, "127.0.0.1");
}
}
//客户端事件处理器
//每隔一段时间向服务端发送一个请求
import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class TimeClientHandler extends ChannelInboundHandlerAdapter
{
private int counter;
private byte[] req;
public TimeClientHandler()
{
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx)
{
ByteBuf msg = Unpooled.buffer(req.length);
msg.writeBytes(req);
ctx.writeAndFlush(msg);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException, InterruptedException
{
String body = (String)msg;
System.out.println("Now is :" + body + ";the counter is:" + (++counter));
ByteBuf r = Unpooled.buffer(req.length);
r.writeBytes(req);
ctx.writeAndFlush(r);
TimeUnit.SECONDS.sleep(1);
ReferenceCountUtil.release(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
{
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{
cause.printStackTrace();
ctx.close();
}
}
代码释疑
NioEventLoopGroup包含若干EventLoop用于调度线程进程真正的事件处理。在这里,没有为NioEventLoopGroup指定线程数,默认情况线程为DEFAULT_EVENT_LOOP_THREADS(线程数最小为1,如果配置了系统参数io.netty.eventLoopThreads,设置为该系统参数值,否则设置为核心数的2倍。)
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
在服务端创建了两个NioEventLoopGroup对象,这是因为服务器需要两组不同的Channel,其中bossGroup用来接受客户端传入的连接,workerGroup用户处理已经由bossGroup接受的连接。
ServerBootstrap这是一个辅助类,在服务端这个辅助类名为ServerBootstrap,在客户端其名为Bootstrap。ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
通过ServerBootstrap可以配置:
1.传输通道的类型,这里服务端使用的是NioServerSocketChannel,而客户端使用的是NioSocketChannel,它们都是异步非阻塞的。
2.childHandler,用于让channel创建注册事件处理器,数据的入站和出站将会依据这些处理器的注册顺序依次处理。
3.其他:option:配置channel选项参数,bind:绑定到指定地址端口等。
在这里定义了三个channel处理事件,前两个(LineBasedFrameDecoder, StringDecoder)是用于处理半包读写的问题,因为由服务器发送的消息可能会被分块接收。作为一个面向流的协议,TCP 保证了字节数组将会按照服务器发送它们的顺序被接收,但若不经过特殊处理,通信一方某一次所发的消息不能保证所有的消息都被另一方一次性接收。如果服务器发送了5字节,可能第一次使用一个持有3 字节的ByteBuf(Netty 的字节容器),第二次使用一个持有2 字节的ByteBuf来接收这些数据。当向信道添加额外的LineBasedFrameDecoder时,发送的数据将由换行符界定,可以确保同一行数据一定会被对方一次性接收,不会出现半包读写的问题。
ChannelFuture将服务端绑定到某个端口,然后直接返回,它是一个Future,是异步的。
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
closeFuture动作具体什么时候会被执行取决于若干因素,但当信道被关闭前它肯定会被执行。
你需要关闭EventLoopGroup,它将处理任何挂起的事件和任务,并且随后释放所有活动的线程。这就是调用EventLoopGroup.shutdownGracefully()方法的作用。
关于自定义的事件处理器:
TimeServerHandler和TimeClientHandler都继承了ChannelInboundHandlerAdapter,用来定义当channel上有事件发生时,应该执行哪些相应的动作:
channelRead:对于每一个写入到channel的消息,该方法都会被调用一次。
channelReadComplete:通知ChannelInboundHandler最后一次对channelRead的调用是当前批量读取中的最后一条消息。
exceptionCaught:在读取操作期间,有异常抛出时会被调用。