Appearance
SockJS Fallback:WebSocket 的优雅降级方案 🔄
引言:为什么需要 SockJS?
想象一下,你精心开发了一个基于 WebSocket 的实时聊天应用,在本地测试一切完美。但当部署到生产环境后,却发现部分用户无法正常使用——他们的网络代理服务器阻止了 WebSocket 连接,或者使用的是不支持 WebSocket 的老旧浏览器。这就是 SockJS 要解决的核心问题。
IMPORTANT
SockJS 的设计哲学:让应用使用 WebSocket API,但在必要时自动降级到非 WebSocket 替代方案,而无需修改应用代码。
什么是 SockJS?
SockJS(Socket.JS)是一个 JavaScript 库,它为 WebSocket 提供了一个类似的对象,但会在 WebSocket 不可用时自动降级到其他传输方式。
SockJS 的核心组件
传输方式的降级策略
SockJS 支持三大类传输方式,按优先级排序:
- WebSocket - 最优选择
- HTTP Streaming - 次优选择
- HTTP Long Polling - 保底选择
TIP
SockJS 客户端会首先发送 GET /info
请求获取服务器信息,然后根据浏览器支持情况选择最佳传输方式。
在 Spring Boot 中启用 SockJS
基础配置
kotlin
@Configuration
@EnableWebSocket
class WebSocketConfiguration : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(myHandler(), "/chat")
.withSockJS()
}
@Bean
fun myHandler(): WebSocketHandler {
return ChatWebSocketHandler()
}
}
kotlin
class ChatWebSocketHandler : TextWebSocketHandler() {
private val sessions = mutableSetOf<WebSocketSession>()
override fun afterConnectionEstablished(session: WebSocketSession) {
sessions.add(session)
logger.info("客户端连接建立: ${session.id}")
}
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
// 广播消息给所有连接的客户端
sessions.forEach { webSocketSession ->
if (webSocketSession.isOpen) {
webSocketSession.sendMessage(message)
}
}
}
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
sessions.remove(session)
logger.info("客户端连接关闭: ${session.id}")
}
companion object {
private val logger = LoggerFactory.getLogger(ChatWebSocketHandler::class.java)
}
}
SockJS 的 URL 结构
SockJS 的所有传输请求都遵循以下 URL 结构:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
其中:
{server-id}
- 用于集群路由(通常不使用){session-id}
- 关联属于同一 SockJS 会话的 HTTP 请求{transport}
- 传输类型(如websocket
、xhr-streaming
等)
SockJS 的工作原理
连接建立流程
消息帧格式
SockJS 添加了最小的消息帧:
o
- "open" 帧(连接建立)a["message1","message2"]
- 消息帧(JSON 编码数组)h
- "heartbeat" 帧(心跳,默认 25 秒)c
- "close" 帧(关闭连接)
处理 IE 8/9 兼容性问题
核心挑战
WARNING
Internet Explorer 8 和 9 不支持标准的 WebSocket,这是使用 SockJS 的重要原因之一。
解决方案配置
kotlin
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/portfolio")
.withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js")
}
// 其他配置...
}
WARNING
如果使用 iframe 传输方式,需要注意 X-Frame-Options
头部设置。Spring Security 默认设置为 DENY
,可能需要调整为 SAMEORIGIN
或 ALLOW-FROM <origin>
。
心跳机制
为什么需要心跳?
心跳配置
kotlin
@Configuration
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/websocket")
.withSockJS()
.setHeartbeatTime(25000) // 25秒心跳间隔
}
}
NOTE
默认心跳间隔为 25 秒,这符合 IETF 对公共互联网应用的建议。
客户端断开检测
挑战
Servlet API 不提供客户端断开的通知,但 SockJS 通过心跳机制可以在 25 秒内(或更短时间)检测到客户端断开。
日志处理
kotlin
// 在 application.yml 中配置
logging:
level:
org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession: TRACE
TIP
Spring 会尽力识别客户端断开导致的网络故障,并使用专用日志类别 DISCONNECTED_CLIENT_LOG_CATEGORY
记录最小消息。
CORS 支持
自动 CORS 处理
SockJS 协议在 XHR streaming 和 polling 传输中使用 CORS 进行跨域支持:
kotlin
@Configuration
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/websocket")
.setAllowedOrigins("https://example.com")
.withSockJS()
}
}
CORS 头部
SockJS 期望以下 CORS 头部:
Access-Control-Allow-Origin
- 从Origin
请求头初始化Access-Control-Allow-Credentials
- 始终设置为true
Access-Control-Allow-Methods
- 传输支持的 HTTP 方法Access-Control-Max-Age
- 设置为 31536000(1年)
SockJS Java 客户端
使用场景
SockJS Java 客户端特别适用于:
- 服务器间双向通信
- 测试目的(模拟大量并发用户)
- 网络代理阻止 WebSocket 的环境
客户端配置示例
kotlin
// 配置传输方式
val transports = mutableListOf<Transport>().apply {
add(WebSocketTransport(StandardWebSocketClient()))
add(RestTemplateXhrTransport())
}
// 创建 SockJS 客户端
val sockJsClient = SockJsClient(transports)
// 连接到 SockJS 端点
sockJsClient.doHandshake(
MyWebSocketHandler(),
"ws://example.com:8080/sockjs"
)
高并发配置
kotlin
// 配置 Jetty HTTP 客户端以支持大量并发连接
val jettyHttpClient = HttpClient().apply {
maxConnectionsPerDestination = 1000
executor = QueuedThreadPool(1000)
}
// 服务端配置优化
@Configuration
class WebSocketConfig : WebSocketMessageBrokerConfigurationSupport() {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/sockjs")
.withSockJS()
.setStreamBytesLimit(512 * 1024) // 512KB
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000) // 30秒
}
}
实际应用示例
聊天应用完整示例
点击查看完整的聊天应用示例
kotlin
// 前端 JavaScript 代码
const socket = new SockJS('/chat');
socket.onopen = function() {
console.log('连接已建立');
document.getElementById('status').textContent = '已连接';
};
socket.onmessage = function(event) {
const message = event.data;
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML += '<div>' + message + '</div>';
};
socket.onclose = function() {
console.log('连接已关闭');
document.getElementById('status').textContent = '已断开';
};
function sendMessage() {
const input = document.getElementById('messageInput');
socket.send(input.value);
input.value = '';
}
kotlin
// 后端 WebSocket 处理器
@Component
class ChatWebSocketHandler : TextWebSocketHandler() {
private val sessions = Collections.synchronizedSet(mutableSetOf<WebSocketSession>())
override fun afterConnectionEstablished(session: WebSocketSession) {
sessions.add(session)
broadcastMessage("用户 ${session.id} 加入聊天室")
logger.info("新用户连接: ${session.id}")
}
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
val username = session.attributes["username"] ?: "匿名用户"
val chatMessage = "$username: ${message.payload}"
broadcastMessage(chatMessage)
}
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
sessions.remove(session)
broadcastMessage("用户 ${session.id} 离开聊天室")
logger.info("用户断开连接: ${session.id}")
}
private fun broadcastMessage(message: String) {
sessions.removeIf { session ->
try {
if (session.isOpen) {
session.sendMessage(TextMessage(message))
false
} else {
true
}
} catch (e: Exception) {
logger.error("发送消息失败", e)
true
}
}
}
companion object {
private val logger = LoggerFactory.getLogger(ChatWebSocketHandler::class.java)
}
}
最佳实践与建议
1. 开发阶段
TIP
在开发阶段启用 SockJS 客户端的 devel
模式,防止浏览器缓存 SockJS 请求。
2. 生产环境
生产环境注意事项
- 合理配置心跳间隔
- 监控客户端断开情况
- 优化服务器资源配置
- 考虑负载均衡和集群部署
3. 错误处理
kotlin
override fun handleTransportError(session: WebSocketSession, exception: Throwable) {
logger.error("传输错误: ${session.id}", exception)
// 实现重连逻辑或错误恢复机制
}
总结
SockJS 为 WebSocket 应用提供了强大的降级机制,确保在各种网络环境和浏览器中都能正常工作。通过合理配置和使用 SockJS,我们可以构建出既现代又兼容的实时通信应用。
IMPORTANT
记住 SockJS 的核心价值:透明的降级机制让开发者专注于业务逻辑,而不用担心底层传输的兼容性问题。
🎉 现在你已经掌握了 SockJS 的核心概念和实际应用,可以开始构建自己的实时通信应用了!