优化
连接假死
1、原因
(1)网络设备出现故障,例如:网卡,机房等,导致底层 TCP 连接已经断开,但应用程序没有感知,仍然占用资源
(2)公网网络不稳定,出现丢包,如果连续出现丢包,客户端无法发送数据,服务端无法接收数据,消耗资源
(3)应用程序线程阻塞,无法进行数据读写
2、问题
(1)假死连接占用的资源不能自动释放
(3)向假死连接发送数据,反映发送超时
IdleStateHandler
1、当一个通道在一段时间内没有进行读、写或两者的操作时,会触发一个 IdleStateEvent
2、构造器:创建一个新的实例,来触发一个或多个 IdleStateEvent
public IdleStateHandler(boolean observeOutput,
long readerIdleTime,
long writerIdleTime,
long allIdleTime,
TimeUnit unit)
(1)observeOutput:在评估写入空闲状态时,是否要考虑字节的消耗,默认为 false
(2)readerIdleTime:一个 IdleStateEvent,其状态为 IdleState.READER_IDLE,当在指定的时间内,没有进行读取时,将被触发,指定 0 来禁用
(3)writerIdleTime:一个 IdleStateEvent,其状态为 IdleState.WRITER_IDLE,当在指定时间内,没有进行写操作时,将被触发,指定 0 来禁用
(4)allIdleTime:一个 IdleStateEvent,其状态为 IdleState.ALL_IDLE,当在指定的时间段内,没有进行读或写时,将被触发,指定 0 来禁用
(5)unit:readerIdleTime、writerIdleTime、allIdleTime 的时间单位
2、IdleStateEvent
(1)当一个通道处于空闲状态时,由 IdleStateHandler 触发的用户事件
(2)返回空闲状态
public IdleState state()
3、IdleState
(1)枚举类
(2)代表 Channel 的空闲状态
4、ChannelDuplexHandler
(1)双向处理器
(2)处理入站、出站事件
(3)ChannelHandler 实现代表 ChannelInboundHandler 和 ChannelOutboundHandler 的组合,如果 ChannelHandler 实现需要拦截操作和状态更新,它是一个很好的起点
(4)IdleStateHandler 中的事件为特殊事件,需要实现 ChannelInboundHandler 的 userEventTriggered 方法,判断事件类型,并自定义处理事件的方式
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
服务端:空闲检测
1、添加 ChannelDuplexHandler
2、重写 userEventTriggered
(1)参数 Object evt,向下转型为 IdleStateEvent
(2)state() 获取其 IdleState
(3)判断其类型,进行对应处理,如:断开连接
客户端:发送心跳
1、避免因非网络等原因引发 IdleStateEvent
2、添加 ChannelDuplexHandler
(1)客户端向服务器发送心跳包,发送频率要小于服务器设置的 IdleTime,一般设置为其值的一半
3、重写 userEventTriggered
(1)参数 Object evt,向下转型为 IdleStateEvent
(2)state() 获取其 IdleState
(3)判断其类型,发送心跳包
ctx.writeAndFlush(new PingMessage());
AbstractBootstrap
1、子类
(1)Bootstrap
(2)ServerBootstrap
2、允许指定一个 ChannelOption,一旦 Channel 实例被创建,它就被用于 Channel 实例;使用 null 值删除之前设置的 ChannelOption
(1)AbstractBootstrap 所定义的方法
public <T> B option(ChannelOption<T> option, T value)
(2)ServerBootstrap 所定义的方法
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)
3、配置参数
(1)客户端通过 Bootstrap.option 来配置参数,配置参数作用于 SocketChannel
(2)服务器通过 ServerBootstrap 来配置参数,对于不同 Channel 需要选择不同方法
(3)通过 option 来配置 ServerSocketChannel 上的参数
(4)通过 childOption 来配置 SocketChannel 上的参数
ChannelOption
1、ChannelOption 允许以一种类型安全的方式配置 ChannelConfig
2、支持哪种 ChannelOption 取决于 ChannelConfig 的实现,也可能取决于它所属的传输的性质
CONNECT_TIMEOUT_MILLIS
1、属于 SocketChannal 的参数
2、客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常
3、区分
(1)SO_TIMEOUT:用于阻塞 IO 的传统 API,即 ServerSocket、Socket
(2)CONNECT_TIMEOUT_MILLIS:用于非阻塞 IO,如:Netty
4、默认值
private volatile int connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT;
private static final int DEFAULT_CONNECT_TIMEOUT = 30000;
超时判断、线程通信源码
1、客户端中 NIO 线程连接服务器,主线程抛出异常
2、io.netty.channel.nio.AbstractNioChannel.AbstractNioUnsafe
public final void connect(final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
……
// Schedule connect timeout.
// 设置超时时间,通过option方法传入的CONNECT_TIMEOUT_MILLIS参数进行设置
// 读取CONNECT_TIMEOUT_MILLIS
int connectTimeoutMillis = config().getConnectTimeoutMillis();
// 超时的判断主要通过 Eventloop 的 schedule 方法和 Promise 共同实现
// 如果超时时间大于0
if (connectTimeoutMillis > 0) {
// 创建一个定时任务,延时connectTimeoutMillis(设置的超时时间时间)后执行
// schedule(Runnable command, long delay, TimeUnit unit)
connectTimeoutFuture = eventLoop().schedule(new Runnable() {
@Override
public void run() {
//创建ConnectTimeoutException
ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
ConnectTimeoutException cause = new ConnectTimeoutException("connection timed out: " + remoteAddress);
// 判断是否建立连接,Promise进行NIO线程与主线程之间的通信
// 如果超时,则通过tryFailure方法将cause放入Promise中
// 在主线程中,Futture获取异常,抛出ConnectTimeoutException
if (connectPromise != null && connectPromise.tryFailure(cause)) {
close(voidPromise());
}
}
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}
……
}
三次握手
1、第一次握手
(1)client 发送 SYN 到 server,状态修改为 SYN_SEND
(2)server 收到 SYN,状态改变为 SYN_REVD,并将该请求放入 sync queue 队列
(3)因为客户端与服务器之间的连接还未完全建立,连接会被放入半连接队列中
2、第二次握手
(1)server 回复 SYN + ACK 到 client
(2)client 收到,状态改变为 ESTABLISHED,并发送 ACK 给 server
3、第三次握手
(1)server 收到 ACK,状态改变为 ESTABLISHED,将该请求从 sync queue 放入 accept queue
(2)当完成三次握手以后,连接会被放入全连接队列中
(3)在 TCP 三次握手,即建立连接之后,服务器处理 Accept 事件,服务器会从全连接队列中,获取连接并进行处理
连接队列
1、在 Linux 2.2 之前,backlog 大小包括两个连接队列的大小
2、在 linux 2.2 之后,分别用下面两个参数来控制两个连接队列
3、sync queue
(1)半连接队列
(2)大小通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在 syncookies 启用的情况下,逻辑上没有最大值限制,这个设置便被忽略
4、accept queue
(1)全连接队列
(2)大小通过 /proc/sys/net/core/somaxconn 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值
(3)如果 accpet queue 队列满,server 将发送一个拒绝连接的错误信息到 client
SO_BACKLOG
1、属于 ServerSocketChannal 参数
2、Netty 的 NioEventLoop,触发 accept 事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
//服务器会从全连接队列中,获取连接并进行处理
unsafe.read();
}
3、默认值
(1)java.nio.channels.ServerSocketChannel
public abstract ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException;
(2)io.netty.channel.socket.nio.ServerSocket.NioServerSocketChannel
@SuppressJava6Requirement(reason = "Usage guarded by java version check")
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
//此处调用java.nio.channels.ServerSocketChannel的bind
//config:ServerSocketChannelConfig,默认实现:DefaultServerSocketChannelConfig
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
(3)io.netty.channel.socket.DefaultServerSocketChannelConfig
@Override
public int getBacklog() {
return backlog;
}
private volatile int backlog = NetUtil.SOMAXCONN;
(4)io.netty.util.NetUtil
/**
* 当前机器的SOMAXCONN值
* 如果无法获得该值,Windows使用200作为默认值,其他则使用128
*/
public static final int SOMAXCONN;
static {
……
SOMAXCONN = AccessController.doPrivileged(new SoMaxConnAction());
}
private static final class SoMaxConnAction implements PrivilegedAction<Integer> {
@Override
public Integer run() {
// Determine the default somaxconn (server socket backlog) value of the platform.
// The known defaults:
// - Windows NT Server 4.0+: 200
// - Linux and Mac OS X: 128
int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
//尝试获取Linux配置文件
File file = new File("/proc/sys/net/core/somaxconn");
BufferedReader in = null;
try {
//若文件存在,则以其值为系统参数
if (file.exists()) {
in = new BufferedReader(new FileReader(file));
somaxconn = Integer.parseInt(in.readLine());
if (logger.isDebugEnabled()) {
logger.debug("{}: {}", file, somaxconn);
}
} else {
// Try to get from sysctl
Integer tmp = null;
if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
tmp = sysctlGetInt("kern.ipc.somaxconn");
if (tmp == null) {
tmp = sysctlGetInt("kern.ipc.soacceptqueue");
if (tmp != null) {
somaxconn = tmp;
}
} else {
somaxconn = tmp;
}
}
if (tmp == null) {
logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
somaxconn);
}
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
file, somaxconn, e);
}
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
// Ignored.
}
}
}
return somaxconn;
}
}
ulimit -n
1、属于操作系统参数
2、限制一个进程同时打开文件描述符的最大数量
TCP_NODELAY
1、属于 SocketChannal 参数
2、因为 Nagle 算法,数据包会堆积到一定的数量后一起发送,可能导致数据的发送存在一定延时
3、该参数默认为 false,如果不希望的发送被延时,则需要将该值设置为 true(建议)
SO_SNDBUF、SO_RCVBUF
1、滑动窗口:发送缓冲区、接收缓冲区
2、SO_SNDBUF 属于 SocketChannal 参数
3、SO_RCVBUF 既可用于 SocketChannal 参数,也可以用于 ServerSocketChannal 参数(一般设置到 ServerSocketChannal 上)
4、不建议设置,系统自动调整
ALLOCATOR
1、属于 SocketChannal 参数
2、分配 ByteBuf,配置 ByteBuf 是池化 / 非池化,是直接内存 / 堆内存
(1)池化直接内存
new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator(true);
(2)池化堆内存
new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator(false);
(3)非池化直接内存
new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(true);
(4)非池化堆内存
new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false);
3、-Dio.netty.allocator.type
(1)池化:=pooled
(2)非池化:=unpooled
4、-Dio.netty.noPreferDirect
(1)直接内存:=false
(2)非内存:=true
RCVBUF_ALLOCATOR
1、属于 SocketChannal 参数
2、控制 Netty 接收缓冲区大小
3、Handler 内部,由 ChannelHandlerContext 的 alloc() 所获取的 ByteBufAllocator,可以决定缓冲区类型
4、负责入站数据的分配,决定入站缓冲区的大小(可动态调整),统一采用直接内存,池化 / 非池化由 ByteBufAllocator 决定
5、使用 I/O 读写数据,直接内存效率比堆内存高,Netty 强制使用直接内存
6、源码
(1)AbstractNioByteChannel 的 read()
//决定池化或非池化
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
ByteBuf byteBuf = null;
//决定byteBuf大小、直接内存或堆内存
byteBuf = allocHandle.allocate(allocator);
(2)DefaultChannelConfig
@Override
public ByteBufAllocator getAllocator() {
return allocator;
}
private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
(3)DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandler
@Override
public ByteBuf allocate(ByteBufAllocator alloc) {
//ioBuffer:分配一个ByteBuf,一个适合I/O的直接缓冲区
//guess():根据实际数据量,决定ByteBuf大小
return alloc.ioBuffer(guess());
}
(4)AbstractChannel.AbstractUnsafe
@Override
public RecvByteBufAllocator.Handle recvBufAllocHandle() {
if (recvHandle == null) {
recvHandle = config().getRecvByteBufAllocator().newHandle();
}
return recvHandle;
}
(5)DefaultChannelConfig
private volatile RecvByteBufAllocator rcvBufAllocator;
public DefaultChannelConfig(Channel channel) {
this(channel, new AdaptiveRecvByteBufAllocator());
}
protected DefaultChannelConfig(Channel channel, RecvByteBufAllocator allocator) {
setRecvByteBufAllocator(allocator, channel.metadata());
this.channel = channel;
}
private void setRecvByteBufAllocator(RecvByteBufAllocator allocator, ChannelMetadata metadata) {
checkNotNull(allocator, "allocator");
checkNotNull(metadata, "metadata");
if (allocator instanceof MaxMessagesRecvByteBufAllocator) {
((MaxMessagesRecvByteBufAllocator) allocator).maxMessagesPerRead(metadata.defaultMaxMessagesPerRead());
}
setRecvByteBufAllocator(allocator);
}
@Override
public ChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) {
rcvBufAllocator = checkNotNull(allocator, "allocator");
return this;
}
@SuppressWarnings("unchecked")
@Override
public <T extends RecvByteBufAllocator> T getRecvByteBufAllocator() {
return (T) rcvBufAllocator;
}
(6)AdaptiveRecvByteBufAllocator:在反馈中自动增加和减少预测的缓冲区大小。如果之前的读取完全填满了分配的缓冲区,它就会逐渐增加预期的可读字节数;如果读操作不能连续两次填满分配的缓冲区的一定数量,它就会逐渐减少预期的可读字节数。否则,它将继续返回相同的预测值
public AdaptiveRecvByteBufAllocator() {
this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
}
static final int DEFAULT_MINIMUM = 64;
// Use an initial value that is bigger than the common MTU of 1500
static final int DEFAULT_INITIAL = 2048;
static final int DEFAULT_MAXIMUM = 65536;
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战