学习Netty最好的方法就是读官方文档http://netty.io/wiki/user-guide-for-4.x.html#wiki-h3-10。
下面我就文档里面的时间服务器例子,对Netty的基本应用进行一下总结。
Netty是基于NIO的通讯框架,用它开发应用的步骤要比直接使用NIO简洁很多。我们先来回忆一下NIO开发TimeServer的步骤:
(1)创建ServerSocketChannel,配置它为非阻塞模式。
private ServerSocketChannel serverChannel;
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
(2)绑定监听,配置TCP参数,例如backlog大小
serverChannel.socket().bind(new InetSocketAddress(port), 1024);
(3)创建一个独立的IO线程,用于轮询多路复用器(选择器)Selector
(4)创建Selector,将之前注册的ServerSocketChannel注册到Selector上,监听SelectionKey.ACCEPT
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
(5)启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的Channel
while (!stop) {
try {
selector.select(1000);
Set selectionKeys = selector.selectedKeys();
Iterator it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
(6)当轮询到处于就绪状态的Channel是,需要对其进行判断,如果是OP_ACCEPT状态,说明是新的客户端接入,则调用ServerSocketChannel.accept()方法接收新的客户端
(7)设置新接入的客户端链路SocketChannel为非阻塞模式,配置其他的一些TCP参数
(8)把SocketChannel注册到Selector,并监听OP_READ操作位
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
(9)如果轮询的Channel为OP_READ,则说明SocketChannel中有新的就绪的数据包需要读取,则构造ByteBuf对象,读取数据包
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server recieve order:" + body);
String currentTime = "QUERY".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(sc, currentTime);
} else if (readBytes < 0) {
//对端链路关闭
key.channel();
sc.close();
} else {
;//读到0字节,忽略
}
}
(10)如果轮询的Channel为OP_WRITE,说明还有数据没有发送完成,需要继续发送。
So,NIO完成上面最基本的消息发送的步骤还是挺多的。下面我们来看一下Netty是怎样简化的。
TimeServer:
public final class TimeServer {
static int PORT = 8989;
static final boolean SSL = System.getProperty("ssl") != null;
public static void main(String[] args) throws Exception {
//Configure SSL
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslCtx = null;
}
//配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childHandler(new ChannelInitializer() { //为accept channel的pipeline预添加的inboundhandler
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(sslCtx.newHandler(LogLevel.INFO))
p.addLast(new TimeServerHandler());//为当前的channel的pipeline添加自定义的处理函数
}
});
//绑定端口,同步等待成功,服务器启动
ChannelFuture f = b.bind(PORT).sync();
//等待服务器监听端口关闭
f.channel().closeFuture().sync();
} finally {
//优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
同样地,我们一步步的看TimeServer的搭建过程。在现在没有深入了解源码的情况下,对一些Netty的类库和用法进行基础的介绍。
(1)创建两个NioEventLoopGroup实例。NioEventLoopGroup是一个线程组,它包含了一组NIO线程,专门用于网络事件的处理,实际上它们就是Reactor线程组。这里创建两个的原因是一个用于服务端接收客户端的连接,一个用于进行SocketChannel的读写。
(2)创建ServerBootstrap对象,它是netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度。当然,这里没看源码也不知道它怎么降低的。
(3)调用ServerBootstrap的group()方法,将两个NIO线程传入到ServerBootstrap中。
(4)设置创建的Channel为NioServerSocketChannel,它的功能对应于JDK NIO类库中的ServerSocketChannel类。
(5)然后配置NIOServerSocketChannel的TCP参数,此处将它的backlog(最大队列长度)设置为100。
(6)配置ChildHandler,作用类似于Reactor模式中的Handler类,主要用于处理网络IO事件,例如记录日志,对消息进行编解码等。
(7)服务端启动辅助类完成配置后,调用它的bind()方法就可以绑定监听端口,随后,调用它的同步阻塞方法sync等待绑定操作完成。
(8)完成后Netty会返回一个ChannelFuture,主要用于异步操作的通知回调。
(9)f.channel().closeFuture().sync()进行阻塞,直到服务端链路关闭之后才推出main函数。
(10)释放资源
TimeServerHandler
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
final ByteBuf time = ctx.alloc().buffer(4);
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
final ChannelFuture f = ctx.writeAndFlush(time);
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
assert f == future;
ctx.close();
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
(1)继承ChannelInboundHandlerAdapter:
Abstract base class for ChannelInboundHandler implementations which provide implementations of all of their methods.
This implementation just forward the operation to the next ChannelHandler in the ChannelPipeline. Sub-classes may override a method implementation to change this.
(2)重写方法:
channelActive():通道激活时触发,当客户端connect成功后,服务端就会接收到这个事件,从而可以把客户端的Channel记录下来,供后面复用
channelRead():当收到对方发来的数据后,就会触发,参数msg就是发来的信息,可以是基础类型,也可以是序列化的复杂对象.
channelReadComplete():channelRead执行后触发
exceptionCaught():出错时会触发,做一些错误处理
public class TimeClient {
public static void main(String[] args) throws InterruptedException {
final String host = "127.0.0.1";
final int port = 8989;
//配置客户端NIO线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
//发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
//等待客户端关闭
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
可以看到,客户端的代码和服务端的代码很像,不同点如下:
(1)客户端只创建了一个NioEventLoopGroup对象,用以相应IO事件
(2)设置创建的Channel为NioSocketChannel,它的功能对应于JDK NIO类库中的SocketChannel类。
(3)然后配置NIOSocketChannel的TCP参数为ChannelOption.SO_KEEPALIVE,服务端是SO_BACKLOG
使用netty框架步骤确实简单了很多,但要深入学习netty,就必须研读它类库的源码,清楚它们的原理,所以后面我会陆续记录的。