文章目录
参考黑马程序员
1.什么是Netty?
Netty是一个异步的,基于事件驱动的网络应用框架,用于快速开发可维护,高性能的网络服务器和客户端,这里说的异步是Netty采用了多线程的方式,调用方法的线程和处理结果的线程是两个,如果是1个的话就可能发生阻塞,解放了调用方法的线程,让他能腾出手来干其他的。所以这里说的异步不是异步IO模型,Netty的IO模型是IO多路复用。
2.一个简单地Netty程序
就是通过Netty搭建一个客户端和一个服务端,客户端向服务器端发送一条消息,服务器仅接收不返回。
服务端
public class HelloServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(
// 5. channel 代表和客户端进行数据读写的通道 Initializer 初始化,负责添加别的 handler
new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 添加具体 handler
ch.pipeline().addLast(new StringDecoder()); // 将 ByteBuf 转换为字符串
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 自定义 handler
@Override // 读事件
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception {
System.out.println(msg); // 打印上一步转换好的字符串
}
});
}
})
// 7. 绑定监听端口
.bind(8080);
}
}
- ServerBootstrap():启动器,将下面提供的Netty组件组装在一起,启动服务器。
- NioEventLoopGroup():BossEventLoop负责处理可连接事件, WorkerEventLoop(selector,thread), 负责处理可读事件,这个EventLoop,就是循环处理事件,其实就包含了线程和选择器。
- channel:选择ServerChannel的实现,Netty支持很多种实现由Nio还有OIO(BIO),epoll等等。
- childHandler:Boss负责处理连接,Worker(child)负责处理读写,childHandler决定了worker(child)能干哪些事。
- channel代表和客户端进行数据读写的通道。Initializer,对channel里的Handler进行初始化负责添加别的handler,在initChannel当中规定具体的handler。这里我们添加了StringDecoder()做一个解码操作,因为字节传过来需要进行解码,转成String类型。ChannelInboundHandlerAdapter就是我们自定义的handler。
- channelRead:在读事件发生之后我处理这个时间,这里是将上一步转换好的字符串。
- bind:绑定监听端口。
客户端
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, wjz");
}
}
- Bootstrap():客户端启动类
- EventLoop():添加EventLoop
- channel:选择客户端channel实现,这里使用了NIOsocket。
- handler:连接一旦建立就会触发,这里我们还是使用InitChannel初始化对发送数据进行编码。即添加StringEncoder()。
- connect:连接服务器
- .sync():
- .channel():
- .writeAndFlush():向服务器发送数据。
然后我们运行这段程序可以看到结果是服务端接收到了来自客户端发送的数据。
3.Netty执行流程简析
我们来简单的分析一下上面程序执行的流程,如下图所示。
3.1服务端准备工作
首先服务端这块需要建立一个ServerBootStrap来组装组件启动服务器,然后添加group组件他的作用是不断轮询看看是否有连接过来,还有建立连接之后通知Handler去处理。添加channel组件一会建立连接之后,通道的实现类。然后就是添加childHandler实现类,这个是处理请求的主力。最后绑定监听窗口。
3.2客户端准备工作
客户端建立一个Bootstrap组装客户端的一些组件,添加group,channel,childerHandler等组件。这些组件添加完之后,寻求与服务器建立连接。然后会调用.sycn方法,如果没建立连接就阻塞,建立连接了才能往下走。之后.channel就是实例化channel对象。之后就可以向服务器端发送消息了。
3.3交互过程
建立连接之后,客户端会触发handler,会对我要发送的信息进行encode成bytebuf发出去,这个时候服务器的eventLoop也被触发了,那他就会通知handler对拿到的消息进行decode,然后执行channelRead方法,处理拿到的字符串。
4.一些概念的再明确
- channel:数据的一个通道
- handler:就是处理数据的多道工序,一大堆工序合在一起就是pipeline,pipline相当于流水线,pipeline负责读取并传播给handler,handler再对pipeline加工的数据进行处理(即重写了一些read方法)。
- eventLoop就是处理数据的工人,工人通过IO多路复用来管理多个channel的io操作,一旦工人绑定了某个channel,就要负责到底(绑定),就相当于你到一个理发店剪头,你就觉着这个师傅剪得好,然后你就一直找他剪。这个目的是为了保护线程安全,不能多个线程同时对一个channel进行读写,这样可能导致数据的混乱。
- eventLoop不仅可以处理IO操作,也可以进行任务处理,每位工人有任务队列,队列里可以堆放channel待处理的任务。
- 工人按照pipline的顺序,依次按照handler的规划处理数据,可以为每道工序指定特定的工人。
5. EventLoop组件
- 他是一个事件循环对象,主要是为了处理channel上源源不断的io事件,他还是一个单线程执行器,他内部有一个selector从而维护多个channel连接。他继承了scheduledExecutorService因此它包含了线程池中所有的方法。它还继承了netty下的OrderedEvenetExecutor。
- 事件循环组:EventLoopGroup是一组EventLoop,channel一般会调用EventLoopGroup中的register方法来绑定其中一个EventLoop,后续这个Channel上的io事件都由此EventLoop来处理(保证了io事件处理时的线程安全。
5.1 EventLoop用法
5.1.1执行普通任务
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup(2); // io 事件,普通任务,定时任务
System.out.println(group.next());
group.next().execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("ok");
});
log.debug("main");
}
我们来看一下这段代码首先我创建了一个eventLoop组,用Nio去实现,这样的话我这个可以执行io事件,普通任务,定时任务,里面的参数是创建多少个线程。group.next()是拿到下一个事件循环对象。我们可以看一下他的构造函数。
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
然后我们追一下底层,最后追到的是这个函数
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
我们可以看到他首先会判断这个nThreads是不是为0,如果是0的话,线程数量为默认线程数,如果不是0那线程数量就按照你指定的来。这个默认线程数。我们看看它是如何定义的。
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
这个默认线程数,首先我会读取netty默认的系统参数看能不能读到如果读不到就去计算你当前CPU核心数*2,为保险起见,万一SystemPropertyUtil.getInt还是要返回0,我就让他是1,就是在这两者当中取最大值最次也得为1。
我们运行一下
5.1.2 执行定时任务
public static void main(String[] args) {
// 1. 创建事件循环组
EventLoopGroup group = new NioEventLoopGroup(2); // io 事件,普通任务,定时任务
// 2. 获取下一个事件循环对象
System.out.println(group.next());
// 3. 执行定时任务
group.next().scheduleAtFixedRate(() -> {
log.debug("ok");
}, 0, 1, TimeUnit.SECONDS);
log.debug("main");
}
5.2 eventLoop分工细化
我们最好对eventLoop进行分工细化,不然的话如果只有一个eventLoop那这个eventLoop不仅得处理accept还得处理read事件。.group()是可以有两个参数的,前一参数为boss他只会处理accept事件,后一个参数为worker参数他只会处理read事件这样就达到了分工的目的。
服务端
public static void main(String[] args) {
new ServerBootstrap()
// boss 和 worker
// 细分1:boss 只负责 ServerSocketChannel 上 accept 事件 worker 只负责 socketChannel 上的读写
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override // ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(Charset.defaultCharset()));
}
});
}
})
.bind(8080);
}
客户端
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override // 在连接建立后被调用
protected void initChannel(NioSocketChannel ch) throws Exception {
ch