netty项目可以通过maven创建,需要用到io.netty.netty-all的jar包
pom.xml中添加依赖:
io.netty
netty-all
4.1.32.Final
编写一个简单的服务器实现简单地将客户端的内容返回回去
EchoServer.java和EchoServerHandller.java:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 java.net.InetSocketAddress;
public class EchoServer {
private final int port;
public EchoServer(int prot) {
this.port = prot;
}
public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
// 使用NIO的Channel
.channel(NioServerSocketChannel.class)
// 绑定到本地地址的某个端口
.localAddress(new InetSocketAddress(port))
// 为每个channel的pipe line添加handler
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel socketChannel) throws Exception {
// 将handler添加到pipeline中
socketChannel.pipeline().addLast(serverHandler);
}
});
// 绑定,获取future回调
ChannelFuture future = bootstrap.bind().sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new EchoServer(9999).start();
}
}
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server received:" + in.toString(CharsetUtil.UTF_8));
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
可以看到这个Server里用到的了netty里重要的几个组件:
下面分别讲解。
netty的网络传输抽象为Channel接口,因为netty支持多种传输的实现,上面的各种组件也都有实现了不同传输的不同版本。
上面每个版本的Channel只能和对应版本的组件合作,如要用NioServerSocketChannel就必须使用NioEventLoopGroup。接下来再说和Channel相关的组件:
Channel可以调用方法返回pipeling,config和eventLoop的引用,还有一些方法:
isActive:判断Channel的状态是否是活动的
localAddress/remoteAddress:返回本地/远程地址
write/flush/writeAndFlush:将数据传递给pipeline出站
EventLoop是事件循环的抽象。
while (!terminated) {
// 阻塞,直到有事件可以执行
List readyEvents = blockUntilEventReady();
for (Runnable event: readyEvents) {
// 循环遍历处理所有的事件
event.run();
}
}
每个Channel的IO操作和事件都交给同一个EventLoop的线程来处理(写操作可以由其他线程触发,写操作传到pipline时处理仍是该EventLoop来处理),但是一个EventLoop可以绑定多个Channel,(EventLoop能识别分配给自己的线程,如果是分配的线程则执行,否则将任务入队留给其他EventLoop识别)只要每个Channel都是在一个EventLoop的线程里执行IO操作和事件就仍具有线程上的简单性。
不过由于一个EventLoop的多个Channel的ThreadLocal都在一个Thread中,故可能产生冲突要慎用,而且一个EventLoop上执行耗时操作可以会阻塞这个EventLoop上的其他Channel事件,所以单个耗时操作尽量将它交给专门的执行耗时任务的线程池来处理,主线程继续去EventLoop,当子线程计算完毕再讲结果交给主线程。
EventLoopGroup,和EventLoop间接继承了ScheduledExecutorService接口。它们的底层为一个线程池,拥有调度任务的接口方法。而且EventLoop调度任务添加了新的方法如定时任务,解决ScheduledExecutorService接口方法的一些问题:
// 执行一个定时任务
ScheduledFuture> future = channel.eventLoop().schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);
// ...
boolean mayInterruptIfRunnning = false;
// 取消该任务
future.cancel(mayInterruptIfRunnning);
一个EventLoopGroup对应多个EventLoop,每个EventLoop和一个Thread关联。一个EventLoop对应多个Channel。这里server和client有所不同,client有一个EventLoopGroup,而server有两个EventLoopGroup:BossEventLoopGroup和WorkerEventLoopGroup。
BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel的Selector实例,BoosEventLoop不断轮询Selector将连接事件分离出来,通常是OP_ACCEPT事件,然后将accept得到的SocketChannel(ChildChannel)交给WorkerEventLoopGroup,WorkerEventLoopGroup会由next选择其中一EventLoop来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件进行处理。BossEventLoopGroup主要是对多线程的扩展,而每个EventLoop的实现涵盖IO事件的分离,和分发。
一个EventLoopGroup对应多个EventLoop,每个EventLoop和一个Thread关联。一个EventLoop对应一个Channel。
ChannelPipeline 是一系列ChannelHandler的实例,流经一个Channel的事件(Channel生命周期改变和进行读写操作等事件)和可以被ChannelPipeline拦截,然后交给它上面的ChannelHandler来处理。入站和出站方向只会触发该方向上的Handler。
当ChannelPipeline拦截到某个事件时Handler的对应监听方法将被调用,处理事件和数据。
生命周期:
ChannelHandler两个重要的接口:
(各种触发事件可以查看API文档)
ChannelHandler添加到Pipeline时自动为该Handler生成一个对应的Context,Context的监听方法被调用时同时会触发下一个Handler的监听方法,由此实现多个Handler的链式调用。调用context的事件方法将会从下一个Handler开始触发,而Channel,ChannelPipeline的方法会触发整条Handler的监听方法,前者可以避免多余的Handler方法被触发。
这是最常用的类型,ByteBuf将数据存储在JVM的堆空间,通过将数据存储在支撑数组(hasArray方法返回的值是区分ByteBuf类型的方法)中实现的。
Direct Buffer在堆之外直接分配内存,直接缓冲区不会占用堆的容量。
虽然netty的Direct Buffer有这个缺点,但是netty通过内存池来解决这个问题。直接缓冲池不支持数组访问数据,但可以通过间接的方式访问数据数组:
ByteBuf directBuf = Unpooled.directBuffer(16);
if(!directBuf.hasArray()){
int len = directBuf.readableBytes();
byte[] arr = new byte[len];
directBuf.getBytes(0, arr);
}
但是上面的操作太过复杂,所以在使用时如果需要对Buffer进行修改,建议一般是用heap buffer。
不过对于一些IO通信线程中读写缓冲时建议使用DirectByteBuffer,因为这涉及到大量的IO数据读写。对于后端的业务消息的编解码模块使用HeapByteBuffer。
复合缓冲区就类似于一个ByteBuf的聚合视图,在这个视图里面我们可以创建不同的ByteBuf(可以是不同类型的)。 这样,复合缓冲区就类似于一个列表,我们可以动态的往里面添加和删除其中的ByteBuf。
ByteBuf默认第一个字节的索引为0,最后一个字节的索引为capacity()-1。另外ByteBuf维护了两个索引readerIndex和writerIndex,读写操作时会分别移动这两个索引。这些索引将ByteBuf分为了三个区域。
清理可丢弃字节可以使用discardReadBytes(),这个方法将清理可丢弃字节然后将数据向前移动清理的字节数,readerIndex和writerIndex也会移动相同数目的字节。还有一个clear()方法,它直接将readerIndex和writerIndex置0。
ByteBuf拥有查找字节,派生(复制,裁剪),读写等常用操作,netty还提供了一些类来帮助管理字节:
bootstap的功能是将netty的各个部件组装在一起,并运行起来。因为server和client在结构上由所不同,server只需要绑定地址端口而client有绑定和连接两个操作,而且server要为每个请求创建响应的子连接,故netty提供了Bootstrap和ServerBootstrap。
将上面的各个必要的组件(Channel,EventLoopGroup,ChannelHandler等)提供给bootstrap,bootstrap就会自动处理各组件之间的关系,隐藏他们的交互细节。使用bind方法或connect方法进行绑定与连接。
可能需要引导添加多个ChannelHandler时可以在某个Handler的重写方法中获取对应的ChannelPipeline再添加Handler。还可以使用ChannelInitializer类,它继承了ChannelInboundHandlerAdapter类可以使用handler方法或childHandler方法添加到Bootstrap。可以重写initChannel方法在里面获取pipeline再添加Handler。当Channel注册了EventLoop时initChannel方法会被调用。