Websocket——心跳检测

1. 前言:为什么需要心跳机制?

在现代的实时网络应用中,保持客户端和服务端的连接稳定性是非常重要的。尤其是在长时间的网络连接中,存在一些异常情况,导致服务端无法及时感知到客户端的断开,可能造成不必要的资源浪费,甚至是服务端的潜在错误。为了避免这种情况,我们需要一种机制来确保连接的有效性,这就是"心跳机制"。

心跳机制的必要性

心跳机制的作用在于周期性地检测连接是否仍然活跃。简单来说,心跳就像人类的心跳一样,不断"跳动",如果在规定的时间内没有收到心跳信号,服务端就可以判断客户端可能已经断开连接,从而主动释放资源或者做出其他处理。

异常断开连接的场景

在正常情况下,前端和后端的连接断开是可以通过调用相关方法来通知对方的。例如,当用户关闭浏览器或者点击"退出"按钮时,前端可以主动向服务端发送断开连接的请求,服务端也可以通过监听断开事件来进行清理工作。

然而,如果用户的浏览器突然崩溃、网络中断或者关闭页面时,前端无法发送断开请求,服务端也无法及时感知到客户端已经下线。在这种情况下,服务端就需要一个手段来周期性地检查连接是否还存在。

服务端如何通过心跳保持客户端状态

为了应对这种情况,心跳机制便应运而生。心跳的基本原理是客户端定时发送一个简单的信号(通常是一个空的数据包)到服务端。服务端通过检测这个信号是否按时到达来判断客户端是否仍然连接。如果在规定时间内没有收到心跳包,服务端就认为该客户端可能已经断开,并可以主动关闭连接或执行其他操作。

Netty 作为一个高性能的网络框架,内置了非常方便的心跳机制实现工具------IdleStateHandler。通过这个工具,开发者可以非常方便地设置心跳检测的时间间隔,以及如何处理空闲状态,从而确保网络连接的健康和稳定。

小结

心跳机制在分布式应用、即时通讯、在线游戏等场景中是非常关键的,它帮助服务端及时发现并处理客户端断开的情况,避免资源的浪费和潜在的服务异常。接下来,我们将深入介绍 Netty 如何利用心跳机制来维持连接的稳定性。

2. Netty 心跳机制的实现原理

Netty 提供了 IdleStateHandler 组件,它是处理心跳机制的关键工具。这个处理器能够帮助我们自动监测连接的空闲状态,并且根据设定的时间间隔触发心跳事件,从而帮助服务端检测客户端是否还保持连接。

2.1 IdleStateHandler 的作用

IdleStateHandler 是 Netty 提供的一个特殊的 ChannelHandler,主要作用是根据指定的时间,自动检测连接的空闲状态。它通过配置三个时间参数来定义空闲状态:

  • readerIdleTime:如果在指定的时间内没有读取到数据,触发空闲事件;
  • writerIdleTime:如果在指定的时间内没有写入数据,触发空闲事件;
  • allIdleTime:如果在指定的时间内既没有读也没有写,触发空闲事件。

通常情况下,我们会使用 readerIdleTime 来进行心跳检测。也就是说,客户端需要定期发送数据包(通常是心跳包)给服务端,确保在规定时间内,服务端能够检测到客户端的活动。如果服务端在设定的时间内没有收到心跳包,就会触发相应的空闲事件(如 IdleStateEvent),然后服务端可以采取关闭连接等措施。

2.2 工作原理

Netty 的心跳机制的工作过程通常如下:

  1. 客户端:每隔一定时间(如 10 秒),客户端向服务端发送一个"心跳包",该包通常是一个简单的请求或一个空的数据包,目的是告诉服务端"我还活着"。
  2. 服务端:服务端在接收到客户端的心跳包后,更新连接的活跃状态,并且继续等待客户端的心跳信号。
  3. 超时检测 :如果在规定的时间(如 30 秒)内,服务端没有收到客户端的心跳包,就会触发 IdleStateEvent,并根据配置的事件类型,执行相关的处理逻辑。
  4. 断开连接:当服务端检测到客户端超过了心跳的最大空闲时间后,会主动断开连接,释放资源,避免无效连接占用资源。

2.3 Netty 实现步骤

