- 连接数高的情况下:阻塞 -> 消耗源、效率低
- AIO在Windows 下比较成熟,但是很少用来做服务器
- Linux 常用来做服务器,但是AIO实现不够成熟
- Linux 的AIO相比NIO的性能没有显著的提升,反而会为开发者带来高额的维护成本
- Netty 暴露了更多的可用参数,如:
- JDK 的 NIO 默认实现是水平触发
- Netty 是边缘触发(默认)和水平触发可以切换
- Netty 实现的垃圾回收更少、性能更好
Netty 三种开发模式版本
BIO 下是 Thread-Per-Connection
Thread-Per-Connection:对应每个连接都有1个线程处理,1个线程同时处理:读取、解码、计算、编码、发送
NIO 下是 Reactor
Reactor 多线程模式,由多个线程负责:读取、发送,由线程池负责处理:解码、计算、编码
Reactor 主从多线程模式,由单独mainReactor 单线程负责接收请求,subReactor和 Reactor 多线程模式一致
AIO 下是 Proactor
Reactor 是一种开发模式,模式的核心流程:
注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理
- Reactor 单线程模式
//线程数1
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootStrap serverBootStrap = new ServerBootStrap();
serverBootStrap.group(eventGroup);
- Reactor 多线程模式
//多线程,不传具体的线程数时,Netty会根据CPU核心数分配
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootStrap serverBootStrap = new ServerBootStrap();
serverBootStrap.group(serverBootStrap);
- Reactor 主从多线程模式
// 主线程负责接收请求 acceptor,是单线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 从线程负责:读取、解码、计算、编码、发送,是多线程
EventLoopGroup workerGroup = new NioEventLoopGroup();
SeverBootStrap serverBootStrap = new ServerBootStrap();
serverBootStrap.group(bossGroup, workerGroup);
1.初始化 Main EventLoopGroup
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
// main Event Loop Group
volatile EventLoopGroup group;
....
// 初始化 mian Event Loop Group 方法
public B group(EventLoopGroup group) {
ObjectUtil.checkNotNull(group, "group");
if (this.group != null) {
throw new IllegalStateException("group set already");
}
this.group = group;
return self();
}
....
}
- 初始化 Worker EventLoopGroup
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
// woker Events Loop Group
private volatile EventLoopGroup childGroup;
.....
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
ObjectUtil.checkNotNull(childGroup, "childGroup");
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
// 初始化 worker Event Loop Group 方法
this.childGroup = childGroup;
return this;
}
....
}
- MainEventLoopGroup 和 WorkerEventLoop 绑定# bind(),并实现新建和初始化 SocketChannel 绑定到 MainEventLoopGroup中
// 绑定 地址:端口
public ChannelFuture bind(SocketAddress localAddress) {
validate();
return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));
}
// 绑定逻辑
private ChannelFuture doBind(final SocketAddress localAddress) {
// 初始化 & 注册 MainEventLoopGroup
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
....
}
// 初始化 & 注册 MainEventLoopGroup
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// 创建新的 ServerSocketChannel
channel = channelFactory.newChannel();
// 初始化 ServerSocketChannel 中的 Handler
init(channel);
} catch (Throwable t) {
if (channel != null) {
// channel can be null if newChannel crashed (eg SocketException("too many open files"))
channel.unsafe().closeForcibly();
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
}
// 将 ServerSocketChannel 注册到 MainEventLoop 中
// 因为端口和地址 只有1个,channel只能被注册一次,所以 MainEventLoopGroup 是单线程的
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
...
}
- WorkerEventLoopGroup 和 SocketChannel 绑定关系
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 每次读取都是一个 SocketChannel
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
try {
// 将 SocketChannel 注册到 workerEventLoopGroup中
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
}
关于半包的主要原因:
- 发送写入数据 > 套接字缓冲区大小
- 发送的数据大于协议 MTU(Maximum Transmission Unit,最大传输单元),必须拆包
关于粘包的主要原因:
- 发送方每次写入数据 > 套接字缓冲区大小
- 接收方读取套接字缓冲区不够及时
换个角度看原因:
- 收发时:一个发送可能多次接收,多个发送可能被一次接收
- 传输时:一个发送可能占用多个传输包,多个发送可能公用一个传输包
导致粘包/半包的根本原因:
TCP 是流式协议,消息无边界
提醒:UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都是 “界限”,一个一个签收,所以无粘包、半包问题
解决粘包和半包的手段: 找出消息的边界
-
方式一:短连接(不推荐)
- 手段:TCP 连接改成短连接,一个请求一个短连接,建立连接到释放连接之间的信息即为传输信息
- 优点:简单
- 缺点:效率低下
-
方式二:固定长度(不推荐)
- 手段:满足固定长度即可
- 优点:简单
- 缺点:浪费空间
-
方式三:分隔符(推荐)
- 手段:用确定分隔符切割
- 优点:空间不浪费,也比较简单
- 缺点:内容本身出现分隔符时需要转义,所以需要扫描内容
-
方式四:固定长度字段存个内容的长度信息(推荐+)
- 手段:先解析固定长度的字段获取长度,然后读取后续的内容
- 优点:精确定位用户数据,内容也不用转义
- 缺点:长度理论上是有限制,需要提前预知可能的最大长度从而定义长度占用字节数
-
方式五:序列化方式(根据场景衡量)
- 手段:每种都不同,例如JSON 可以看{} 是否应己成对
- 优缺点:衡量实际场景,很多是对现有协议的支持
- 固定长度
- 解码:FixedLengthFrameDecoder
- 分隔符
- 解码:DelimiterBasedFrameDecoder
- 固定长度存个内容长度字段
- 解码:LengthFieldBasedFrameDecoder
- 编码:LengthFieldPerpender