Netty
文章目录
Netty的概念
- Netty是一个异步事件驱动的网络应用程序框架用于快速开发可维护的高性能协议服务器和客户端。Netty是一个NIO客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。 它极大地简化并简化了TCP和UDP套接字服务器等网络编程。
ps:简单讲,netty是一个基于协议,支持快速构建客户端、服务端,用于网络io交互的异步非阻塞、高性能、可维护框架。(个人理解)
Netty的异步和NIO异步的区别
-
如果使用传统NIO,其工作量大,bug 多
- 需要自己构建协议
- 解决 TCP 传输问题,如粘包、半包
- 因为bug的存在,epoll 空轮询导致 CPU 100%
-
Netty 对 API 进行增强,使之更易用,如
- FastThreadLocal => ThreadLocal
- ByteBuf => ByteBuffer
Netty服务端和客户端的简单搭建
步骤:
服务端基本代码
package com.wxl.netty.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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.string.StringDecoder;
public class HelloServer {
public static void main(String[] args) {
//1、启动器,负责组装netty组件,启动服务器
new ServerBootstrap()
//2.BossEventLoop,WorkerEventLoop——一个worker只有一对selector和thread
//NioEventLoopGroup包含上面两种loop
.group(new NioEventLoopGroup())
//3、选择 服务器的ServerSocketChannel 实现
.channel(NioServerSocketChannel.class)//除此之外 还有oio、bio
//4、boss 负责处理连接 worker(child)负责处理读写,决定了worker能执行哪些操作(handler)
.childHandler(
//5、channel 代表和客户端进行数据读写的通道Initializer初始化,负责添加别的handler
new ChannelInitializer<NioSocketChannel>() {
@Override//服务建立后该方法才会被调用
protected void initChannel(NioSocketChannel ch) throws Exception {
//6、添加具体handler
ch.pipeline().addLast(new StringDecoder());//将byteBuffer转换为字符串
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){//自定义handler
@Override//读事件
public void channelRead(ChannelHandlerContext ctx,Object msg)throws Exception{
System.out.println(msg);//打印上一步转换好的字符串
}
});
}
}
);
}
}
客户端基本代码
package com.wxl.netty.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//1、启动类
new Bootstrap()
//2、添加EventLoop
.group(new NioEventLoopGroup())
//3、选择客户端channel实现
.channel(NioSocketChannel.class)
//4、添加处理器
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override//在连接建立后被调用
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
//5、连接到服务器
.connect(new InetSocketAddress("localhost",8080))
.sync()
.channel()
//6、向服务器发送数据
.writeAndFlush("hello,world");
}
}
Netty服务端和客户端代码执行顺序
Netty的组件
Event Loop
EventLoop的概念
EventLoop的调试
- 在debug启动客户端时,debug默认suspend为all(意为断点停下来会把所有的线程也停下来)从而导致nio线程关闭客户端无法发送数据,因此需要将其值改为Tread(意为在主线程停下来不会影响其他线程)
- 修改方式:点击红色断点
- 发送数据方式:
处理普通与定时任务
public class TestEventLoop {
public static void main(String[] args) {
// 创建拥有两个EventLoop的NioEventLoopGroup,对应两个线程
EventLoopGroup group = new NioEventLoopGroup(2);
// 通过next方法可以获得下一个 EventLoop
System.out.println(group.next());
System.out.println(group.next());
// 通过EventLoop执行普通任务
group.next().execute(()->{
System.out.println(Thread.currentThread().getName() + " hello");
});
// 通过EventLoop执行定时任务
group.next().scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName() + " hello2");
}, 0, 1, TimeUnit.SECONDS);
// 优雅地关闭
group.shutdownGracefully();
}
}
输出结果
io.netty.channel.nio.NioEventLoop@7bb11784
io.netty.channel.nio.NioEventLoop@33a10788
nioEventLoopGroup-2-1 hello
nioEventLoopGroup-2-2 hello2
nioEventLoopGroup-2-2 hello2
nioEventLoopGroup-2-2 hello2
关闭 EventLoopGroup
优雅关闭 shutdownGracefully 方法。该方法会首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的
对EventLoopGoup进行细分
- 在.group(new NioEventLoopGroup(),new NioEventLoopGroup(线程数));——》线程数最好为cpu核心数*2
- NioEventLoopGroup()设置线程数量,不传参默认线程数为1
- 此处将.group()方法由一个参数变为两个参数,
- 前参:boss 只负责serverSocketChannel上的accept事件
- 后参:worker 只负责socketChannel上的读写
进一步细分,解决耗时长的事件
- 若不进行细分,则遇到耗时长的事件长时间霸占worker,从而影响后来的事件,只能等
- 解决办法:创建一个独立的EventLoopGroup用来解决耗时长的事件
EventLoopGroup group = new DefaultEventLoopGroup();
- new ServerBootstrap()中的实现代码
channel
.write()和.writeAndFlush()
- 掉用write方法,服务端不会立刻输出数据,需再调用flush方法,或缓存满了
- 调用writeAndFlush方法便会立即输出
.sync()同步
- 使用ChannelFuture调用writeAndFlush方法向服务端发送数据,服务端结果无响应
-
- 原因:connect方法由主线程调用,channel的建立由group中的NioEvenLoopGroup线程来建立,由于connect为异步非阻塞,当channel的建立晚于获取channel的方法时便会出现无响应。
- 添加.sync方法便会阻塞等待channel建立号再执行下面的语句
.addListener(回调对象)异步处理结果
- 使用此方法也可解决connect异步非阻塞带来的问题
.close异步
- 关闭channel
小案例循环输入,输入指定值便结束输入
- 创建一个新线程用来处理——循环输入的功能
public class ReadClient {
public static void main(String[] args) throws InterruptedException {
// 创建EventLoopGroup,使用完毕后关闭
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
//添加处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080));
channelFuture.sync();
Channel channel = channelFuture.channel();
Scanner scanner = new Scanner(System.in);
// 创建一个线程用于输入并向服务器发送
new Thread(()->{
while (true) {
String msg = scanner.next();
if ("q".equals(msg)) {
// 关闭操作是异步的,在NIO线程中执行
channel.close();
break;
}
channel.writeAndFlush(msg);
}
}, "inputThread").start();
// 获得closeFuture对象
ChannelFuture closeFuture = channel.closeFuture();
System.out.println("waiting close...");
// 同步等待NIO线程执行完close操作
closeFuture.sync();
// 关闭之后执行一些操作,可以保证执行的操作一定是在channel关闭以后执行的
System.out.println("关闭之后执行一些额外操作...");
// 关闭EventLoopGroup
group.shutdownGracefully();
}
}
同步操作处理线程关闭善后工作
- channel调用closeFuture()
- 方法获得一个closeFuture对象
- 在使用该对象调用sync()方法;只有等到channel关闭后才会往下执行
- 最后再设置自己需要处理的代码,
// 获得closeFuture对象
ChannelFuture closeFuture = channel.closeFuture();
// 同步等待NIO线程执行完close操作
closeFuture.sync();
异步操作处理线程关闭善后工作
- 将sync方法改成addListener方法,并在该方法类重写operationComplete方法,再在此方法中书写善后工作
- 善后工作主要有:关闭所有的线程
- 首先要将group中的NioEventLoop设置为全局变量
- 再在善后代码块中调用shutdownGracefully()方法,该方法会在所有线程处理完现有的事件后将他们关闭
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
// 等待channel关闭后才执行的操作
System.out.println("关闭之后执行一些额外操作...");
// 关闭EventLoopGroup
group.shutdownGracefully();
}
});
出现异常也需要将线程关闭
- 同上
Netty异步的好处
jdkFuture、NettyFuture、promise之间的连系与区别
netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口
netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展
- jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
- netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
jdkFuture
//1、线程池
ExecutorService service = Executors.newFixedThreadPool(2);
注意:future的包不要导错
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@Slf4j
public class TestJdkFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1、线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//2、提交文件
Future<Integer> future= service.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("执行计算");
Thread.sleep(1000);
return 50;
}
});
//3、主线程通过future来获取结果
log.info("等待结果");
log.info("结果是{}",future.get());
System.out.println(future.get());
}
}
NettyFuture
NioEventLoopGroup group = new NioEventLoopGroup();
EventLoop eventLoop = group.next();
同步和异步
import io.netty.channel.EventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
EventLoop eventLoop = group.next();
Future<Integer> future=eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行计算");
Thread.sleep(1000);
return 70;
}
});
//同步
//log.debug("等待结果");
//log.debug("结果是{}",future.get());
//异步
future.addListener(new GenericFutureListener<Future<? super Integer>>(){
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("接收结果:{}",future.getNow());
}
});
}
}
同步
异步
promise
import io.netty.channel.EventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
@Slf4j
public class TestNettyPromise {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1、准备EventLoop对象
EventLoop eventLoop = new NioEventLoopGroup().next();
//2、可以主动创建promise,结果容器
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
new Thread(()->{
//3、任意一个线程执行计算,计算完毕后向promise填充结果
log.debug("开始计算");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
promise.setSuccess(80);
}).start();
//4、接收结果的线程
log.debug("等待结果。。。");
log.debug("结果是:{}",promise.get());
}
}
Handler & Pipeline
pipeline
- 入栈执行顺序与pipeline.addLast()添加得顺序一致
- 出栈操作则从后往前执行,只有调用了writeAndFlush(),出栈操作才会被触发
- super.channelRead(ctx,msg),调用该方法意为将前者得ctx,msg得值传给下一个handle者
- ctx.fireChannelRead(msg),也起到将当前值传到下一个handle
- ctx.writeAndFlush()意为从当前handle向前找输出栈,若为入栈便跳过
- ch.writeAndFlush()意为从pipeline.addLast添加得最后一个输出栈handle开始向前找输出栈
ch调用write得流程
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TestPipeline {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//1、通过channel拿到pipeline
ChannelPipeline pipeline = ch.pipeline();
//2、添加处理器head->h1->h2->h3->tail, h1、h2、h3进栈处理器(执行顺序为从上往下) h4、h5、h6(从后往前)出战处理器
pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception {
log.debug("h1");
super.channelRead(ctx,msg);
}
});
pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("h2");
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h3",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("h3");
super.channelRead(ctx, msg);
//ctx.alloc().buffer()获得一个buffer
ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes()));
}
});
pipeline.addLast("h4",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("h4");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h5",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("h5");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h6",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("h6");
super.write(ctx, msg, promise);
}
});
}
}).bind(8080);
}
}
byte Buf
调试工具
该方法可以帮助我们更为详细地查看ByteBuf中的内容
private static void log(ByteBuf buffer) {
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2)
.append("read index:").append(buffer.readerIndex())
.append(" write index:").append(buffer.writerIndex())
.append(" capacity:").append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf, buffer);
System.out.println(buf.toString());
}
直接内存&堆内存
- 默认为直接内存
- 修改方法
-
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);(堆内存)
-
ByteBuf buffer=ByteBufAllocator.DEFAULT.directBuffer(10);(直接内存)
-
池化vs非池化
池化的最大意义在于可以重用 ByteBuf,优点有
- 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
- 池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
ByteBuf的四部分组成
ByteBuf主要有以下几个组成部分
- 最大容量与当前容量
- 在构造ByteBuf时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX_VALUE
- 当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若超出最大容量,会抛出java.lang.IndexOutOfBoundsException异常
- 读写操作不同于ByteBuffer只用position进行控制,ByteBuf分别由读指针和写指针两个指针控制。进行读写操作时,无需进行模式的切换
- 读指针前的部分被称为废弃部分,是已经读过的内容
- 读指针与写指针之间的空间称为可读部分
- 写指针与当前容量之间的空间称为可写部分