如果没有宽恕之心,生命会被无休止的仇恨和报复所支配。——阿萨吉奥

WebSocket 是一种轻量级、双向的实时通信协议,在现代 Web 应用中非常流行。它为客户端和服务端提供了长连接能力,适用于需要频繁数据交互的场景。然而,在实际开发中,我们经常需要处理 WebSocket 的关闭事件,而 关闭状态(CloseStatus) 是其中一个重要的概念,它能够帮助开发者理解连接关闭的原因,从而采取相应的措施。

什么是 CloseStatus?

在 WebSocket 协议中,每次连接关闭都会携带一个 关闭码(close code) 和可选的 关闭原因(reason phrase)。这些关闭码由 RFC 6455 定义,表示连接关闭的原因。例如:

  • 1000 (Normal Closure): 正常关闭,表示连接完成。
  • 1001 (Going Away): 客户端或服务端主动断开(例如页面关闭)。
  • 1002 (Protocol Error): 协议错误。
  • 1003 (Unsupported Data): 不支持的数据类型。

在 Spring Framework 中,org.springframework.web.socket.CloseStatus 提供了对这些状态的封装,便于我们处理 WebSocket 关闭事件。

1
2
3
4
5
6
7
8
9
10
    @Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String deviceId = (String) session.getAttributes().get("deviceId");
if (deviceId == null || deviceId.isEmpty()) {
session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Missing deviceId"));
return;
}
deviceSessionMap.put(deviceId, session);
session.sendMessage(new TextMessage("连接成功,设备ID: " + deviceId));
}

Spring WebSocket 中的 CloseStatus

Spring 提供了 CloseStatus 类来封装关闭码和原因。以下是 CloseStatus 的关键方法和属性:

  • getCode() 获取关闭码。
  • getReason() 获取关闭的原因(可能为空)。
  • 常量值: Spring 提供了常见关闭状态的预定义常量,例如 CloseStatus.NORMALCloseStatus.PROTOCOL_ERROR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 CloseStatus 处理关闭事件
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("WebSocket 连接已关闭");
System.out.println("关闭码: " + status.getCode());
System.out.println("关闭原因: " + status.getReason());

if (status.equals(CloseStatus.NORMAL)) {
System.out.println("连接正常关闭");
} else if (status.getCode() == 1006) { // Abnormal closure
System.out.println("连接异常关闭");
} else {
System.out.println("关闭状态: " + status);
}
}

应用场景:处理不同的 CloseStatus

  1. 正常关闭 (1000)
    适用于连接完成或用户主动断开。可以在关闭事件中释放资源、关闭相关线程或记录日志。

  2. 异常关闭 (1006)
    常见于网络问题或客户端断开。可以设置重连机制来保持连接的稳定性。

  3. 协议错误 (1002)
    当客户端发送了不符合协议的数据时,服务端可以选择断开连接。此时应在日志中记录详细信息,方便排查问题。

  4. 服务器繁忙 (1013)
    如果服务端压力过大,可以选择发送此关闭状态,让客户端稍后重试。

1
2
3
4
5
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.err.println("WebSocket 传输错误: " + exception.getMessage());
session.close(CloseStatus.SERVER_ERROR);
}

客户端处理关闭事件

在客户端中,我们也可以捕获 onclose 事件,并基于关闭状态码进行不同的操作。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const socket = new WebSocket("wss://example.com/socket");

socket.onclose = (event) => {
console.log(`WebSocket 关闭: 关闭码 ${event.code}, 原因: ${event.reason}`);

if (event.code === 1000) {
console.log("连接正常关闭");
} else if (event.code === 1006) {
console.log("异常关闭,尝试重连...");
reconnect();
}
};

function reconnect() {
setTimeout(() => {
console.log("尝试重连...");
// 重新连接逻辑
}, 3000);
}

常见问题与最佳实践

1. 为什么会收到 1006 状态?

1006 是由客户端生成的关闭码,通常用于无法与服务端正常通信的场景(例如网络中断)。建议在服务端日志中查看异常原因。

2. 如何向客户端发送自定义关闭状态?

Spring 提供了 WebSocketSession.close(CloseStatus) 方法,可以指定关闭码和原因。

1
session.close(new CloseStatus(4001, "自定义错误: Token 无效"));

客户端会在 onclose 事件中接收到此信息。

3. 如何避免意外关闭?

  • 定期发送心跳(ping/pong)以保持连接活跃。
  • 在连接关闭后实现自动重连。
  • 在关闭前提示用户保存未完成的数据。

