本文是对netty主要的一些知识点做一个总结,因为netty的底层还是Nio,所以建议有学过Nio的伙伴们再看本文,如果没有Nio基础的小伙伴可以点击链接去看这一篇文件了解一下,这篇文章对Nio也做了比较详细的讲解。
本文主要知识来源: 哔哩哔哩_netty视频 , 下面开始步入正题。
入门
netty底层采用多路复用技术,是异步的。底层也是采用的NIO。
入门代码
要使用netty,第一步当然是导入坐标
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
然后创建服务器端的代码
package com.hs.nettyPrimary;
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;
/**
1. netty入门,服务器端代码
2. @author hs
3. @date 2021/07/16
*/
public class HelloServer {
public static void main(String[] args) {
// 服务器端的启动器 负责组装netty的组件 并协调它们工作
new ServerBootstrap()
// 添加一个EventLoop组 一个Selector+Thread = EventLoop loop循环 event事件 也就是循环处理事件 group组
.group(new NioEventLoopGroup())
// 选择服务器的ServerSocketChannel实现
.channel(NioServerSocketChannel.class)
// child就相当于之前处理read事件的worker childHandler()方法的作用就是决定了child能干什么事情
// 方法的参数 ChannelInitializer 就代表跟客户端进行读写的通道 它其实也是一个handler 它的作用的加载其他的handler
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加具体的handler StringDecoder 解码 因为传输都是用的ByteBuffer 这里是将ByteBuffer转换为String
nioSocketChannel.pipeline().addLast(new StringDecoder());
// 自定义handler 处理一些自定义的事情
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端接收的参数" + msg);
}
});
}
})
// 端口
.bind(8080);
}
}
服务端的大致流程就是:
- 创建一个ServerBootstrap() 服务器的启动器
- 添加NioEventloopGroup组
- 选择ServerSocketChannel的实现
- 添加child的处理器,这里仅仅是添加,执行会在客户端连接服务器后执行
- 绑定端口
客户端的代码和服务器端很相似
package com.hs.nettyPrimary;
import io.netty.bootstrap.Bootstrap;
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.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @author hs
* @date 2021/07/17
*/
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
// 这里也是先创建一个启动器,这是客户端的启动器,注意导包不要导错了
new Bootstrap()
// 这是添加EventLoop的作用主要是体现在服务器端,因为服务器要开多个线程充当boss和worker
.group(new NioEventLoopGroup())
// 选择客户端SocketChannel的实现
.channel(NioSocketChannel.class)
// 添加处理器,下面的方法会在客户端与服务器端连接成功后调用,会调用initChannel()方法
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
// 服务器那边要接收数据,接收的是字节 就需要使用decoder来编码
// 而客户端这边是发送的字符串,要编码成字节发送,所以这里是使用encoder
socketChannel.pipeline().addLast(new StringEncoder());
}
})
// 连接服务器
.connect("localhost",8080)
// 阻塞方法,直到客户端与服务器端连接建立后才会往下执行
.sync()
// 代表的客户端与服务器之间数据传输的SocketChannel,netty对它做了封装
.channel()
// 发送数据
.writeAndFlush("hello,netty!");
}
}
这时候服务器与客户端都启动后,客户端就会在连接服务器成功后往服务器发送一个"hello,netty!" 的消息,首先会经过客户端的Handler将字符串编码为字节数组进行传输,服务器端刚开始接受的也是字节数组,服务器端的Handler会先将字节数组解码为字符串,然后再由下一个Handler进行处理,也就是我们自定义的输出语句。具体的执行流程如下。
执行流程
-
添加Handel处理器时,仅仅只是添加,还没有执行,需要等到客户端与服务器端连接成功后才会调用initChannel()初始化处理器,等到读写事件发生了就执行相应的处理器
-
事件发生都会先到EventLoop这里,然后在由Handel处理器来进行实际的处理
-
收发数据都会经过处理器
主要组件的理解
-
服务器端msg:可以理解为流动的数据,服务器这边首先接收的是ByteBuf,然后在经过多个pipeline的处理加工,会变成其他类型的对象
-
channel:数据的通道
-
handler理解为处理数据的工序,工序有多个,合在一起就是pipeline(流水线)。pipeline负责发布事件,传播给每个handler,handler再对自己感兴趣的事情进行处理(在自定义handler中重写了相应的事件处理方法)
handler分为两类:Inbound和Outbound 也就是入站和出站
-
EventLoop:处理数据的工人,因为EventLoop有一个线程,真正做事的也就是这个线程
- 工人可以管理多个channel的io操作,并且一个工人如果选择了一个channel就要负责到底(绑定) ,这里主要也只是绑定io操作
- 工人即可以执行io操作,也可以进行任务处理,每位工人有任务队列,队列里可以堆放多个channel待处理的任务,任务可以分为普通任务和定时任务
- 工人按照pipeline顺序,依次按照handler的规则(代码)处理数据,对于非io操作的工序可以为指定不同的工人
总之,meg是数据,channel的传输数据的通道,handler是处理数据的工序,多个工序合在一起就是pipeline,EventLoop是处理数据的工人。
主要组件
EventLoop
EventLoop本质上就是一个Selector+一个单线程执行器。里面的run方法处理channel上源源不断的io事件
EventLoop它继承两个类
- 第一个是继承netty自己的OrderedEventExecutor接口
- 第二个是继承java.util.concurrent.ScheduledExecutorService接口
我们一般是不会直接用EventLoop 一般是使用EventLoopGroup。
因为我们肯定是需要多个EventLoop,也就相当于是需要多个线程来处理各种事件,而EventLoop本质又是一个单线程执行器。
channel一般会调用EventLoopGroup中的register()方法来绑定其中一个EventLoop,之后这个channel的io事件都又该EventLoop来处理。
创建EventLoopGroup
public static void main(String[] args) {
// 创建EventLoopGroup,它是一个接口,常用的使用就是NioEventLoopGroup
// 它可以处理io事件,普通任务,定时任务
// 还有另一个实现 DefaultEventLoop 它可以处理普通任务,定时任务。适用于一些没有io事件发生的场景
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
}
一般情况我们都没有往构造方法里面写参数,可以写一个int表示要创建几个EventLoop也就是创建几个线程。那默认会创建几个嘞?
在源码中的第一层,这里默认写了0,然后再一直点下去。
public NioEventLoopGroup() {
this(0);
}
然后到了父类MultithreadEventLoopGroup类
public abstract class MultithreadEventLoopGroup ... {
// 这里最后是创建了cpu核数的两倍 NettyRuntime.availableProcessors()就是cpu核数
private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
// 如果为0就采用一个默认的静态常量的值DEFAULT_EVENT_LOOP_THREADS
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
}
可以发现,如果我们指定了EventLoop的数量那就以我们指定的为准,如果没有指定那就是采用的cou核数*2
执行普通任务和定时任务
package com.hs.nettyPrimary;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @author hs
* @date 2021/07/17
*/
@Slf4j
public class EventLoopGroupTest {
public static void main(String[] args) {
// 创建EventLoopGroup,它是一个接口,常用的使用就是NioEventLoopGroup
// 它可以处理io事件,普通任务,定时任务
// 还有另一个实现 DefaultEventLoop 它可以处理普通任务,定时任务。适用于一些没有io事件发生的场景
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
// 获取下一个EventLoop对象
eventLoopGroup.next();
// 执行普通任务,因为它还有线程池的方法,就可以使用submit(Runnable run)方法来执行一个普通任务
// 执行普通任务的好处是可以执行一个异步处理
eventLoopGroup.next().execute(() -> {
log.debug("执行普通任务");
});
// 执行定时任务 scheduleAtFixedRate(任务对象,初始延迟时间 , 间隔时间 , 时间单位)
// 下面的含义是,刚开始等待1秒执行,然后接下来每隔两秒执行一次
eventLoopGroup.next().scheduleAtFixedRate(() -> {
log.debug("执行定时任务");
} , 1 , 2 , TimeUnit.SECONDS);
}
}
处理io事件
服务器的代码
@Slf4j
public class EventLoopGroupServerTest2 {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
// 这里就没有定义处理ByteBuf和字符串转换的Handler,仅仅定义一个,现在自己进行转换
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
log.debug("接收客户端的消息为:" + byteBuf.toString(Charsets.UTF_8));
}
});
}
})
.bind(8080);
}
}
客户端的代码基本上没什么变化
但是服务器启动后,客户端也启动后,使用debug发送的数据,服务器竟然接收不到,这是因为netty采用的是多线程,而这里的断点主线程和发送数据的线程都停了,这里应该将下面的单选按钮选为Thread,就表示只是停主线程,但是发送数据的线程还是不会停止
EventLoopGroup
我们现在的代码是如下
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(...)
.bind(8080);
这里的group()方法里面只是传了一个参数,就相当于这个组里面有处理accept事件的EventLoop 也有处理read事件的EventLoop。我们可以划分的更细,这个方法里面可以传两个EventLoopGroup对象
public static void main(String[] args) {
new ServerBootstrap()
// 前面负责ServerSocketChannel的accept事件,后面的负责SocketChannel的read事件
.group(new NioEventLoopGroup() , new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(...)
.bind(8080);
}
这里就想,group()方法里面的第一个EventLoopGroup要不要指定数量,其实是不用指定的。因为这里只有一个ServerSocketChannel,只会占用一个EventLoop。
进一步细分,我们往pipeline中添加了多个Handler,但如果其中一个Handler耗时较长,进而影响到了其他的Handler执行,我们可以进一步细分,在创建一个NioEventLoopGroup,让这个新的EventLoopGroup专门去处理耗时较长的操作。
@Slf4j
public class EventLoopGroupServerTest2 {
public static void main(String[] args) {
// 创建一个专门处理耗时较长Handler 的EventLoopGroup
// 因为不是处理io操作,这里可以使用另一个实现 DefaultEventLoopGroup
EventLoopGroup eventLoopGroup = new DefaultEventLoopGroup();
new ServerBootstrap()
// 前面负责ServerSocketChannel的accept事件,后面的负责SocketChannel的read事件
.group(new NioEventLoopGroup() , new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(。。。)
.bind(8080);
}
}
现在的问题就是,EventLoopGroup创建好了后,如何跟这个耗时较长的handler联系起来。其实我们在调用nioSocketChannel.pipeline().addLast()
addlast()方法可以接收多个参数
@Slf4j
public class EventLoopGroupServerTest2 {
public static void main(String