Netty简介
Netty封装了JAVA NIO的底层网络通信库,并依照网络编程Reactor的设计模式,实现了一套易于开发者使用的高性能并发网络开发构架,现已广泛应用于许多的大于项目,如Spark、Kafka等,开发者可以很容易很通过Netty官方文档或github官网找到诸多示例代码,这篇文章仅仅是依官方示例简单拆解,为求更加通俗。
文章中的描述有时会涉及一些Netty构架的设计及实现,如果有些地方不易理解,还请读者浏览本人其它相关博文。
Server端的构建流程
1. 创建EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100);
如上的代码,通过创建一个ServerBootstrap类,引导用户创建一个能够以多线程的方式,来处理Socket通信的服务器实例。
EventLoop:可以认为就是一个无限循环的线程,可以处理Netty中的SocketChannel的连接、读取和写入。当一个新的连接打开时,Netty会从workerGroup中按顺序或是按规则挑选一个EventLoop并返回,然后将这个新的Netty Channel注册到这个EventLoop实例上,实际上就是更新Channel的EventLoop引用为挑选出来的实例上;然后调用自己的register(…)方法将自己注册到这个EventLoop绑定的Selector上,同时也会发送fireChannelRegistered(…)消息给所有注册在这个Channel上的handler。
EventLoopGroup:内部保存了多个EventLoop的实例,可以认为就是一个线程池。
ServerBootstap:可以通过group(…)方法,指定两个EventLoopGroup的实例,分别用来为Selector的处理和Channel的处理指定工作空间。至于什么是Selector处理和Channel处理,请见后面消息处理流程的分析。
NioServerSocketChannel:Netty中定义的类,专门用于ServerBootstrap的构建,它内部封装了JAVA NIO ServerSocketChannel,负责接收客户端新连接,创建相应的NioSocketChannel实例,然后在发生读操作时将新生成的NioSocketChannel注册到WorkerGroup上处理。一个NioServerSocketChannel实例,监听一个端口,并注册在BossGroup的某个EventLoop上,当底层JAVA NIO SocketChannel上的发生读写操作时,利用类继承的特性,实现SocketChannel读写从BossGroup线程池到WorkerGroup线程池的迁移。
2. 创建自定义消息处理器
用户可以通过继承ChannelInboundHandlerAdapter类来定义消息的处理流程,如下的代码仅仅是将收到的消息对象打印,然后再通过ChannelHandlerContext实例返回给Client端。
ChannelInboundHandlerAdapter:消息接收端的处理器,这个是Netty的默认实现类,可以响应底层SocketChannel产生的各种事件,如读消息事件、读消息完成事件、yfSocketChannel异常处理等。Nettry中所有带有InboundHandler关键字的类,表示处理消息的流入,相反的带有OutboundHandler关键字的类处理消息的流出。
ChannelHandlerContext:内部保存了底层SocketChannel的引用,因此可以通过调用此实例的write(…)方法将消息写出到底层的Socket通道。
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.print(msg);
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
3. 注册消息处理器
用户可以定义许多消息处理器类,并将这些处理器对象绑定到底层的SocketChannel实例上,这种绑定关系需要用户在创建ServerBootstrap实例时,通过handler(…)和childHandler(…)方法指明。
例如下面的代码首先调用handler(…)方法,为bossGroup绑定了一个LoggingHandler类的实例,表示bossGroup中的线程在处理收到的事件时,事件会流经这个对象,然后根据事件的不同执行LoggingHandler实现的不同方法,这里仅仅是打印日志信息,就像EchoServerHandler的行为。
然后调用了ServerBootstrap.childHandler(…)方法,为workerGroup绑定了个消息处理器实例,分别是LoggingHandler和EchoServerHandler,表示workerGroup中的线程在处理收到的事件时,事件会流经这两个类,然后根据事件的不同执行它们的不同方法。其中EchoServerHandler实例在处理Channel的读取事件时,打印接收到的消息,然后再返回给客户端,而LoggingHandler则打印所有事件的日志。
NioEventLoop类前面已经提到过,其内部会保存一个JAVA NIO中的Selector的实例,用于Socket Channel各种状态的检测。boosGroup中的线程就是用来循环调用绑定的Selector的方法,获取注册在这个Selector上的JAVA SocketChannel的状态,比如某个JAVA SocketChannel的状态是OP_WRITE,表示底层的SocketChannel可以写出数据,因此就会将在这个通道上所有等待被发送的数据,通过调用JAVA的SocketChannel.write(…)方法,写入到Socket缓存区。
NioServerSocketChannel:这个类是在Server端创建Bootstrap时指定的用于包装JAVA SocketChannel实例的包装类,由于Netty使用的是主从线程模型,因此可以通过继承同一个Netty中的Channel基类,很方便地将bossGroup线程中要进行读写处理的Channel,转换成一个新NioSocketChannel类型,然后重新注册到workerGroup上,交由workerGroup中的某个EventLoop实例执行,从而实现主从分离。
b.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});
4. 启动监听
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
5. 关闭Group
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
完整的代码示例
/**
* Echoes back any received data from a client.
*/
public final class EchoServer {
static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
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;
}
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
Client端的构建流程
基本过程与Server端相同,主要的不同是使用Bootstrap引导构建,并指定工作的Socket通道类型为NioSocketChannel,完整的客户端代码示例如下:
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf firstMessage;
/**
* Creates a client-side handler.
*/
public EchoClientHandler() {
firstMessage = Unpooled.buffer(EchoClient.SIZE);
for (int i = 0; i < firstMessage.capacity(); i ++) {
firstMessage.writeByte((byte) i);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
public final class EchoClient {
static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));
public static void main(String[] args) throws Exception {
// Configure SSL.git
final SslContext sslCtx;
if (SSL) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
// Configure the client.
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
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(HOST, PORT).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully();
}
}
}
NioSocketChannel:与NioServerSocketChannel有相同的父类,Netty中通过继承的方式最大化地复用代码,并利用多态特性为这两个Channel实现类定义不同的行为,即NioSocketChannel的read(…)/write(…)方法是真正地读写操作系统Socket缓存区,而NioSocketServerChannel只负责read(…),其实现实际上是将读的工作委托给NioSocketChannel实例,并在WorkerGroup上完成。