定義
websocket是什么
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通訊的協(xié)議.簡單來說就是客戶端與服務(wù)端建立起長連接可以相互發(fā)送消息.
websocket使用場景
主要用在對消息實(shí)時(shí)性比較高的場景.用來替代
輪詢方案
- 實(shí)時(shí)在線聊天
- 瀏覽器之間的協(xié)同編輯工作
- 多人在線游戲
瀏覽器支持websocket的版本
WebSocket通信協(xié)議于
2011年被修訂為RFC 6455的標(biāo)準(zhǔn).所以對瀏覽器、后端服務(wù)器是有要求的.以下是被支持的版本

tomcat支持websocket的版本
http://tomcat.apache.org/(7.0.27支持websocket,建議用tomcat8,7.0.27中的接口已經(jīng)過時(shí))
瀏覽器與服務(wù)器之間連接如何建立(通信協(xié)議)
Websocket 通過HTTP/1.1 協(xié)議的101狀態(tài)碼進(jìn)行握手,升級成websocket連接
- 請求
# Websocket使用ws或wss統(tǒng)一資源標(biāo)志符(必填)
GET ws://localhost:8090/ws/stomp/561/abkkwlke/websocket HTTP/1.1
# 升級成websocket協(xié)議(必填)
Upgrade: websocket
# Connection必須設(shè)置Upgrade,表示客戶端希望連接升級(必填)
Connection: Upgrade
# Origin字段是可選的,通常用來表示在瀏覽器中發(fā)起此Websocket連接所在的頁面
Origin: http://example.com
# Sec-WebSocket-Key 服務(wù)端會用來驗(yàn)證該請求是否是websocket請求,盡量避免與http請求被誤認(rèn)為websocket(必填)
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
# Websocket支持的版本(必填)
Sec-WebSocket-Version: 13
- 響應(yīng)
# 響應(yīng)的狀態(tài)碼,必須是101
HTTP/1.1 101 //
# 升級的協(xié)議
Upgrade: websocket
# 表示客戶端希望連接升級
Connection: upgrade
# 服務(wù)端根據(jù)Sec-WebSocket-Key生成,用來驗(yàn)證該請求是websocket請求
Sec-WebSocket-Accept: V395OugSb9uYXr6dA44VGcn/oAM=
瀏覽器與服務(wù)器之間數(shù)據(jù)如何傳輸(數(shù)據(jù)協(xié)議)
STOMP 是基于 WebSocket的上層協(xié)議,提供了一個(gè)基于幀的線路格式層,用來定義消息語義.提供了一套完整websocket數(shù)據(jù)傳輸?shù)腶pi.讓前后端能夠快速變現(xiàn).
- 消息發(fā)送的格式
# stomp命令
SEND
# 服務(wù)端接口
destination:/ws/broadcast
content-length:87
# 內(nèi)容 可以是json格式
{"destination":"/topic","payload":"1231231","onErrorDestination":"/topic"}
- 支持的命令
- SEND
- SUBSCRIBE
- UNSUBSCRIBE
- BEGIN
- COMMIT
- ABORT
- ACK
- NACK
- DISCONNECT
瀏覽器與服務(wù)器之間如何實(shí)現(xiàn)消息的廣播、點(diǎn)對點(diǎn)傳輸
主要通過發(fā)布/訂閱的模式來實(shí)現(xiàn)
-
廣播思路
- 瀏覽器訂閱主題: /topic
- 服務(wù)器發(fā)送消息到主題/topic
- 所有訂閱的瀏覽器都能收到消息
-
點(diǎn)對點(diǎn)的思路(瀏覽器A->B)
- 瀏覽器B訂閱主題: /user/B/topic
- 瀏覽器A發(fā)送消息到主題: /user/B/topic
- 瀏覽器B就能收到消息
如何使用
后端使用
spring boot整合websocket
- pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 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) {
// 注冊一個(gè)Stomp的節(jié)點(diǎn)(endpoint),并指定使用SockJS協(xié)議。
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();
// 服務(wù)端發(fā)送消息給客戶端的域,多個(gè)用逗號隔開
registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker())
// 定義心跳間隔 單位(ms)
.setHeartbeatValue(new long[]{webSocketProperties.getHeartBeatInterval(), webSocketProperties.getHeartBeatInterval()})
.setTaskScheduler(taskScheduler);
// 定義webSocket前綴
registry.setApplicationDestinationPrefixes(webSocketProperties.getApplicationDestinationPrefixes());
}
- yml
把stomp的相關(guān)配置做成配置文件,配置在yml中
commons.websocket:
# 監(jiān)聽的節(jié)點(diǎn)
endPoint: "/ws/stomp"
# 跨域支持
allowedOrigins: "*"
# 可訂閱的主題
enableSimpleBroker:
- "/topic"
- "/queue"
- "/user"
- "/client"
# 客戶端向服務(wù)器發(fā)消息時(shí)的前綴
applicationDestinationPrefixes: "/ws"
注冊stomp節(jié)點(diǎn)
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();
// 服務(wù)端發(fā)送消息給客戶端的域,多個(gè)用逗號隔開
registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker())
// 定義心跳間隔 單位(ms)
.setHeartbeatValue(new long[]{webSocketProperties.getHeartBeatInterval(), webSocketProperties.getHeartBeatInterval()})
.setTaskScheduler(taskScheduler);
}
事件的監(jiān)聽
服務(wù)器可以監(jiān)聽到websocket的連接、已連接、訂閱、退訂、斷開事件: .然后可以根據(jù)事件來做相應(yīng)的業(yè)務(wù)處理.
- 例子
當(dāng)某個(gè)客戶端斷開連接之后.發(fā)送消息到指定的topic
/**
* 斷開事件,當(dāng)某個(gè)客戶端斷開連接之后.發(fā)送消息到指定的topic
*/
@Slf4j
@Component
public class WebSocketOnDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent> {
@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的獲取
服務(wù)器可以監(jiān)聽瀏覽器連接成功事件,獲取session信息,用來確定哪個(gè)瀏覽器
@Slf4j
@Component
public class WebSocketOnConnectedEventListener implements ApplicationListener<SessionConnectedEvent> {
@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 ...
發(fā)送消息的接口
- spring boot中如何開啟
瀏覽器發(fā)送消息給服務(wù)端,并且廣播、點(diǎn)對點(diǎn)的發(fā)送給相應(yīng)的其他瀏覽器.這里我們使用@MessageMapping注解來開啟
- 自定義路由與封裝的方法 例如 廣播(
broadcast)、點(diǎn)對點(diǎn)單播(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());
}
}
做成基礎(chǔ)組件
可以把上面整合spring boot的示例.做成基礎(chǔ)組件starter.給其他模塊調(diào)用.這樣別人使用就可以不考慮整合的細(xì)節(jié).只要關(guān)注與業(yè)務(wù)的實(shí)現(xiàn)
pom
<dependency>
<groupId>com.example</groupId>
<artifactId>websocket-starter</artifactId>
</dependency>
yml配置
commons.websocket:
# 監(jiān)聽的節(jié)點(diǎn)
endPoint: "/ws/stomp"
# 跨域支持
allowedOrigins: "*"
# 可訂閱的主題
enableSimpleBroker:
- "/topic"
- "/queue"
- "/user"
- "/client"
# 客戶端向服務(wù)器發(fā)消息時(shí)的前綴
applicationDestinationPrefixes: "/ws"
# 心跳的間隔
heartBeatInterval: 10000
前端使用
使用
stomp js來操作websocket
官網(wǎng)api地址
https://stomp-js.github.io/stomp-websocket/codo/class/Client.html
引入
<script type="text/javascript" src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
連接
// 開啟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("<li>" + message + "</li>");
});
}
發(fā)送消息
// 向‘/ws/customizedcast’服務(wù)端發(fā)送消息
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");
}
心跳
為了使客戶端與服務(wù)器的連接?;?若客戶端、服務(wù)器長時(shí)間不通信,就會斷開)定義了一套維護(hù)心跳的機(jī)制.就是客戶端會起定時(shí)任務(wù)發(fā)送
ping幀,服務(wù)端收到返回一個(gè)pong幀消息.來保證連接的存活
>>> PING stomp.min.js:8
<<< PONG stomp.min.js:8
例子
簡易聊天室
1. 打開瀏覽器A,B
2. A廣播消息 1
3. B廣播消息 2
4. A發(fā)送消息a給B
5. B發(fā)送消息b給A
- 最后顯示如下

思考題
- 客戶端如何處理斷線重連機(jī)制
- 客戶端如何處理事務(wù)的發(fā)送機(jī)制
- 服務(wù)器如何處理統(tǒng)一異常