状态码一览:

1000 - NORMAL

  • 含义: 连接正常关闭,表明 WebSocket 通信已完成。

  • 应用场景: 客户端或服务端主动关闭连接,释放资源。

  • 示例:

    1
    session.close(CloseStatus.NORMAL);

1001 - GOING_AWAY

  • 含义: 连接关闭是由于某一方离开,例如服务器关闭或浏览器跳转页面。

  • 应用场景: 服务器维护期间关闭连接,或者用户关闭浏览器窗口。

  • 示例:

    1
    session.close(CloseStatus.GOING_AWAY);

1002 - PROTOCOL_ERROR

  • 含义: 由于协议错误而关闭连接。

  • 应用场景: 客户端或服务端未遵循 WebSocket 协议(例如发送非法帧)。

  • 示例:

    1
    session.close(CloseStatus.PROTOCOL_ERROR);

1003 - NOT_ACCEPTABLE

  • 含义: 收到了无法处理的数据类型(例如服务端只接受文本,但收到了二进制消息)。

  • 应用场景: 数据类型不匹配时关闭连接。

  • 示例:

    1
    session.close(CloseStatus.NOT_ACCEPTABLE);

1005 - NO_STATUS_CODE

  • 含义: 未提供状态码的关闭,保留值。
  • 应用场景: 一般用于表示关闭帧中没有状态码,不能直接使用。

1006 - NO_CLOSE_FRAME

  • 含义: 连接非正常关闭,例如未发送关闭帧。

  • 应用场景: 网络中断、客户端或服务端崩溃等。

  • 注意: 此状态码仅在客户端或工具中报告,不会出现在关闭帧中。

  • 示例:

    1
    2
    3
    if (status.equals(CloseStatus.NO_CLOSE_FRAME)) {
    // 记录异常并尝试重连
    }

1007 - BAD_DATA

  • 含义: 收到了与消息类型不一致的数据(例如,非 UTF-8 数据)。

  • 应用场景: 数据格式验证失败时关闭连接。

  • 示例:

    1
    session.close(CloseStatus.BAD_DATA);

1008 - POLICY_VIOLATION

  • 含义: 收到的消息违反了服务器的策略。

  • 应用场景: 服务器限制了某些操作或内容(例如,未授权访问)。

  • 示例:

    1
    session.close(CloseStatus.POLICY_VIOLATION.withReason("Unauthorized access"));

1009 - TOO_BIG_TO_PROCESS

  • 含义: 收到的消息太大,无法处理。

  • 应用场景: 限制消息大小的服务器可能在超出限制时关闭连接。

  • 示例:

    1
    session.close(CloseStatus.TOO_BIG_TO_PROCESS);

1010 - REQUIRED_EXTENSION

  • 含义: 客户端期望服务器支持某些扩展,但服务器未提供。

  • 应用场景: 客户端无法与服务器达成握手协议。

  • 示例:

    1
    session.close(CloseStatus.REQUIRED_EXTENSION.withReason("Missing compression extension"));

1011 - SERVER_ERROR

  • 含义: 服务器由于内部错误无法处理请求。

  • 应用场景: 服务器发生未知异常时关闭连接。

  • 示例:

    1
    session.close(CloseStatus.SERVER_ERROR.withReason("Unexpected internal error"));

1012 - SERVICE_RESTARTED

  • 含义: 服务端正在重启,客户端可以稍后重连。

  • 应用场景: 定期维护或部署新版本时关闭连接。

  • 示例:

    1
    session.close(CloseStatus.SERVICE_RESTARTED);

1013 - SERVICE_OVERLOAD

  • 含义: 服务端过载,建议客户端切换到其他服务器或稍后再试。

  • 应用场景: 服务器资源不足时主动关闭连接。

  • 示例:

    1
    session.close(CloseStatus.SERVICE_OVERLOAD);

1015 - TLS_HANDSHAKE_FAILURE

  • 含义: TLS 握手失败,保留值。
  • 应用场景: 用于标记安全连接建立失败的情况。

扩展状态码

4500 - SESSION_NOT_RELIABLE

  • 含义: 会话变得不可靠,例如在超时发送消息时。

  • 应用场景: 服务器检测到会话不稳定时可主动关闭连接。

  • 示例:

    1
    session.close(CloseStatus.SESSION_NOT_RELIABLE);