通过 IdleStateHandler 实现心跳机制的步骤如下:

  1. 创建 IdleStateHandler :在管道(Pipeline)中添加 IdleStateHandler,并配置读、写或总空闲时间。
  2. 自定义事件处理器 :当空闲时间触发时,IdleStateHandler 会触发 IdleStateEvent 事件,开发者可以通过自定义事件处理器来处理这些事件。
  3. 关闭连接:当空闲事件触发时,服务端可以根据具体的业务逻辑决定是否关闭连接或执行其他操作。

2.4 IdleStateHandler 配置实例

假设我们希望每 30 秒检测一次连接,如果 30 秒内没有收到客户端的数据(读空闲),则认为该连接不再活跃,主动断开连接。那么我们可以在 Netty 服务器的 ChannelPipeline 中这样配置:

复制代码
// 30秒内没有读数据即认为连接空闲,触发读空闲事件
pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));

在这里,30 表示如果在 30 秒内没有接收到任何读操作的数据包,Netty 会触发一个 IdleStateEvent,而 0 表示我们不关心写空闲和总空闲的状态。

小结

通过 IdleStateHandler,Netty 提供了非常便捷的机制来处理心跳事件,确保服务端能够及时发现客户端是否断开。接下来的部分,我们将更深入地探讨如何自定义事件处理器,以及如何根据空闲事件的触发来处理连接的关闭或其他业务逻辑。

3. 自定义处理空闲事件

在使用 IdleStateHandler 配置了心跳检测后,我们需要编写一个自定义的事件处理器来响应空闲事件的触发。这个处理器将会监听并处理由 IdleStateHandler 触发的 IdleStateEvent,并根据实际需求采取相应的操作。

3.1 IdleStateEvent 介绍

IdleStateEvent 是 Netty 提供的一个事件对象,表示连接进入了空闲状态。它由 IdleStateHandler 触发,常见的事件类型有:

  • reader_idle:表示连接在指定的时间内没有读取到任何数据,即"读取空闲";
  • writer_idle:表示连接在指定的时间内没有写入任何数据,即"写入空闲";
  • all_idle:表示连接在指定的时间内既没有读也没有写,即"完全空闲"。

通常我们关心的主要是 reader_idle 类型的事件,因为我们希望通过客户端定期发送心跳包,服务端来验证连接是否活跃。

3.2 自定义事件处理器 NettyWebSocketServerHandler

接下来,我们编写一个 NettyWebSocketServerHandler 类,来处理客户端的请求并处理空闲事件。

java 复制代码
public class NettyWebSocketServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 这里可以处理业务逻辑,比如接收来自客户端的数据包
        super.channelRead(ctx, msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 判断是否是 IdleStateEvent 空闲事件
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            
            // 处理读空闲事件
            if (event.state() == IdleState.READER_IDLE) {
                System.out.println("连接空闲,关闭连接:无数据读取!");
                
                // 如果超时没有读数据,认为该连接断开,关闭连接
                ctx.close();  // 关闭连接
            }
        }
    }
}

在上面的代码中,我们实现了 userEventTriggered 方法来处理 IdleStateEvent 事件。当事件类型为 READER_IDLE(即读取空闲事件)时,我们输出日志并关闭连接。此时,服务端通过调用 ctx.close() 关闭连接,释放相关资源。

3.3 将 NettyWebSocketServerHandler 添加到管道

Netty 服务器的 ChannelPipeline 中,添加自定义的 NettyWebSocketServerHandler 处理器,使得它能处理客户端的空闲事件。

java 复制代码
public class NettyWebSocketServer {

    public void start() throws InterruptedException {
        // 设置事件处理器链
        EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 用于接收客户端连接
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理读写操作

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();

                     // 添加空闲状态检测处理器,配置30秒没有读操作触发事件
                     pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
                     // 添加自定义的事件处理器来处理空闲事件
                     pipeline.addLast(new NettyWebSocketServerHandler());
                 }
             });

            // 绑定端口,启动服务器
            b.bind(8090).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

ChannelInitializer 中,我们首先添加了 IdleStateHandler,配置了读空闲的时间为 30 秒。然后,我们添加了自定义的 NettyWebSocketServerHandler 来处理空闲事件。

3.4 处理空闲事件后进行用户下线操作

除了关闭连接外,我们还可以在空闲事件发生时进行更复杂的操作,例如清理用户会话、推送离线通知等。

假设我们有一个用户管理的类来保存当前活跃的 WebSocket 连接,当连接空闲时,我们不仅关闭连接,还可以将该用户从在线列表中移除。

java 复制代码
public class UserManager {
    private static Map<String, Channel> activeUsers = new ConcurrentHashMap<>();

