前言
前置课程:JAVA OOP编程、多线程编程、IO编程、网络编程、常用设计模式
书籍:
《Netty IN ACTION》
《Netty 权威指南》(基于Netty5)
《Netty 进阶之路》
Netty介绍
- Netty是由JBOSS提供的一个java开源框架,现为GitHub上的独立项目
- Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序
- Netty主要针对在TCP协议下,面向客户端的高并发应用,或P2P场景下的大量数据持续传输的应用
- Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
- 要透彻理解Netty,需要先学习NIO
从上至下:
Netty
NIO
原生JDK io/网络
TCP/IP
Netty应用场景
- 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。
- 典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
- 游戏行业:Netty作为高性能的基础通信组件,提供了TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈,账号登陆服务器;地图服务器之间可以方便的通过Netty进行高性能的通信。
- 大数据领域:经典的Hadoop的高性能通信和序列化组件(AVRO实现数据文件共享)的RPC框架,默认使用Netty进行跨界点通信;它的Netty service基于Netty框架二次封装实现。
IO模型
IO模型基本说明
- 简单理解:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
- Java支持3中网络编程IO模型:BIO NIO AIO
- BIO :同步并阻塞,服务器实现模式位一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销。
- NIO:同步非阻塞,服务器实现模式位一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
- AIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用了proactor模式,简化了程序编写,有效的请求才启动线程;它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
BIO NIO AIO适用场景
- BIO 适合连接数目较小且比较固定的架构,这种方式对服务器资源要求比较高,并发局限在应用中,jdk1.4之前的唯一选择,程序简单易理解
- NIO适合连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务期间通讯。编程比较复杂,jdk1.4开始支持。
- AIO适合连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作。编程比较复杂,jdk1.7开始支持。
BIO基本介绍
- Java BIO就是传统的Java io编程,其相关的类和接口在java.io
- BIO:同步阻塞,服务器实现模式位一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销。
- 可以通过线程池机制改善(实现多个客户连接服务器)。
- BIO方式适合于连接数目较小且比较固定的架构,这种方式对服务器资源要求比较高,并发局限在应用中,jdk1.4之前的唯一选择,程序简单易理解
BIO工作机制
[Socket] —— (read/write) —— [Thread]
流程:
- 服务器端启动一个ServerSocket
- 客户端启动Socket对服务器端进行通信,默认情况下服务器端需要对每个客户建立一个socket与之通讯
- 客户端发出请求后,先咨询服务端是否有线程响应,如果没有则等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,再继续执行(因为阻塞机制)
BIO应用实例(线程池)
用命令行窗口创建客户端
>telnet 127.0.0.1 portId
# 进入127.0.0.1窗口之后 输入 ctrl+]
> send hello #即可发送hello
客户端
- 新建一个线程池
- 新建ServerSocket,监听一个接口
- while(true) ServerSocket.accept(),当有连接时,新建一个线程进行下一步操作。否则,一直等待(阻塞)
- 对与ServerSocket.accept(),得到它的输入流,打印输入内容
- 没有打印内容,但连接未关闭,read()会阻塞,等待输入内容。
问题分析:
- 每个请求都需要创建独立的线程,与对应的客户端进行数据read,业务处理,数据write
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用比较大
- 连接建立后,如果当前线程暂时没有数据可读,则线程阻塞在read操作上,造成资源浪费。
NIO基本介绍
一个server,新建一个Thread,对应一个Selector,每个Selector可以在多个通道之间选择,一个通道对应一个Buffer,双向读写,每个Buffer对应一个连接。
- Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会湖区,而不是保持线程阻塞;所以直至数据变得可以读取之前,该线程可以继续做其它的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:NIO是可以做到用一个线程处理多个操作的。假设有10000个请求过来,根据实际情况,可分配50/100个线程来处理,而不是像BIO一样,一定要分配10000个线程。
- HTTP2.0使用了多路复用技术,左到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
NIO相关的Selector、 SelectionKey、ServerSocketChannel 和 SocketChannel 关系梳理
- 当客户端连接时,会通过ServerSocketChannel 得到对应的SocketChannel
- 将SocketChannel 到注册Selector上;一个Selector上可以注册多个SocketChannel
- 注册后返回一个SelectionKey,会和该Selector关联(Selector有SelectionKey的集合)
- Selector监听select方法,返回有事件发生的通道的个数
- 进一步得到各个(有对应事件发生的)SelectionKey
- 再通过SelectionKey反向获取SocketChannel ,通过channel()方法
- 可以通过得到的channel。完成业务处理
demo
服务端:
public class NIOServer {
public static void main(String[] args) {
final int portId = 6666;
try (
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
) {
System.out.println("服务器(监听端口:"+portId+")启动......");
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(portId));
//设为非阻塞
serverSocketChannel.configureBlocking(false);
// 注册选择器
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
System.out.println("等待客户端连接......");
//监听,等待客户连接
if (selector.select(1000)==0){//没有时间发生
// 或selectNow()
System.out.println("服务器等待了1秒,无连接。");
continue;
}
//如果返回的>0,表示以及获取到关注的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if (key.isAcceptable()){//有新的客户端连接
//给客户端生成一个socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将当前socketChannel也注册到选择器,同时关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接成功");
}else if (key.isReadable()){
//通过 key反向获取channel
SocketChannel channel = (SocketChannel) key.channel();
// 获取 关联buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("来自客户端"+new String(buffer.array()));
}
// 手动移除key,防止重复操作
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
public class NIOClient {
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器,如果连接失败
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
}
}
//如果连接成功
String str = "hello,链接成功";
// 产生一个buffer,里面存储str,buffer的大小等于str字节数组的大小
ByteBuffer wrap = ByteBuffer.wrap(str.getBytes());
//发送数据,buffer数据写入channel
socketChannel.write(wrap);
//会阻塞在此处
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
出现了异常
Exception in thread "main" java.nio.channels.IllegalBlockingModeException
at java.nio.channels.spi.AbstractSelectableChannel.register(AbstractSelectableChannel.java:201)
at com.km.mpdemo001.netty.nio.NIOServer.main(NIOServer.java:59)
原因:未将连接的socketChannel设为非阻塞模式
//给客户端生成一个socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
有个问题:会重复打印接收数据?
解决:把给客户端生成的socketChannel 关掉, channel.close();
NIO群聊系统
- 实现多人群聊
- 服务器端可以检测用户上线,离线,并实现消息准发
- 客户端,无阻塞发送消息给其它用户,也能接收到其它用户的消息
代码有蛮多问题,先不放。
NIO与零拷贝
傻瓜三歪让我教他「零拷贝」——敖丙
零拷贝是网络编程的关键,与性能优化有关。
DMA:direct memory access
0拷贝指的不是不拷贝,而是没有CPU copy
传统IO经过4次拷贝,3次切换
- DMA Copy: Hard drive --> [Kernel space]kernel buffer
- CPU copy: [Kernel space] kernel buffer --> [User space] user buffer
- CPU copy: [User space] user buffer -> [Kernel space]socket buffer
- DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样在网络传输时,就可以减少内核空间到用户空间的拷贝次数。
- DMA Copy: Hard drive --> [Kernel space]kernel buffer
- [Kernel space] kernel buffer --> [User space] user buffer
- CPU copy: [Kernel space]kernel buffer -> [Kernel space]socket buffer
- DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)
sendFile优化
linux2.1版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时由于和用户态完全无关,减少了一次上下文切换。
- DMA Copy: Hard drive --> [Kernel space]kernel buffer
- CPU copy: [Kernel space]kernel buffer -> [Kernel space]socket buffer
- DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)
Linux2.4版本。避免了从内核缓冲区拷贝到Socket buffer的操作,从而再一次减少了数据拷贝
- DMA Copy: Hard drive --> [Kernel space]kernel buffer
- copy desc(拷贝少量描述信息): [Kernel space]kernel buffer -> [Kernel space]socket buffer
- DMA Copy: [Kernel space]socket buffer–>protocol engine(协议栈)
mmap和sendFile的区别
- mmap适合小数据量读写,sendFile适合大文件传输
- mmap经过3次拷贝,4次上下文切换;sendFile经过最少2次数据拷贝,3次上下文切换
- sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能。
NIO的零拷贝方式传递(transferTo)一个大文件
AIO基本介绍
- 在进行IO编程时,常用到两种模式:Reactor和Proactor。Java的NIO就是Reactor。当有事件触发时,服务器端得到通知,进行相应的处理。
- AIO即NIO2.0,叫做异步不阻塞的IO。引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
- 参考《Java新一代网络模型AIO原理及Linux系统AIO介绍》
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
编程难度 | 易 | 难 | 难 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
Netty概述
原生NIO存在的问题
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握selector、socketchannel等。
- 需要具备其它额外技能:多线程和网络编程
- 开发工作量和难度都非常大:比如客户端面临断线重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等。
- NIO的bug:例如臭名昭著的Epoll Bug,会导致selector空轮询,最终导致CPU 100%。
知名的Elasticsearch Dubbo框架内部都采用了Netty
Netty的优点:
- 设计优雅:适用于各种传输类型的统一API 阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰的分离关注点;高度可定制的线程模型——单线程,一个或多个线程池
- 使用方便:详细记录的javadoc;没有其它依赖项。jdk5(Netty3.x)或jdk6(Netty4.x)就够了。
- 高性能、高吞吐量,低延迟,减少资源消耗,最小化不必要的内存复制。
- 安全:完整的SLL/TLS StartTLS支持
- 社区活跃,不断更新
线程模型
- 传统阻塞IO服务模型
- Reactor模式
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
- Netty线程模式(主要基于主从Reactor多线程做了一定改进)
传统阻塞IO服务模型
黄色框代表对象
蓝色框代表线程
白色框表示方法
模型特点:
- 采用阻塞IO模式获取输入的数据
- 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回
问题分析:
- 当并发数很大,就会创建大量线程,占用很大系统资源
- 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费
Reactor模式
Reactor模式:反应器模式/分发者(dispatcher)模式/通知者(notifier)模式
针对传统阻塞IO服务模型的2个缺点,解决方案:
- 基于IO复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个链接有新的数据可以处理时,操作系统通知该应用程序,线程从阻塞状态返回,开始进行业务处理
- 基于线程池复用线程资源
reactor模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此也叫分发者(dispatcher)模式。
reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键。
单reactor单线程
优点:模型简单
缺点:
- 性能问题,只有一个线程,无法完全发挥多核CPU 的性能。handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,容易导致性能瓶颈
- 可靠性问题:若线程意外终止/进入死循环,会导致整个系统通信模块不可用
使用场景:
客户端数量有限,业务处理非常快,如redis在业务处理时间复杂度O(1)的情况
单reactor多线程模式
主从reactor多线程模式
主从模型在许多项目中广泛使用:Nginx、Memcached、Netty
Netty模型
- Netty抽象出两组线程池
- BossGroup专门负责接收客户端的连接
- WorkerGroup专门负责网络的读写
- BossGroup和WorkerGroup的类型都是NioEventLoopGroup。
- NioEventLoop相当于一个事件循环组,是一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯。
- NioEventLoopGroup可以有多个线程,即可以含有多个NioEventLoop
- 每个NioEventLoop的循环执行步骤:
- 轮询 accept事件
- 处理 accept事件,与client建立连接,生成NioSocketChannel,并将其注册到WorkerGroup的某个NioEventLoop上的selector。
- 处理任务队列的任务,即runAllTasks。
- WorkerGroup的每个NioEventLoop循环执行步骤:
- 轮询read,write事件
- 处理I/O事件,在对应NioSocketChannel处理。
- 处理任务队列的任务,即runAllTasks。
- 每个NioEventLoop(Worker)处理业务时。会使用pipeline(管道),pipeline中包含了很多channel,即通过pipeline可以获取到对应通道,通道中维护了很多的处理器。
Netty快速入门实例——TCP服务
实现:
- Netty服务器在6668端口监听,客户端能发送消息给服务器
- 服务器可以回复消息给客户端
引入依赖:
代码实现(启动)
server 的handler:
package com.km.mpdemo001.netty.tcp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
/**
* @description: 自定义Handler
*
* @author: 大颗
* @time: 2020/10/11 19:35
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据
* @param ctx : 上下文对象,含有pipeline,channel,地址
* @param msg : 客户端发送的数据,默认Object类型
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx: "+ctx);
//将msg转成一个 ByteBuf,注意不是java.nio.ByteBuffer;
ByteBuf buf = (ByteBuf)msg;
System.out.println("客户端发送的消息是" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:"+ctx.channel().remoteAddress());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将数据写入到缓存,并刷新 writeAndFlush(Object:msg)
//一般对msg进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
}
/**
* 处理异常,一般需要关闭通道
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
client的handler:
/**
* @description: 自定义Handler
*
* @author: 大颗
* @time: 2020/10/11 19:35
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数
* @param ctx : 上下文对象,含有pipeline,channel,地址
* @param msg : 客户端发送的数据,默认Object类型
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx: "+ctx);
//将msg转成一个 ByteBuf,注意不是java.nio.ByteBuffer;
ByteBuf buf = (ByteBuf)msg;
System.out.println("服务器发送的消息是" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器地址:"+ctx.channel().remoteAddress());
}
// @Override
// public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// //将数据写入到缓存,并刷新 writeAndFlush(Object:msg)
// //一般对msg进行编码
// ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
// }
/**
* 处理异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
/**
* 当通道就绪就会触发该方法
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client: " +ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello ,server",CharsetUtil.UTF_8));
}
}
server:
public class NettyServer {
public final static int PORT_ID = 6668;
public static void main(String[] args) {
/**
* 1. 创建两个线程组 bossGroup和workerGroup,无限循环
* workerGroup 连接请求
* bossGroup 读写请求
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup,workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128)//设置线程队列等待连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true) // 设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化测试(匿名)对象
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
}); //给workerGroup的EventLoop对应的管道设置处理器
System.out.println("服务器 is ready .....");
//绑定一个端口并且同步,生成了一个 ChannelFuture 对象
//启动服务器
ChannelFuture cf = bootstrap.bind(PORT_ID).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
client:
public class NettyClient {
public final static int PORT_ID = 6668;
public final static String aInetHost = "127.0.0.1";
public static void main(String[] args) {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
// 注意:客户端是io.netty.bootstrap.Bootstrap,服务端是io.netty.bootstrap.ServerBootstrap;
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("客户端启动......");
ChannelFuture channelFuture = bootstrap.connect(aInetHost, PORT_ID).sync();
//对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
参数解析
NioEventLoopGroup
new NioEventLoopGroup()的children大小默认为CPU核心数*2,如果要设置,可以初始化时传入参数。
- NioEventLoopGroup下包含多个NioEventLoop;NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终都由IO线程NioEventLoop负责。
- 每个NioEventLoop包含一个selector,一个taskQueue
- 每个NioEventLoop的selector可以注册监听多个NioChannel
- 每个NioChannel只会绑定在唯一的NioEventLoop上
- 每个NioChannel都绑定有一个自己的ChannelPipeline
ChannelHandlerContext
toString():
ctx: ChannelHandlerContext(NettyServerHandler#0, [id: 0x92760a4d, L:/127.0.0.1:6668 - R:/127.0.0.1:55693])
pipeline和channel
toString():
channel:[id: 0x92760a4d, L:/127.0.0.1:6668 - R:/127.0.0.1:55693]
pipeline: DefaultChannelPipeline{(NettyServerHandler#0 = com.km.mpdemo001.netty.tcp.NettyServerHandler)}
pipeline是一个双向链表
pipeline和channel是一一对应的关系
taskQueue
任务队列中的task有3种典型使用场景
- 用户程序自定义的普通任务
- 用户自定义定时任务
- 非当前reactor线程调用channel的各种方法
例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write 类方法向该用户推送消息,就会进入到这种场景。最终的write方法会提交到任务队列中被异步消费。
1. 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端; tag2\n",CharsetUtil.UTF_8));
}
});
类的关系:
taskQueue中的Runnable依次在一个线程中执行: poll and run
NioEventLoop extends SingleThreadEventLoop
.......
SingleThreadEventLoop extends SingleThreadEventExecutor
.......
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
private final Executor executor;
private final Queue<Runnable> taskQueue;
......//略
@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
}
2. 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端; tag4\n",CharsetUtil.UTF_8));
}
},5, TimeUnit.SECONDS);
SingleThreadEventExecutor extends AbstractScheduledEventExecutor...
public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
ObjectUtil.checkNotNull(command, "command");
ObjectUtil.checkNotNull(unit, "unit");
if (delay < 0) {
delay = 0;
}
return schedule(new ScheduledFutureTask<Void>(
this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
}
}
异步模型
基本概念:
- 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
- Netty中的IO操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture。
- 调用者并不能立刻获得结果,而是通过FutureListener机制,用户可以方便的主动获取或通过通知机制获得IO操作结果。
- netty的异步模型是建立在future和callback之上的。callback就是回调。Future的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,后续可以通过future去监控方法fun的处理过程。
Future
- Future表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等。
- ChannelFuture是一个接口,可以添加监听器,当监听的事件发生时,就会通知到监听器。
Future-Listener机制
当有Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
- isDone()
- isSuccess()
- getCause()失败原因
- isCancelled()
- addListener()注册监听器
/**
* 注册监听器
*/
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()){
System.out.println("监听端口"+PORT_ID+"成功。");
}else {
System.out.println("监听端口"+PORT_ID+"失败。");
}
}
});
入门实例——HTTP服务
- netty服务器在6668端口监听,浏览器发出请求"http://localhost:6668/"(6668有问题,可能被占用,改成了8998)
- 服务器可以发送消息给客户端 ,并对特定请求资源进行过滤
- 目的:Netty可以做http服务开发,并且理解handler实例和客户端及其请求的关系。
server编写与上面基本一样,只是把ServerInitializer 写一个自定义的。
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty提供的 处理Http的编解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
pipeline.addLast("MyHttpServerHandler",new HttpServerHandler());
}
}
同时,handler的代码:
/**
* @description:
* SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类
* HttpObject 客户端和服务器端相互通信的数据被封装成这个类型
* @author: 大颗
* @time: 2020/10/11 22:31
*/
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
/**
* 读取客户端数据
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判断msg是不是一个HttpRequest请求
if(msg instanceof HttpRequest){
System.out.println("msg Class type = "+msg.getClass());
System.out.println("客户端地址:"+ctx.channel().remoteAddress());
//回复信息给浏览器
ByteBuf content = Unpooled.copiedBuffer("hello, i'm a server", CharsetUtil.UTF_8);
//构建一个 http response
DefaultHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
//发送response
ctx.writeAndFlush(response);
}
}
}
有一个问题,浏览器每次发出请求,上面handler的channelRead0方法都会执行两次。
原因:浏览器点击一次,其实发出了两个请求,一个是网内页面的请求,一个是对网页图标的请求。那么可以尝试过滤掉第二个请求。
HTTP协议是无状态的协议,因此不同的请求对应的handler不是同一个。
Netty核心模块
Bootstrap、ServerBootstrap
一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务程序的启动引导类,均继承自AbstractBootstrap。
常用的方法有:
- group:设置两个EventLoopGroup
- group:该方法用于客户端,用来设置一个EventLoopGroup
- channel:设置一个服务器端的通道实现
- childHandler :创建一个channelInitializer的方式;对应的是workerGroup
- handler:对应的是bossGroup
AbstractBootstrap:
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable{
volatile EventLoopGroup group;
public B group(EventLoopGroup group)
public B channel(Class<? extends C> channelClass)
public <T> B option(ChannelOption<T> option, T value) {......}
public ChannelFuture bind() {
}
ServerBootstrap:
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup){......}
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {.......}
public ServerBootstrap childHandler(ChannelHandler childHandler) {......}
}
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
public ChannelFuture connect(String inetHost, int inetPort) {......}
}
Future、ChannelFuture
Channel
- Netty网络通信的组件,能够用于执行网络IO操作
- 通过 Channel可获得当前网络连接的通道的状态
- 通过Channel可获得网络连接的配置参数
- Channel提供异步的IO操作(如建立连接,读写,绑定端口),异步调用意味着任何IO调用都将立即返回。并且不保证在调用结束时所请求的IO操作已完成。
- 调用立即返回一个ChannelFutrue实例,通过注册监听器到 ChannelFutrue上,可以IO操作成功、失败或取消时回调通知调用方。
- 支持关联IO操作和对应的处理程序
- 不同协议、不同的阻塞类型的连接有不同的Channel类型与之对应,常用的Channel类型:
- NioSocketChannel:异步客户端TCP Socket连接
- NioServerSocketChannel:异步服务端TCP Socket连接
- NioDatagramChannel:异步UDP连接
- NioSctpChannel:异步客户端Sctp连接
- NioSctpServerChannel:异步服务端Sctp连接
Selector
ChannelHandler
- ChannelHandler 是一个接口,处理IO事件或拦截IO操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
- ChannelHandler本身并没有提供很多方法,因为这个接口有很多的方法需要实现,方便使用期间,可以继承它的子类
入站:数据流入channel
子类
- ChannelInboundHandler 用于处理入站IO事件(接口)
- ChannelInboundHandlerAdapter:用于处理入站IO事件(类)
- ChannelOutboundHandler 用于处理出站IO事件
- ChannelOutboundHandlerAdapter:用于处理出站IO事件(类)
- ChannelDuplexHandler:用于处理入站和出站事件
Pipeline和(*)ChannelPipeline
ChannelPipeline:
- ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链。(也可以这样理解:ChannelPipeline是保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作)
- ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完成控制事件的处理方式,以及Channel中各个的ChannelHandler如何互相交互。
Pipeline和ChannelPipeline:
- 一个Channel包含了一个ChannelPipeline,而ChannelPipeline中又维护了由ChannelHandlerContext(接口)组成的双向链表,并且每个 中又包含着一个ChannelHandler
- 入站时间和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler户不干扰。