一、TCP连接
1 三次握手
2 四次挥手
3 长连接和短连接
短连接的操作步骤是:
建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
长连接的操作步骤是:
建立连接——数据传输…(保持连接)…数据传输——关闭连接
正常来说,TCP连接建立后,只要不主动释放,连接会一直存在,所以为了避免无用连接占用资源导致客户端无法建立新连接,就需要保活机制,保活机制在传输层和应用层都有实现。短连接每次交互后会主动释放连接,不需要保活。
二、tcp keep-alive
传输层保活机制
tcp具有保活功能,当tcp服务端回复之后会开启保活定时器,时间一到就会发送探测报文,重复多次后没有得到响应,则关闭连接。这个功能不是tcp的,而是内核支持的。
三、应用层保活机制
以netty举例,通过IdleStateHandler来保活。
1 client
代码语言:java复制@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// send heartbeat when read idle.
if (evt instanceof IdleStateEvent) {
try {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
if (logger.isDebugEnabled()) {
logger.debug("IdleStateEvent triggered, send heartbeat to channel " channel);
}
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setEvent(HEARTBEAT_EVENT);
channel.send(req);
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
2 server
代码语言:java复制public class NettyServerHandler extends ChannelDuplexHandler {
private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
/**
* the cache for alive worker channel.
* <ip:port, dubbo channel>
*/
private final Map<String, Channel> channels = new ConcurrentHashMap<>();
private final URL url;
private final ChannelHandler handler;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// server will close channel when server don't receive any heartbeat from client util timeout.
if (evt instanceof IdleStateEvent) {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
logger.info("IdleStateEvent triggered, close channel " channel);
//关闭
channel.close();
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
super.userEventTriggered(ctx, evt);
}
}
3 IdleStateHandler
该类会开启心跳定时器,如果超时,会立刻注册一个IdleStateEvent
代码语言:java复制private void initialize(ChannelHandlerContext ctx) {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
switch (state) {
case 1:
case 2:
return;
default:
break;
}
state = 1;
initOutputChanged(ctx);
//开启定时器,
//客户端每过心跳间隔就立刻发送心跳。
//服务端定时扫描连接上次读写的时间,如果超时则关闭。
lastReadTime = lastWriteTime = ticksInNanos();
if (readerIdleTimeNanos > 0) {
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (writerIdleTimeNanos > 0) {
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (allIdleTimeNanos > 0) {
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
大概处理逻辑是:
- client开启定时任务,每隔一个心跳时间就发送一个心跳包,如果多次失败就reconnect。
- server开启定时任务来扫描,如果发现某条连接超过若干个心跳没有收到请求,则表示这条连接可能已经结束了,直接close,及时回收掉资源,避免文件句柄的浪费。
四、总结长连接适用场景
- 连接频繁,复用连接,可以减少连接创建和释放的开销,适用于客户端比较稳定的场景。个人觉得内部服务之间的RPC比较稳定,适合长连接。与终端用户的交互不太稳定,适合短连接。
- 会一直占用文件句柄,需要保活机制及时释放掉断连的连接。
tcp保活机制在内核实现,不太适应应用层,不区分长连接和短连接。可能因为应用层导致无法及时响应请求,但连接还是正常的。tcp保活机制探测时间太长,会持续几个小时,反应不够及时,而应用层保活机制更适合业务开发。