    public static void addUser(String userId, Channel channel) {
        activeUsers.put(userId, channel);
    }

    public static void removeUser(String userId) {
        activeUsers.remove(userId);
    }
}

public class NettyWebSocketServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            
            if (event.state() == IdleState.READER_IDLE) {
                // 这里假设可以通过 channel 获取用户的 ID
                String userId = (String) ctx.channel().attr(UserSession.USER_ID).get();

                System.out.println("用户 " + userId + " 超时,关闭连接!");
                
                // 移除该用户
                UserManager.removeUser(userId);

                // 关闭连接
                ctx.close();
            }
        }
    }
}

在这个修改版的 NettyWebSocketServerHandler 中,我们假设每个连接都有一个 userId,通过 channelattr 方法获取用户 ID,断开连接时将该用户从 UserManager 中移除。

小结

Netty 的心跳机制和空闲事件处理功能非常强大,它通过 IdleStateHandler 自动检测连接的空闲状态,帮助服务端发现和处理长时间不活动的客户端连接。通过自定义的事件处理器,我们可以在空闲事件触发时,进行连接关闭、资源清理、用户下线等操作,确保服务器能够及时响应并释放资源。

4. 心跳机制的优化与扩展

在实现了基本的心跳检测后,我们可以进一步对心跳机制进行优化和扩展。心跳机制的设计不仅仅是为了检测连接是否存活,还可以用于其他优化,例如:

4.1 调整心跳时间间隔

默认情况下,我们在 Netty 服务器端设置了 IdleStateHandler(30, 0, 0),即 30 秒内没有收到客户端的消息,就会触发 READER_IDLE 事件。但在实际应用中,我们可以根据业务需求调整心跳的频率:

  • 如果服务器的负载较高,可以适当增加心跳间隔,例如 1 分钟检测一次,减少无用的心跳消息,降低服务器压力。
  • 如果对在线状态的准确性要求较高,可以缩短心跳间隔,例如 10~15 秒检测一次,以便尽快发现连接异常。

心跳间隔需要根据实际业务进行权衡:间隔太短会增加服务器负担,间隔太长可能会导致掉线检测不及时

4.2 采用双向心跳

目前我们的设计是 由客户端定期发送心跳包,服务器被动检测 。但在一些场景下,例如 移动端网络不稳定、浏览器休眠、弱网环境等 ,可能会导致客户端心跳发送失败或延迟。为此,我们可以采用 双向心跳 机制,即:

  • 客户端主动发送心跳(例如每 10 秒发送一次)。
  • 服务器也定期主动向客户端发送心跳请求,如果客户端在规定时间内没有响应,则认为连接已断开。

这样可以 确保双向通信的可靠性,避免单方面心跳导致的误判。

4.3 结合 Redis 或数据库存储用户在线状态

在多服务器(集群)环境下,单个服务器维护的连接信息可能会不够准确。例如,某个用户可能已经断线,但由于服务器没有立即感知,导致用户状态仍然是"在线"。
为了解决这个问题,我们可以:

  • 将用户的心跳时间存入 Redis,每次收到心跳更新 Redis 中的时间戳。
  • 其他服务器可以通过 Redis 检测用户是否长时间没有发送心跳,从而更准确地判断用户在线状态。

这样,即使用户的 WebSocket 连接在某个服务器上断开了,整个系统仍然可以通过 Redis 统一管理用户的在线状态

4.4 结合 Netty 的自定义 ChannelHandler

除了 IdleStateHandler 之外,我们还可以自定义一个 HeartbeatHandler 来进行更加灵活的心跳控制。例如:

  • 记录心跳次数,如果 连续 3 次心跳超时,才真正断开连接,避免短暂的网络抖动影响用户体验。
  • 结合 流量控制,如果服务器在高负载状态下,可以适当放宽心跳检测标准,防止误判导致大规模掉线。

通过这些优化,我们可以让 心跳机制更加智能、灵活、稳定,提高 WebSocket 连接的可靠性,为后续的即时通讯、推送等功能提供坚实的基础。

5. 具体实现心跳检测

在前面的介绍中,我们提到了 Netty 提供的 IdleStateHandler 组件,它可以帮助我们 检测连接是否空闲 。现在,我们来看它的 具体实现

5.1 服务器端的心跳检测

NettyWebSocketServer 中,我们已经添加了 IdleStateHandler(30, 0, 0),即 如果 30 秒内没有收到客户端的消息,就会触发 READER_IDLE 事件
但是,仅仅触发事件是不够的 ,我们还需要在 Handler 中监听这个事件,并进行相应的处理。

