Netty 一
概述
- Netty是一个异步事件驱动的网络应用框架, 可以快速开发高性能服务器和客户端
- Netty对 JDK的 NIO API进行了封装, 解决了 Epoll Bug, 导致 Selector空轮询的问题& 改善了 NIO的使用繁琐& 难的等问题
- 官网: https://netty.io
版本
- 版本分为 netty3.x, netty4.x和 netty5.x. 其中5.x, 出现重大 bug, 已被官网废弃, 目前推荐使用4.x的稳定版
架构设计
- 线程模型: 目前有两种线程模型 1. 阻塞 I/O模型 2.
Reactor事件驱动模型
- 阻塞 I/O模型:
(-) 每个连接都需要通过独立的线程来处理数据
(-) 当并发数多时, 由于每个连接都需要开启线程, 导致严重影响性能
(-) 连接建立后, 如果当前线程, 暂时, 没有可读数据, 此时该线程会阻塞 read操作, 造成线程资源浪费- Reactor事件驱动模型:
(-) 基于 I/O复用模型: 多个连接共用一个阻塞对象, 程序只需在一个阻塞对象上等待, 无需阻塞等待所有连接. 当某个连接有新的数据可以处理时, 操作系统通知程序, 线程从阻塞状态返回, 开始处理业务
(-) 不必再为每个连接创建线程, 一个线程可以处理多个连接的业务
(-) Reactor又称: 反应器模式, 分发者模式(Dispatcher)或通知者模式(Notifier)
- Reactor模式基本设计图:
Reactor模式又分3种实现
- 单 Reactor单线程:
- Select是 I/O复用模型的标准网络编程 API, 可以实现程序通过一个阻塞对象监听多路连接请求
- Reactor对象通过 Select监控客户端请求事件, 收到事件后通过 Dispatch进行分发
- 如果客户端建立连接, 则通过 Accept获得通道, 再创建一个 Handler对象待后续业务处理
- 如果不是建立连接事件, 则 Reactor会分发调用连接对应的 Handler来处理
- Handler会完成 Read -> 业务处理 -> Send
- 缺点:
- 只有一个线程, 未充分利用多核 CPU的性能. 并发多时, 容易导致性能瓶颈
- 发生线程意外终止等意外情况, 会导致整个系统的不可用, 也就是可靠性上的问题
- 单 Reactor多线程:
- Reactor对象通过 select监控客户端请求, 收到事件后,通过 dispatch进行分发
- 如果客户端建立连接, 则通过 accept获得通道, 再创建一个 handler对象, 处理完成连接的各种事件
- 如果不是建立连接事件, 则 reactor会分发调用连接对应的 handler来响应
- Handler只负责响应事件, 不做具体的业务处理, 通过 read读取数据后, 会分发给后面的 worker线程池的某个线程处理业务
- Worker线程池会分配独立线程完成实际业务, 并将结果返回给 handler
- Handler收到响应后, 通过 send, 将结果返回给 client
- 缺点: reactor处理所有事件的监听和响应, 可是在单线程上运行, 这会导致高并发场景上发生性能瓶颈
主从 Reactor(Netty线程模式是基于主从 Reactor)
:
- Reactor主线程 Main reactor对象通过 select监听连接事件, 收到连接事件后建立连接
- 处理连接事件后, 将连接分配给 subReactor
- Subreactor将连接加入到连接队列进行监听, 并创建 handler进行各种事件处理
- 当有新事件发生时, subreactor就会调用对应的 handler来处理
- Handler通过 read读取数据, 分发给后面的 worker线程处理
- Worker线程池分配独立的 worker线程进行业务处理, 并返回结果
- Handler收到结果后, 再通过 send将结果返回给 client
- Reactor主线程可以对应多个 reactor子线程, 即 Main recator可以关联多个 subReactor
- 优点: 父线程与子线程的数据交互简单职责明确, 父线程只需要接收新连接, 子线程完成后续的业务处理
- 缺点: 编程复杂度较高
- 各种模式的生活案例:
- 单 Reactor单线程, 前台接待员和服务员是同一个人
- 单 Reactor多线程, 1个前台接待员, 多个服务员, 接待员只负责接待
- 主从 Reactor多线程, 多个前台接待员, 多个服务员
- Reactor模式具有如下的优点:
- 响应快, 不必为单个同步时间所阻塞, 虽然 reactor本身依然是同步的
- 可以最大程度的避免复杂的多线程及同步问题, 且避免了多线程/进程的切换开销
- 扩展性好, 可以方便的通过增加 reactor实例个数来充分利用 CPU资源
- 复用性好, reactor模型本身与具体事件处理逻辑无关, 具有很高的复用性
- Netty模型:
- BossGroup线程维护的 selector, 当接收到 accept事件(连接事件)后, 首先获取到对应的 SocketChannel, 封装成 NIOScoketChannel, 并注册到 worker线程中(进行事件循环)
- 当 Worker线程的 selector监听到自己所关注的事件后, 就由 handler进行处理(读写业务)
- Netty抽象出两组线程池 BossGroup只负责接收客户端的连接(accept事件), WorkerGroup负责实际业务
- BossGroup和 WorkerGroup类型都是 NioEventLoopGroup
- NioEventLoopGroup是事件循环组, 组内含有多个事件循环 NioEventLoop
- NioEventLoop表示一个任务的线程, 每个 NioEventLoop都有一个 selector, 用于监听绑定在其上的 socket
- 每个 Boss NioEventLoop循环执行的步骤有3步
5-1) 轮询 accept事件
5-2) 处理 accept事件, 与 client建立连接, 生成 NioScocketChannel, 并将其注册到某个 worker NIOEventLoop上的 selector
5-3) 处理任务队列的任务, 即 runAllTasks- 每个 Worker NIOEventLoop循环执行的步骤
6-1) 轮询 read& write事件
6-2) 处理 read, write事件
6-3) 处理任务队列的任务, 即 runAllTasks- 每个 Worker NIOEventLoop处理业务是在 pipeline(管道), 即通过pipeline获取到对应通道, 并处理业务
- Netty实例(TCP服务)
NioEventLoopGroup下包含多个 NioEventLoop
(-) 每个 NioEventLoop中包含有一个 Selector和一个 taskQueue
(-) 每个 NioEventLoop的 Selector上可以注册监听多个 NioChannel
(-) 每个 NioChannel只会绑定在唯一的 NioEventLoop上
(-) 每个 NioChannel都绑定有一个自己的 ChannelPipeline异步模型
(-) Netty中的 I/O操作是异步的 如 Bind, Write, Connect等操作会返回一个 ChannelFuture
(-) 调用者需主动或通过通知机制(Future-Listener)获得 IO操作结果
* Future-Listener机制
:当 Future对象刚刚创建时, 处于非完成状态, 此时可以通过返回的 ChannelFuture来获得操作执行的状态, 注册监听函数, 再通过该函数来完成执行操作
常用操作:
- isDone(): 判断当前操作是否完成
- isSuccess(): 判断已完成的当前操作是否成功
- cause(): 获取已完成的当前操作失败的原因
- isCancelled(): 判断已完成的当前操作是否被取消
- addListener(): 注册监听器, 当操作已完成(isDone方法返回完成), 将会通知指定的监听器
public class NettyServer {
public static void main(String[] args) throws Exception {
// 1. bossGroup处理客户端连接请求, workerGroup处理客户端业务请求
// 2. 两个组都是无限循环
// 3. NioEventLoopGroup参数为启动子线程(NioEventLoop)个数(可选), 默认 CPU核数 x 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
// 创建服务器端的启动对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup) // 设置线程组
.channel(NioServerSocketChannel.class) // 设置服务器的通道实现
// SO_BACKLOG对应 tcp/ip协议的监听函数 listen(int socketfd,int backlog)中的 backlog参数, 用来设置, 等待处理客户端连接服务端的队列大小
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
//.handler(null) // handler()对应 serverBootstrap, childHandler()对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {
// 初始化通道对象
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客户端: SocketChannel.hashcode=" + ch.hashCode());
// 给 workerGroup(NioEventLoopGroup)中, 各 NioEventLoop, 对应 pipeline设置处理器
ch.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("Netty Server is ready...");
// 服务器绑定端口
ChannelFuture cf = serverBootstrap.bind(6666).sync();
// 给 cf注册监听器
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) {
System.out.println("监听端口6666成功!");
} else {
System.out.println("监听端口6666失败!");
}
}
});
// 对关闭通道进行监听
cf.channel().closeFuture().sync();
} finally {
// 优雅关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
// 读取客户端发送的数据
// ChannelHandlerContext ctx:上下文对象, 含管道 pipeline, 通道channel等
// Object msg: 客户端发送的数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("读事件: 服务器线程=" + Thread.currentThread().getName() + " channle=" + ctx.channel());
System.out.println("客户端发送的消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8) + " 客户端地址=" + ctx.channel().remoteAddress());
// TODO: 当有比较耗时的业务, 可以异步处理;
// 解决方案 1: 添加普通任务(任务会加到 NioEventLoop的 taskQueue中
ctx.channel().eventLoop().execute(() -> {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!!", CharsetUtil.UTF_8));
System.out.println("服务器任务线程1=" + Thread.currentThread().getName() + " channel code=" + ctx.channel().hashCode());
} catch (Exception e) {
System.out.println(e.getMessage());
}
});
// 在多任务, 在同一个 NioEventLoop时, 由于是在同一个线程内, 所以异步按顺序执行的
ctx.channel().eventLoop().execute(() -> {
try {
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!!!", CharsetUtil.UTF_8));
System.out.println("服务器任务线程2=" + Thread.currentThread().getName() + " channel code=" + ctx.channel().hashCode());
} catch (Exception e) {
System.out.println(e.getMessage());
}
});
//解决方案 2: 定时任务 -> 该任务是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!!!!", CharsetUtil.UTF_8));
System.out.println("服务器任务线程3=" + Thread.currentThread().getName() + " channel code=" + ctx.channel().hashCode());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}, 10, TimeUnit.SECONDS);
System.out.println("go on...");
}
// 数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 将对数据进行编码后写入到缓存, 并刷新
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!", CharsetUtil.UTF_8));
}
// 处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
public class NettyClient {
public static void main(String[] args) throws Exception {
// 创建客户端的事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group) // 设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 加入自己的处理器
// 给 group(NioEventLoopGroup)中, 各 NioEventLoop, 对应 pipeline设置处理器
ch.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("NettyClient is ok...");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture cf = bootstrap.connect("127.0.0.1", 6666).sync();
// 给 cf注册监听器
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) {
System.out.println("监听服务器成功!");
} else {
System.out.println("监听服务器失败!");
}
}
});
// 给关闭通道进行监听
cf.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
// 当通道就绪时触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client.ChannelHandlerContext=" + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server!", CharsetUtil.UTF_8));
}
// 当通道有读取事件时触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器的地址=" + ctx.channel().remoteAddress() + ", 消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8));
}
// 处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
===> 客户端连接时, 服务端打印:
Netty Server is ready...
监听端口6666成功!
客户端: SocketChannel.hashcode=106062425
读事件: 服务器线程=nioEventLoopGroup-3-1 channle=[id: 0x176ae39e, L:/127.0.0.1:6666 - R:/127.0.0.1:60577]
客户端发送的消息=hello, server! 客户端地址=/127.0.0.1:60577
go on...
服务器任务线程1=nioEventLoopGroup-3-1 channel code=106062425
服务器任务线程2=nioEventLoopGroup-3-1 channel code=106062425
服务器任务线程3=nioEventLoopGroup-3-1 channel code=106062425
*可以看到 Handler处理器内普通异步任务的线程和 channelRead方法的线程实际上是同一个, 所以该方式处理耗时工作(如 数据库, 网络请求等操作)依然会严重影响 Netty对 Socket的处理速度
- 处理器加异步线程池两种方式:
- Handler中加线程池: 使用灵活, 比如有耗时操作时异步, 没有就不使用
- Context中加线程池: 不管耗不耗时都会放到异步队列里处理, 不够灵活
方式 1: Handler中加线程池
public class NettyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
});
ChannelFuture cf = serverBootstrap.bind(6666).sync();
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
// 通过 DefaultEventExecutorGroup额外开辟业务线程池, 用于处理耗时任务
static final EventExecutorGroup executorGroup = new DefaultEventExecutorGroup(16);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("读事件: 服务器线程=" + Thread.currentThread().getName() + " channle=" + ctx.channel());
System.out.println("客户端发送的消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8) + " 客户端地址=" + ctx.channel().remoteAddress());
// 使用异步线程处理业务
executorGroup.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
Thread.sleep(5 * 1000); // 休眠5秒
System.out.println("异步线程 1=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("哈 1", CharsetUtil.UTF_8));
return null;
}
});
executorGroup.submit(() -> {
Thread.sleep(10 * 1000); // 休眠10秒
System.out.println("异步线程 2=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("哈 2", CharsetUtil.UTF_8));
return null;
});
executorGroup.submit(() -> {
Thread.sleep(15 * 1000); // 休眠15秒
System.out.println("异步线程 3=" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("哈 3", CharsetUtil.UTF_8));
return null;
});
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
===> 客户端连接时, 服务端打印:
读事件: 服务器线程=nioEventLoopGroup-3-1 channle=[id: 0xea4d1524, L:/127.0.0.1:6666 - R:/127.0.0.1:65202]
客户端发送的消息=hello, server! 客户端地址=/127.0.0.1:65202
异步线程 1=defaultEventExecutorGroup-4-1
异步线程 2=defaultEventExecutorGroup-4-2
异步线程 3=defaultEventExecutorGroup-4-3
方式 2: Context中加线程池
public class NettyServer {
// 通过 DefaultEventExecutorGroup额外开辟业务线程池, 用于处理耗时任务
static final EventExecutorGroup executorGroup = new DefaultEventExecutorGroup(2);
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(executorGroup, new NettyServerHandler());
}
});
ChannelFuture cf = serverBootstrap.bind(6666).sync();
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("读事件: 服务器线程=" + Thread.currentThread().getName() + " channle=" + ctx.channel());
System.out.println("客户端发送的消息=" + ((ByteBuf) msg).toString(CharsetUtil.UTF_8) + " 客户端地址=" + ctx.channel().remoteAddress());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client!", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
===> 客户端连接时, 服务端打印:
读事件: 服务器线程=defaultEventExecutorGroup-2-1 channle=[id: 0x76b09cdb, L:/127.0.0.1:6666 - R:/127.0.0.1:49628]
客户端发送的消息=hello, server! 客户端地址=/127.0.0.1:49628
- Netty实例(HTTP服务)
浏览器打开 http://localhost
// Netty Http Server
public class TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
// 服务器绑定端口
ChannelFuture cf = serverBootstrap.bind(80).sync();
System.out.println("Netty Http Server is ready...");
// 给 cf注册监听器
cf.addListener((ChannelFuture future) -> {
if (future.isSuccess()) {
System.out.println("监听端口80成功!");
} else {
System.out.println("监听端口80失败!");
}
});
// 对关闭通道进行监听
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
// init Handler
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 取得管道为了加入处理器(Worker Group -> NioEventLoopGroup -> NioEventLoop -> pipeline)
ChannelPipeline pipeline = ch.pipeline();
// 配置 netty的 http的编/解码器
pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
//2. 加一个自定义的 handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
}
}
// SimpleChannelInboundHandler继承了 ChannelInboundHandlerAdapter
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
// channelRead0读取客户端数据
// HttpObject 客户端与服务器端相互通讯的数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
System.out.println("current channel=" + ctx.channel() + " pipeline=" + ctx.pipeline() + " > channel" + ctx.pipeline().channel());
//判断 msg 是不是 httprequest请求
if (msg instanceof HttpRequest) {
System.out.println("客户端地址=" + ctx.channel().remoteAddress() + " ctx类型=" + ctx.getClass());
System.out.println("pipeline hashcode=" + ctx.pipeline().hashCode() + " TestHttpServerHandler hashcode=" + this.hashCode());
// 获取客户端数据
HttpRequest httpRequest = (HttpRequest) msg;
// 获取uri
URI uri = new URI(httpRequest.uri());
// 可以过滤指定的资源
if ("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico, 不做响应");
return;
}
// 回复信息给浏览器 [http协议]
ByteBuf content = Unpooled.copiedBuffer("hello, this is http server!", CharsetUtil.UTF_8);
// 构造一个 http相应
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
// 配置 http header
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
// 返回 response
ctx.writeAndFlush(response);
}
}
}
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!