1.原生NIO存在的问题
原生NIO有一个最大的问题就是epoll bug,它会导致Selector空轮询,最终导致CPU 占用率100%,直到JDK1.7该问题都没有完全解决,其次NIO模型开发的工作量比较大,需要考虑很多问题如客户端断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理
2.Netty的优点
netty是JBoss公司出品的一个开源的java网络应用框架,它是基于NIO的主从Reactor模型的同步非阻塞IO
1.更安全可靠:支持SSL/TLS协议
2.更高效:支持NIO的零拷贝,更加高效的socket底层
3.更灵活:支持多种decoder encoder,可以对TCP粘包分包自动化,可以接受处理线程池,支持重连心跳检测,可配置化IO线程、TCP的参数、TCP接受发送缓冲区的参数等
3.Netty原理详解
1.Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写且BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,相当于一个事件循环组, 这个组中含有多个NioEventLoop事件循环,它是一个可以不断去循环处理任务的线程,每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
2.Boss的NioEventLoop 循环执行的步骤有3步
(1)轮询accept 事件
(2)处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector
(3)处理任务队列的任务 , 即 runAllTasks
3.每个 Worker NIOEventLoop 循环执行的步骤
(1)轮询read, write 事件
(2)处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
(3)处理任务队列的任务 , 即 runAllTasks
4.每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器
4.Netty入门编程
- 客户端
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
- 服务端
public class NettyServer {
public static void main(String[] args) throws Exception {
//1.创建两个线程组 bossGroup 和 workerGroup 其中bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成两个都是无限循环 bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//2.创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//3.使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
.handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个道初始化对象(匿名对象)
//给work关联的pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听,当监听到关闭事件时才会关闭
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class ServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
}
//接收数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(ctx.channel().remoteAddress()+":消息:"+msg);
ctx.writeAndFlush(msg);
}
}
5.代码描述
客户端与服务端均由各自的启动辅助类BootStrap开始,开始执行链式编程,其实就是类似于get与set方法,设置一些参数而已,重要的就是.group .channel .childHandler这三个必须设置好,其中前两个是在给BossGroup做配置,后面的childHandler根据名字就知道是给子事件循环组即WorkGroup做配置,Netty模型中也是work做真实的事件处理,除此之外你还可以.option或者childoption(依然父子关系)指定超时或者tcp层缓冲区大小等等的设置,详细说几个配置:
.channel(NioServerSocketChannel.class) //代表服务端以NioServerSocketChannel作为通道类型
.childHandler(new ChannelInitializer() //ChannelInitializer是用于channel注册到eventLoop后对其实例化的一个类,其继承于ChannelInboundHandler接口,是一个抽象类不能直接使用,要实现其中的 initChannel抽象方法,指定其类型为NioSocketChannel,因为workGroup最终是要与Client连接的,所以你会发现NettyClient中的也是NioSocketChannel类型。
而initChannel方法就是去对Handler的设置,首先需要去创建ChannelPipeline容器(源码中也是这样),这个容器是一个双向链表,用addLast方法添加各种我们需要的处理器,编码器解码器以及我们自定义的Handler等等
其中详细说一下ChannelPipeline:
Channelpipeline是一个双向链表,ChannelHandlerContext看作链表的节点,ChannelHandler则为每个节点中保存的一个属性对象
那其实这里的ChannelHandler其实就是节点中具体放的东西,像编码器解码器这些,以及这里最重要的我们自己编写的SeverHandler,这个接口本身没有太多方法,是以继承重写的形式,因而我们自己编写SeverHandler也是一样,去继承重写相关方法即可,那么继承什么呢?这里得先说一下,既然ChannelPipeline是一个双向链表,那么就可以来回前后两个方向去运行,我们把从head到tail即
ChannelInboundHandlerAdapter或者SimpleChannelInboundHandler去重写相关方法