步骤 1:继承 SimpleChannelInboundHandler<TextWebSocketFrame>

我们需要自定义一个 NettyWebSocketServerHandler用于处理心跳事件WebSocket 消息

java 复制代码
public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;

            if (event.state() == IdleState.READER_IDLE) {
                System.out.println("【心跳超时】关闭连接:" + ctx.channel().remoteAddress());
                ctx.channel().close();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
        System.out.println("收到消息:" + msg.text());
        ctx.writeAndFlush(new TextWebSocketFrame("服务器已收到消息"));
    }
}

代码解析

  1. 监听 IdleStateEvent 事件

    • event.state() == IdleState.READER_IDLE 说明 30 秒内没有收到消息 ,意味着客户端可能已经断线,我们就 手动关闭连接
  2. 处理正常的 WebSocket 消息

    • channelRead0 方法用于处理 客户端发来的普通消息 ,这里简单打印出来,并返回一个 确认消息

5.2 客户端的心跳发送

为了防止服务器误判掉线,客户端需要定期发送心跳消息。
前端(JavaScript)可以这样实现:

java 复制代码
let socket = new WebSocket("ws://localhost:8090/ws");

socket.onopen = function () {
    console.log("WebSocket 连接成功");
    setInterval(() => {
        if (socket.readyState === WebSocket.OPEN) {
            socket.send("ping");
        }
    }, 10000); // 每 10 秒发送一次心跳
};

socket.onmessage = function (event) {
    console.log("收到服务器消息: " + event.data);
};

socket.onclose = function () {
    console.log("WebSocket 连接关闭");
};

代码解析

  1. 建立 WebSocket 连接 ,监听 onopen 事件。
  2. 每 10 秒发送 "ping" 消息,保持连接活跃。
  3. 监听服务器的 onmessage 事件,打印服务器返回的消息。
  4. 监听 onclose 事件,一旦连接断开,前端可以尝试重新连接。

6. 服务器如何区分心跳和普通消息?

channelRead0 方法中,我们目前对所有消息都进行了打印和回写。
但在实际应用中,我们需要 区分普通消息和心跳消息,避免误处理:

java 复制代码
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
    String text = msg.text();
    if ("ping".equals(text)) {
        System.out.println("收到客户端心跳");
        ctx.writeAndFlush(new TextWebSocketFrame("pong")); // 返回心跳确认
    } else {
        System.out.println("收到普通消息:" + text);
        ctx.writeAndFlush(new TextWebSocketFrame("服务器已收到消息:" + text));
    }
}

改进点

  1. 如果收到 "ping" ,说明是 心跳消息 ,直接返回 "pong",避免误处理。
  2. 如果收到普通消息,进行正常的逻辑处理。

7. 心跳机制测试

  1. 正常连接时

    • 前端每 10 秒发送 "ping",服务器返回 "pong",连接保持活跃。
  2. 如果前端关闭网页

    • 服务器在 30 秒后触发 READER_IDLE 事件,自动断开连接。
  3. 如果网络异常

    • 服务器仍然可以在 30 秒后感知到超时,并清理资源,保证不会有 无效连接 长时间占用服务器资源。

这样,我们就完成了 基于 Netty 的 WebSocket 心跳检测 ,并且实现了 前端心跳发送、后端心跳检测、心跳超时处理等功能

相关推荐
luckywuxn8 分钟前
ant design pro 项目发布遇到登录页访问404
运维·服务器·网络
火云牌神28 分钟前
本地大模型编程实战(32)用websocket显示大模型的流式输出
python·websocket·llm·fastapi·流式输出
lml485633 分钟前
Web基础与HTTP协议
网络·网络协议·http
时迁24742 分钟前
【计算机网络】DHCP——动态配置ip地址
网络·网络协议·dhcp
遇见火星4 小时前
Linux 下使用tcpdump进行网络分析原
linux·网络·tcpdump
189228048614 小时前
NV203NV207SSD固态闪存NV208NV213
网络·数据库·oracle
wfsm4 小时前
计算机网络01-网站数据传输过程
网络·计算机网络·智能路由器
Charlotte's diary4 小时前
OSPF路由协议配置
网络·计算机网络·智能路由器
firshman_start5 小时前
第六章,BGP---边界网关协议
开发语言·网络·php
chichengfeng15 小时前
HTTP和HTTPS
网络·http·https