服務端主動推送技術☞WebSocket
[toc]
簡介
- 什麼是WebSocket
WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動傳送資訊給客戶端
- 實用場景
用到服務端主動推送的地方,都會使用WebSocket來實現,如:
彈幕,網頁聊天系統,實時監控,股票行情推送等
術語
單播(Unicast):
點對點,私信私聊
廣播(Broadcast)(所有人):
遊戲公告,釋出訂閱
多播,也叫組播(Multicast)(特地人群):
多人聊天,釋出訂閱
複製程式碼
webjar
1、方便統一管理
2、主要解決前端框架版本不一致,檔案混亂等問題
3、把前端資源,打包成jar包,藉助maven工具進行管理
複製程式碼
既然用管理jar的方式管理js,那麼這個專案肯定是沒有前後端分離的。
對於純前端專案,有其他方式去管理js版本與依賴。就像maven管理jar那樣方便。
編寫基本WebSocket服務端
pom
- SpringBoot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
複製程式碼
- WebSocket依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
複製程式碼
配置類:WebSocketConfig
package com.example.websocket.websocketdemo01.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import com.example.websocket.websocketdemo01.intecepter.HttpHandShakeIntecepter;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 註冊端點,釋出或者訂閱訊息的時候需要連線此端點
* setAllowedOrigins 非必須,*表示允許其他域進行連線
* withSockJS 表示開始sockejs支援
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpoint-websocket")
// .addInterceptors(new HttpHandShakeIntecepter())
.setAllowedOrigins("*").withSockJS();
}
/**
* 配置訊息代理(中介)
* enableSimpleBroker 服務端推送給客戶端的路徑字首
* setApplicationDestinationPrefixes 客戶端傳送資料給伺服器端的一個字首
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/chat");
registry.setApplicationDestinationPrefixes("/app");
}
}
複製程式碼
Controller
package com.example.websocket.websocketdemo01.controller.v1;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import com.example.websocket.websocketdemo01.model.InMessage;
import com.example.websocket.websocketdemo01.model.OutMessage;
@Controller
public class GameInfoController {
//接收訊息
@MessageMapping("/v1/chat")
//傳送訊息
@SendTo("/topic/game_chat")
public OutMessage gameInfo(InMessage message){
System.out.println("GameInfoController->gameInfo");
return new OutMessage(message.getContent());
}
}
複製程式碼
測試
- 管理端
http://localhost:8080/v1/admin.html
- 客戶端
http://localhost:8080/v1/index.html
連線上伺服器後,管理端傳送的內容會顯示在客戶端
小結
至此,我們的基本的服務端就算編寫完畢了
當客戶端連線上/endpoint-websocket
後,可以往/v1/chat
傳送訊息,並且監聽/topic/game_chat
,服務端會將訊息發往/topic/game_chat
任何客戶端,只要監聽了/topic/game_chat
,就會收到這個推送
服務端主動推送訊息
在上一章中,服務端通過接收前端的WebSocket請求進行響應,其實還是一個請求響應推送,只不過這個過程中連結不斷來。
當我們使用WebSocket的時候,更多情況下都是服務端被客戶端連線上後進行主動推送,這個時候該怎麼做呢?
@Controller
public class GameInfoController {
@Autowired
private SimpMessagingTemplate template;
@GetMapping("/v1/chat/http")
@ResponseBody
public OutMessage gameInfoHttp(InMessage message) {
System.out.println("gameInfoHttp");
OutMessage outMessage = new OutMessage(message.getContent());
template.convertAndSend("/topic/game_chat", new OutMessage(message.getContent()));
return outMessage;
}
}
複製程式碼
只要使用SimpMessagingTemplate
,就可以往指定destination
傳送特定的資料,只要監聽了這個destination
的客戶端都會收到
測試
訪問介面:http://localhost:8080/v1/chat/http?from=1&to=2&content=哇哈哈
可以看到http://localhost:8080/v1/index.html
收到的服務端的推送
關於客戶端連線地址:ws or http
因為我們後端使用的是stomp協議,所以此時客戶仍舊使用http進行連線
如果要使用ws
進行連線,那麼後端做修改
去掉withSocketJs
前端做修改
stompClient的構建方式發生了點變化
@SendTo
與SimpMessagingTemplate
@SendTo
不夠通用,固定傳送給指定的訂閱者
SimpMessagingTemplate
比較靈活
simpMessagingTemplate.convertAndSend("/topic/game_chat", new OutMessage(message.getContent()));
複製程式碼
可以動態的指定要傳送給誰
SpringBoot對WebSocket的監聽
連線監聽
package com.example.websocket.websocketdemo01.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;
@Component
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent>{
@Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("【ConnectEventListener監聽器事件 型別】"+headerAccessor.getCommand().getMessageType());
}
}
複製程式碼
當客戶端連線的時候,會觸發CONNECT
事件
訂閱監聽
package com.example.websocket.websocketdemo01.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
@Component
public class SubscribeEventListener implements ApplicationListener<SessionSubscribeEvent>{
/**
* 在事件觸發的時候呼叫這個方法
*
* StompHeaderAccessor 簡單訊息傳遞協議中處理訊息頭的基類,
* 通過這個類,可以獲取訊息型別(例如:釋出訂閱,建立連線斷開連線),會話id等
*
*/
@Override
public void onApplicationEvent(SessionSubscribeEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("【SubscribeEventListener監聽器事件 型別】"+headerAccessor.getCommand().getMessageType());
System.out.println("【SubscribeEventListener監聽器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
}
}
複製程式碼
當客戶端連線的時候,會觸發SUBSCRIBE
事件
取消監聽事件:SessionUnsubscribeEvent
客戶端斷開監聽
package com.example.websocket.websocketdemo01.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
@Component
public class DissconnectEventListener implements ApplicationListener<SessionDisconnectEvent>{
/**
* 在事件觸發的時候呼叫這個方法
*
* StompHeaderAccessor 簡單訊息傳遞協議中處理訊息頭的基類,
* 通過這個類,可以獲取訊息型別(例如:釋出訂閱,建立連線斷開連線),會話id等
*
*/
@Override
public void onApplicationEvent(SessionDisconnectEvent sessionDisconnectEvent) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(sessionDisconnectEvent.getMessage());
System.out.println("【SubscribeEventListener監聽器事件 型別】"+headerAccessor.getCommand().getMessageType());
System.out.println("【SubscribeEventListener監聽器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
}
}
複製程式碼
當客戶端連線的時候,會觸發DISCONNECT
事件
獲取客戶端id
@Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("【ConnectEventListener監聽器事件 型別】"+headerAccessor.getCommand().getMessageType());
System.out.println("simpSessionId\t"+headerAccessor.getHeader("simpSessionId"));
}
複製程式碼
這個SimpSessionId
會在客戶端連線、訂閱、斷線等情況下獲取到,可以用於標記客戶端
而在上面的監聽器中,我們使用
System.out.println("【SubscribeEventListener監聽器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
複製程式碼
來獲取sessionId,這個好需要我們編寫攔截器,將sessionId手動放到這個SessionAttributes,才能取到。
攔截器
package com.example.websocket.websocketdemo01.intecepter;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
public class HttpHandShakeIntecepter implements HandshakeInterceptor{
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("【握手攔截器】beforeHandshake");
if(request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
HttpSession session = servletRequest.getServletRequest().getSession();
String sessionId = session.getId();
System.out.println("【握手攔截器】beforeHandshake sessionId="+sessionId);
attributes.put("sessionId", sessionId);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
System.out.println("【握手攔截器】afterHandshake");
if(request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
HttpSession session = servletRequest.getServletRequest().getSession();
String sessionId = session.getId();
System.out.println("【握手攔截器】afterHandshake sessionId="+sessionId);
}
}
}
複製程式碼
- 註冊攔截器
在攔截器中,我們把sessionId放在了session中,此時WebSocket的監聽器就能夠獲取到sessionId
headerAccessor.getSessionAttributes().get("sessionId")
複製程式碼
點對點傳送
客戶端訂閱的端,需要唯一,這個需要通過客戶端通過引數傳遞上來
template.convertAndSend("/chat/single/"+message.getTo(),
new OutMessage(message.getFrom()+" 傳送:"+ message.getContent()));
複製程式碼
同樣的,對於組播來說,只要保證客戶端的訂閱頻道是同一組的就行。
一般對於組
與點
的定義,都會通過業務來處理
Nginx反向代理WebSocket
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
ip_hash; #使用ip固定轉發到後端伺服器
server localhost:3100;
server localhost:3101;
server localhost:3102;
}
server {
listen 8020;
location / {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; # 宣告支援websocket
}
}
}
#http/2 nginx conf
#server{
# listen 443;
# server_name example.com www.example.com;
# root /Users/welefen/Develop/git/firekylin/www;
# set $node_port 8360;
# ssl on;
# ssl_certificate %path/ssl/chained.pem;
# ssl_certificate_key %path/ssl/domain.key;
# ssl_session_timeout 5m;
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
# ssl_session_cache shared:SSL:50m;
# ssl_dhparam %path/ssl/dhparams.pem;
# ssl_prefer_server_ciphers on;
# index index.js index.html index.htm;
# location ^~ /.well-known/acme-challenge/ {
# alias %path/ssl/challenges/;
# try_files $uri = 404;
# }
# location / {
# proxy_http_version 1.1;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header Host $http_host;
# proxy_set_header X-NginX-Proxy true;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_pass http://127.0.0.1:$node_port$request_uri;
# proxy_redirect off;
# }
# location = /development.js {
# deny all;
# }
# location = /testing.js {
# deny all;
# }
# location = /production.js {
# deny all;
# }
# location ~ /static/ {
# etag on;
# expires max;
# }
#}
#server {
# listen 80;
# server_name example.com www.example.com;
# rewrite ^(.*) https://example.com$1 permanent;
#}
複製程式碼
階段1原始碼
截止當前的所有程式碼位於:碼雲