在讲netty之前我们现总结一下JAVA NIO / JAVA AIO的不足之处。
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。但实际上呢,Netty框架并不只是封装了多路复用的IO模型,也包括提供了传统的阻塞式/非阻塞式 同步IO的模型封装。Netty提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
客户端在第二篇中已经给出。
服务端:
package demo.com.test.io.netty;
import io.netty.bootstrap.ServerBootstrap;
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.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.bytes.ByteArrayDecoder;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.net.InetSocketAddress;
import java.nio.channels.spi.SelectorProvider;
import java.util.concurrent.ThreadFactory;
public class NettyServer {
public static void main(String[] args) throws Exception {
//这就是主要的服务启动器
ServerBootstrap serverBootstrap = new ServerBootstrap();
//=======================下面我们设置线程池
//BOSS线程池
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
//WORK线程池:这样的申明方式,主要是为了向读者说明Netty的线程组是怎样工作的
ThreadFactory threadFactory = new DefaultThreadFactory("work thread pool");
//CPU个数
int processorsNumber = Runtime.getRuntime().availableProcessors();
EventLoopGroup workLoogGroup = new NioEventLoopGroup(processorsNumber * 2, threadFactory, SelectorProvider.provider());
//指定Netty的Boss线程和work线程
serverBootstrap.group(bossLoopGroup , workLoogGroup);
//如果是以下的申明方式,说明BOSS线程和WORK线程共享一个线程池
//(实际上一般的情况环境下,这种共享线程池的方式已经够了)
//serverBootstrap.group(workLoogGroup);
//========================下面我们设置我们服务的通道类型
//只能是实现了ServerChannel接口的“服务器”通道类
serverBootstrap.channel(NioServerSocketChannel.class);
//当然也可以这样创建(那个SelectorProvider是不是感觉很熟悉?)
//serverBootstrap.channelFactory(new ChannelFactory() {
// @Override
// public NioServerSocketChannel newChannel() {
// return new NioServerSocketChannel(SelectorProvider.provider());
// }
//});
//========================设置处理器
//为了演示,这里我们设置了一组简单的ByteArrayDecoder和ByteArrayEncoder
//Netty的特色就在这一连串“通道水管”中的“处理器”
serverBootstrap.childHandler(new ChannelInitializer() {
/* (non-Javadoc)
* @see io.netty.channel.ChannelInitializer#initChannel(io.netty.channel.Channel)
*/
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
//========================设置netty服务器绑定的ip和端口
serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
serverBootstrap.bind(new InetSocketAddress("0.0.0.0", 83));
//还可以监控多个端口
//serverBootstrap.bind(new InetSocketAddress("0.0.0.0", 84));
}
}
package demo.com.test.io.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.util.AttributeKey;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@Sharable
public class TCPServerHandler extends ChannelInboundHandlerAdapter{
/**
* 日志
*/
private static Log LOGGER = LogFactory.getLog(TCPServerHandler.class);
/**
* 每一个channel,都有独立的handler、ChannelHandlerContext、ChannelPipeline、Attribute
* 所以不需要担心多个channel中的这些对象相互影响。
* 这里我们使用content这个key,记录这个handler中已经接收到的客户端信息。
*/
private static AttributeKey content = AttributeKey.valueOf("content");
/* (non-Javadoc)
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelRead(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
TCPServerHandler.LOGGER.info("channelRead(ChannelHandlerContext ctx, Object msg)");
/*
* 我们使用IDE工具模拟长连接中的数据缓慢提交。
* 由read方法负责接收数据,但只是进行数据累加,不进行任何处理
* */
ByteBuf byteBuf = (ByteBuf)msg;
try {
StringBuffer contextBuffer = new StringBuffer();
while(byteBuf.isReadable()) {
contextBuffer.append((char)byteBuf.readByte());
}
//加入临时区域
StringBuffer content = ctx.channel().attr(TCPServerHandler.content).get();
if(content == null) {
content = new StringBuffer();
ctx.channel().attr(TCPServerHandler.content).set(content);
}
content.append(contextBuffer);
} catch(Exception e) {
throw e;
} finally {
byteBuf.release();
}
}
/* (non-Javadoc)
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelReadComplete(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
TCPServerHandler.LOGGER.info("super.channelReadComplete(ChannelHandlerContext ctx)");
/*
* 由readComplete方法负责检查数据是否接收完了。
* 和之前的文章一样,我们检查整个内容中是否有“over”关键字
* */
StringBuffer content = ctx.channel().attr(TCPServerHandler.content).get();
//如果条件成立说明还没有接收到完整客户端信息
if(content.indexOf("over") == -1) {
return;
}
//当接收到信息后,首先要做的的是清空原来的历史信息
ctx.channel().attr(TCPServerHandler.content).set(new StringBuffer());
//准备向客户端发送响应
ByteBuf byteBuf = ctx.alloc().buffer(1024);
byteBuf.writeBytes("您好客户端:我是服务器,这是回发响应信息!".getBytes());
ctx.writeAndFlush(byteBuf);
/*
* 关闭,正常终止这个通道上下文,就可以关闭通道了
* (如果不关闭,这个通道的回话将一直存在,只要网络是稳定的,服务器就可以随时通过这个回话向客户端发送信息)。
* 关闭通道意味着TCP将正常断开,其中所有的
* handler、ChannelHandlerContext、ChannelPipeline、Attribute等信息都将注销
* */
ctx.close();
}
}
//BOSS线程池
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
BOSS线程池实际上就是JAVA NIO框架中selector工作角色(这个后文会详细讲),针对一个本地IP的端口,BOSS线程池中有一条线程工作,工作内容也相对简单,就是发现新的连接;Netty是支持同时监听多个端口的,所以BOSS线程池的大小按照需要监听的服务器端口数量进行设置就行了。
//Work线程池
int processorsNumber = Runtime.getRuntime().availableProcessors();
EventLoopGroup workLoogGroup = new NioEventLoopGroup(processorsNumber * 2, threadFactory, SelectorProvider.provider());
这段代码主要是确定Netty中工作线程池的大小,这个大小一般是物理机器/虚拟机器 可用内核的个数 * 2。work线程池中的线程(如果封装的是JAVA NIO,那么具体的线程实现类就是NioEventLoop)都固定负责指派给它的网络连接的事件监听,并根据事件状态,调用不同的ChannelHandler事件方法。而最后一个参数SelectorProvider说明了这个EventLoop所使用的多路复用IO模型为操作系统决定。
serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
option方法,可以设置这个ServerChannel相应的各种属性(在代码中我们使用的是NioServerSocketChannel);childOption方法用于设置这个ServerChannel收到客户端事件后,所生成的新的Channel的各种属性(代码中,我们生成的是NioSocketChannel)。详细的option参数可以参见ChannelOption类中的注释说明。
在前文介绍JAVA对多路复用IO技术的支持中,我们说过,Selector可以是在主线程上面操作,也可以是一个独立的线程进行操作。在Netty中,这里的部分工作就是交给BOSS线程做的。BOSS线程负责发现连接到服务器的新的channel(SocketServerChannel的ACCEPT事件),并且将这个channel经过检查后注册到WORK连接池的某个EventLoop线程中。
而当WORK线程发现操作系统有一个它感兴趣的IO事件时(例如SocketChannel的READ事件)则调用相应的ChannelHandler事件。当某个channel失效后(例如显示调用ctx.close())这个channel将从绑定的EventLoop中被剔除。
在Netty中,如果我们使用的是一个JAVA NIO框架的封装,那么进行这个循环的是NioEventLoop类(实现多路复用的支持时)。参见该类中的processSelectedKeysPlain方法 和 processSelectedKey方法。另外在这个类中Netty解决了之前我们说到的java nio中”Selector.select(timeout) CPU 100%” 的BUG和一个“NullPointerException in Selector.open()”(http://bugs.java.com/view_bug.do?bug_id=6427854)的BUG
processSelectedKeysPlain方法
for (;;) {
final SelectionKey k = i.next();
final Object a = k.attachment();
i.remove();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask task = (NioTask) a;
processSelectedKey(k, task);
}
if (!i.hasNext()) {
break;
}
if (needsToSelectAgain) {
selectAgain();
selectedKeys = selector.selectedKeys();
// Create the iterator again to avoid ConcurrentModificationException
if (selectedKeys.isEmpty()) {
break;
} else {
i = selectedKeys.iterator();
}
}
}
processSelectedKey方法:
if (!k.isValid()) {
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
// Connection already closed - no need to handle write.
return;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
一个Work线程池的线程将按照底层JAVA NIO的Selector的事件状态,决定执行ChannelHandler中的哪一个事件方法(Netty中的事件,包括了channelRegistered、channelUnregistered、channelActive、channelInactive等事件方法)。执行完成后,work线程将一直轮询直到操作系统回复下一个它所管理的channel发生了新的IO事件。
Netty uses its own buffer API instead of NIO ByteBuffer to represent a sequence of bytes. This approach has significant advantages over using ByteBuffer. Netty’s new buffer type, ChannelBuffer has been designed from the ground up to address the problems of ByteBuffer and to meet the daily needs of network application developers. To list a few cool features:
You can define your own buffer type if necessary.
Transparent zero copy is achieved by a built-in composite buffer type.
A dynamic buffer type is provided out-of-the-box, whose capacity is expanded on demand, just like StringBuffer.
There’s no need to call flip() anymore.
It is often faster than ByteBuffer.
上面的引用来自于JBOSS-Netty官方文档中,对ByteBuf缓存的解释。翻译成中文就是:Netty重写了JAVA NIO框架中的缓存结构,并将这个结构应用在更上层的封装中。
为什么要重写呢?JBOSS-Netty给出的解释是:我写的缓存比JAVA中的ByteBuffer牛。
这里说一说Netty中几个比较特别的ByteBuf实现:
Channel,通道。您可以使用JAVA NIO中的Channel去初次理解它,但实际上它的意义和JAVA NIO中的通道意义还不一样。我们可以理解成:“更抽象、更丰富”。如下如所示:
Abstract base class for Channel implementations that use Old-Blocking-IO
您可以这样理解:Netty的Channel更具业务抽象性。Netty中的每一个Channel,都有一个独立的ChannelPipeline,中文称为“通道水管”。只不过这个水管是双向的里面流淌着数据,数据可以通过这个“水管”流入到服务器,也可以通过这个“水管”从服务器流出。
在ChannelPipeline中,有若干的过滤器。我们称之为“ChannelHandler”(处理器或者过滤器)。同“流入”和“流出”的概念向对应:用于处理/过滤流入数据的ChannelHandler,称之为“ChannelInboundHandler”;用于处理/过滤流出数据的ChannelHandler,称之为“ChannelOutboundHandler”。
在上文给出的服务端示例代码中,书写的业务处理器TCPServerHandler就是继承了ChannelInboundHandlerAdapter适配器。下面,我们将介绍几个常使用的ChannelInboundHandler处理器和ChannelOutboundHandler处理器
上面讲到了Netty中的重要概念。我们花很大篇幅讲解了Channel、ChannelPipeline、ChannelHandler,以及他们的联系和工作方式。
在说到ChannelInHandler为什么会使用“适配器”模式的时候,特别指出了原因:因为ChannelInHandler接口中的方法加上父级接口中的方法,总共有11个接口事件方法需要实现。而事实上很多时候我们只会关心其中的一个或者两个接口方法。
那么这些方法是什么时候被触发的呢?这就要说到Netty中一个Channel的生命周期了(这里我们考虑的生命周期是指Netty对JAVA NIO技术框架的封装):
这里有一个channel事件没有在图中说明,就是exceptionCaught(ChannelHandlerContext, Throwable)事件。只要在调用图中的所有事件方法时,有异常抛出,exceptionCaught方法就会被调用。
另外,不是channelReadComplete(ChannelHandlerContext)方法调用后就一定会调用channelInactive事件方法。channelReadComplete和channelRead是可以反复调用的,只要客户端有数据发送过来。
最后补充一句,这个生命周期的事件方法调用顺序只是针对Netty封装使用JAVA NIO框架时,并且在进行TCP/IP协议监听时的事件方法调用顺序。
阻塞和非阻塞:这个概念是针对应用程序而言,是指应用程序中的线程在向操作系统发送IO请求后,是否一直等待操作系统的IO响应。如果是,那么就是阻塞式的;如果不是,那么应用程序一般会以轮询的方式以一定周期询问操作系统,直到某次获得了IO响应为止(轮序间隔应用程序线程可以做一些其他工作)。
同步和异步:IO操作都是由操作系统进行的(这里的IO操作是个广泛概念了:磁盘IO、网络IO都算),不同的操作系统对不同设备的IO操作都有不同的模式。同步和异步这两个概念都指代的操作系统级别,同步IO是指操作系统和设备进行交互时,必须等待一次完整的请求-响应完成,才能进行下一次操作(当然操作系统和设备本身也有很多技术加快这个反应过程,例如“磁盘预读”技术、数据缓存技术);异步IO是指操作系统和设备进行交互时,不必等待本次得到响应,就可以直接进行下一次操作请求。设备处理完某次请求后,会主动给操作系统相应的响应通知。
以上这些IO工作模型,在JAVA中都能够找到对应的支持:传统的JAVA Socket套接字支持阻塞/非阻塞模式下的同步IO(有的技术资料里面也称为OIO或者BIO);JAVA NIO框架在不同操作系统下支持不同种类的多路复用IO技术(windows下的select模型、Linux下的poll/epoll模型);JAVA AIO框架支持异步IO(windows下的IOCP和Linux使用epoll的模拟AIO)
实际上Netty是对JAVA BIO 、JAVA NIO框架的再次封装。让我们不再纠结于选用哪种底层实现。您可以理解成Netty/MINA 框架是一个面向上层业务实现进行封装的“业务层”框架。而JAVA Socket框架、JAVA NIO框架、JAVA AIO框架更偏向于对下层技术实现的封装,是面向“技术层”的框架。
“技术层”框架本身只对IO模型技术实现进行了封装,并不关心IO模型中流淌的数据格式;“业务层”框架对数据格式也进行了处理,让我们可以抽出精力关注业务本身。