定义
websocket是什么
WebSocket是一种在单个TCP连接上进行全双工通讯的协议.简单来说就是客户端与服务端建立起长连接可以相互发送消息.
websocket使用场景
主要用在对消息实时性比较高的场景.用来替代
轮询
方案
- 实时在线聊天
- 浏览器之间的协同编辑工作
- 多人在线游戏
浏览器支持websocket的版本
WebSocket通信协议于
2011
年被修订为RFC 6455
的标准.所以对浏览器、后端服务器是有要求的.以下是被支持的版本
tomcat支持websocket的版本
http://tomcat.apache.org/(7.0.27支持websocket,建议用tomcat8,7.0.27中的接口已经过时)
浏览器与服务器之间连接如何建立(通信协议)
Websocket 通过HTTP/1.1 协议的101状态码进行握手,升级成websocket连接
- 请求
# Websocket使用ws或wss统一资源标志符(必填)
GET ws://localhost:8090/ws/stomp/561/abkkwlke/websocket HTTP/1.1
# 升级成websocket协议(必填)
Upgrade: websocket
# Connection必须设置Upgrade,表示客户端希望连接升级(必填)
Connection: Upgrade
# Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面
Origin: http://example.com
# Sec-WebSocket-Key 服务端会用来验证该请求是否是websocket请求,尽量避免与http请求被误认为websocket(必填)
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
# Websocket支持的版本(必填)
Sec-WebSocket-Version: 13
- 响应
# 响应的状态码,必须是101
HTTP/1.1 101 //
# 升级的协议
Upgrade: websocket
# 表示客户端希望连接升级
Connection: upgrade
# 服务端根据Sec-WebSocket-Key生成,用来验证该请求是websocket请求
Sec-WebSocket-Accept: V395OugSb9uYXr6dA44VGcn/oAM=
浏览器与服务器之间数据如何传输(数据协议)
STOMP 是基于 WebSocket的上层协议,提供了一个基于帧的线路格式层,用来定义消息语义.提供了一套完整websocket数据传输的api.让前后端能够快速变现.
- 消息发送的格式
# stomp命令
SEND
# 服务端接口
destination:/ws/broadcast
content-length:87
# 内容 可以是json格式
{"destination":"/topic","payload":"1231231","onErrorDestination":"/topic"}
- 支持的命令
- SEND
- SUBSCRIBE
- UNSUBSCRIBE
- BEGIN
- COMMIT
- ABORT
- ACK
- NACK
- DISCONNECT
浏览器与服务器之间如何实现消息的广播、点对点传输
主要通过发布/订阅的模式来实现
-
广播思路
- 浏览器订阅主题: /topic
- 服务器发送消息到主题/topic
- 所有订阅的浏览器都能收到消息
-
点对点的思路(浏览器A->B)
- 浏览器B订阅主题: /user/B/topic
- 浏览器A发送消息到主题: /user/B/topic
- 浏览器B就能收到消息
如何使用
后端使用
spring boot整合websocket
- pom
org.springframework.boot
spring-boot-starter-websocket
- stomp的配置
@Configuration
@ComponentScan("com.websocket.test")
@EnableConfigurationProperties(value = {WebSocketProperties.class})
@EnableWebSocketMessageBroker
public class WebSocketConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketProperties webSocketProperties;
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
// 注册一个Stomp的节点(endpoint),并指定使用SockJS协议。
stompEndpointRegistry
.addEndpoint(webSocketProperties.getEndPoint())
.setAllowedOrigins(webSocketProperties.getAllowedOrigins())
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义心跳线程
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setThreadNamePrefix("wss-heartbeat-thread-");
taskScheduler.setDaemon(true);
taskScheduler.initialize();
// 服务端发送消息给客户端的域,多个用逗号隔开
registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker())
// 定义心跳间隔 单位(ms)
.setHeartbeatValue(new long[]{webSocketProperties.getHeartBeatInterval(), webSocketProperties.getHeartBeatInterval()})
.setTaskScheduler(taskScheduler);
// 定义webSocket前缀
registry.setApplicationDestinationPrefixes(webSocketProperties.getApplicationDestinationPrefixes());
}
- yml
把stomp的相关配置做成配置文件,配置在yml中
commons.websocket:
# 监听的节点
endPoint: "/ws/stomp"
# 跨域支持
allowedOrigins: "*"
# 可订阅的主题
enableSimpleBroker:
- "/topic"
- "/queue"
- "/user"
- "/client"
# 客户端向服务器发消息时的前缀
applicationDestinationPrefixes: "/ws"
注册stomp节点
stompEndpointRegistry.addEndpoint("/ws/stomp")
定义支持订阅的主题列表
# 可订阅的主题
enableSimpleBroker:
- "/topic"
- "/queue"
- "/user"
- "/client"
registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker());
定义跨域的支持
stompEndpointRegistry.setAllowedOrigins("*")
定义心跳的支持
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义心跳线程
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setThreadNamePrefix("wss-heartbeat-thread-");
taskScheduler.setDaemon(true);
taskScheduler.initialize();
// 服务端发送消息给客户端的域,多个用逗号隔开
registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker())
// 定义心跳间隔 单位(ms)
.setHeartbeatValue(new long[]{webSocketProperties.getHeartBeatInterval(), webSocketProperties.getHeartBeatInterval()})
.setTaskScheduler(taskScheduler);
}
事件的监听
服务器可以监听到websocket的连接、已连接、订阅、退订、断开事件: .然后可以根据事件来做相应的业务处理.
- 例子
当某个客户端断开连接之后.发送消息到指定的topic
/**
* 断开事件,当某个客户端断开连接之后.发送消息到指定的topic
*/
@Slf4j
@Component
public class WebSocketOnDisconnectEventListener implements ApplicationListener {
@Autowired
private WebSocketService webSocketService;
@Override
public void onApplicationEvent(SessionDisconnectEvent sessionDisconnectEvent) {
log.info("WebSocketOnDisconnectEventListener ... ");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sessionDisconnectEvent.getMessage());
if (sha.getSessionAttributes().get("onDisconnectTopic") != null) {
String onDisconnectTopic = (String) sha.getSessionAttributes().get("onDisconnectTopic");
String clientId = (String) sha.getSessionAttributes().get("clientId");
webSocketService.send(
WebSocketMsgDefaultVo
.builder()
.payload(clientId + "断开连接")
.destination(onDisconnectTopic)
.build()
);
}
}
}
session的获取
服务器可以监听浏览器连接成功事件,获取session信息,用来确定哪个浏览器
@Slf4j
@Component
public class WebSocketOnConnectedEventListener implements ApplicationListener {
@Override
public void onApplicationEvent(SessionConnectedEvent sessionConnectEvent) {
String sessionId = (String) sessionConnectEvent.getMessage().getHeaders().get("simpSessionId");
log.info("sessionId: {} ", sessionId);
log.info("WebSocketOnConnectedEventListener ...");
}
}
INFO c.k.k.k.w.l.WebSocketOnConnectedEventListener - sessionId: 4gfxeh2z
INFO c.k.k.k.w.l.WebSocketOnConnectedEventListener - WebSocketOnConnectedEventListener ...
发送消息的接口
- spring boot中如何开启
浏览器发送消息给服务端,并且广播、点对点的发送给相应的其他浏览器.这里我们使用@MessageMapping注解来开启
- 自定义路由与封装的方法 例如 广播(
broadcast
)、点对点单播(unicast
)
@Slf4j
@Controller
public class WebSocketController {
@Autowired
private WebSocketService webSocketService;
@MessageMapping("/broadcast")
public ResponseMessage broadcast(WebSocketMsgDefaultVo vo) throws Exception {
log.info("/web_socket/broadcast test ... ", vo.toString());
webSocketService.send(vo);
return ResponseMessage.ok(vo.getPayload());
}
@MessageMapping("/unicast")
public ResponseMessage unicast(WebSocketMsgDefaultVo vo) throws Exception {
log.info("/web_socket/unicast test ... {} ", vo.toString());
webSocketService.send(vo.getUserId(), vo);
return ResponseMessage.ok(vo.getPayload());
}
}
做成基础组件
可以把上面整合spring boot的示例.做成基础组件starter.给其他模块调用.这样别人使用就可以不考虑整合的细节.只要关注与业务的实现
pom
com.example
websocket-starter
yml配置
commons.websocket:
# 监听的节点
endPoint: "/ws/stomp"
# 跨域支持
allowedOrigins: "*"
# 可订阅的主题
enableSimpleBroker:
- "/topic"
- "/queue"
- "/user"
- "/client"
# 客户端向服务器发消息时的前缀
applicationDestinationPrefixes: "/ws"
# 心跳的间隔
heartBeatInterval: 10000
前端使用
使用
stomp js
来操作websocket
官网api地址
https://stomp-js.github.io/stomp-websocket/codo/class/Client.html
引入
连接
// 开启socket连接
function connect() {
var socket = new SockJS('/ws/stomp');
stompClient = Stomp.over(socket);
stompClient.connect({"userId": "1", "onDisconnectTopic": "/topic", "clientId": "1"}, function (frame) {
setConnected(true);
subscribe();
});
}
订阅
function subscribe() {
console.log("subscribe");
stompClient.subscribe("/topic", function (data) {
var message = data.body;
messageList.append("" + message + " ");
});
}
发送消息
// 向‘/ws/customizedcast’服务端发送消息
function sendName() {
var value = document.getElementById('name').value;
stompClient.send("/ws/clientcast", {}, JSON.stringify({
"destination": "/topic",
"payload": "payload " + value,
"clientId": "1",
"onErrorDestination":"/topic"
}));
}
断开
// 断开socket连接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect(function (frame) {
setConnected(false);
}, {"userId": "1", "onDisconnectTopic": "/topic", "clientId": "1"});
}
console.log("Disconnected");
}
心跳
为了使客户端与服务器的连接保活(若客户端、服务器长时间不通信,就会断开)定义了一套维护心跳的机制.就是客户端会起定时任务发送
ping
帧,服务端收到返回一个pong
帧消息.来保证连接的存活
>>> PING stomp.min.js:8
<<< PONG stomp.min.js:8
例子
简易聊天室
1. 打开浏览器A,B
2. A广播消息 1
3. B广播消息 2
4. A发送消息a给B
5. B发送消息b给A
- 最后显示如下
思考题
- 客户端如何处理断线重连机制
- 客户端如何处理事务的发送机制
- 服务器如何处理统一异常
参考资料
- https://spring.io/guides/gs/messaging-stomp-websocket/
- https://zh.wikipedia.org/wiki/WebSocket#cite_note-4
- https://stomp-js.github.io/stomp-websocket/codo/class/Client.html