天天看點

WebSocket的使用

目錄

      • WebSocket簡介
      • SpringBoot整合WebScoket
        • 依賴
        • 方式一
        • 方式二
        • 方式三(推薦)
      • WebSocket的監聽器
      • WebSocket的攔截器

WebSocket簡介

WebSocket是基于TCP的一種新的網絡協定,實作了浏覽器、伺服器之間的全雙工通信,允許伺服器主動發送資訊給用戶端。

WebSocket隻需要一次HTTP握手,整個通訊過程建立在一次連接配接狀态中。

WebSocket常用于伺服器推送實時資料給用戶端,常見的應用場景如下

  • 彈幕
  • 網頁聊天系統
  • 公告
  • 實時資料監控,eg. 雙11交易資料的實時監控
  • 實時資料推送,eg. 伺服器給炒股軟體(用戶端)實時推送k線圖走勢

WebSocket中的廣播分為3類

  • 單點傳播:點對點,常用于私信、私聊​
  • 多點傳播:也叫多點傳播,常用于推送消息給特定人群,eg. 群聊,推送消息給該群中的所有人。常用于多人聊天、釋出訂閱
  • 廣播:推送消息給所有人,常用于推送公告、釋出訂閱

SpringBoot整合WebScoket

依賴

Messaging -> 勾選WebSocket,也可以手動添加依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
           

websocket前後端的寫法很多,此處介紹3種,第三種簡單強大,推薦。

方式一

背景自己實作Endpoint,前端使用内置的WebSocket。

配置類

@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 掃描@ServerEndpoint,将@ServerEndpoint修飾的類注冊為websocket
     * 如果使用外置tomcat,則不需要此配置
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter()
    {
        return new ServerEndpointExporter();
    }

}
           

Endpoint 端點

@ServerEndpoint("/websocket")
@Component  //放到spring容器中
@Slf4j
public class WebSocketServer{

    /**
     * 所有連接配接的用戶端
     */
    private static ConcurrentHashMap<String,Session> clients = new ConcurrentHashMap<>();


    /**
     * 建立連接配接時調用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        clients.put(session.getId(),session);
        //向特定使用者發送消息,使用的session是接收方的session
        session.getAsyncRemote().sendText("已加入群聊");
    }


    /**
     * 連接配接關閉時調用的方法
     */
    @OnClose
    public void onClose(Session session) {
        clients.remove(session.getId());
        session.getAsyncRemote().sendText("已退出群聊");
    }


    /**
     * 收到用戶端發送過來的消息時調用的方法
     * @param msg 用戶端使用者發送過來的消息,二進制可以聲明為byte[]
     */
    @OnMessage
    public void onMessage(String msg) {
        //群發消息
        for (Session session : clients.values()) {
            session.getAsyncRemote().sendText(msg);
        }
    }


    /**
     * 發生錯誤時調用的方法
     */
    @OnError
    public void onError(Session session, Throwable e) {
        log.error("發送錯誤的sessionId:"+session.getId()+",錯誤資訊:"+e.getMessage());
    }

}
           

接收到前端傳來的消息時,會自動調用onMessage()方法。

前端

<script>
    let socket;

    //手動打開連接配接
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您使用的浏覽器不支援WebSocket");
        }else{
            //連接配接到websocket的某個endpoint
            socket = new WebSocket("ws://127.0.0.1:8080/websocket");
            //以下幾個方法相當于事件監聽,在特定事件觸發時會自動調用
            socket.onopen = () => {
                console.log("已連接配接到websocket");
            };
            socket.onmessage = resp => {
                console.log("接收到服務端資訊:" + resp.data);
            };
            socket.onclose = () => {
                console.log("已斷開websocket連接配接");
            };
            socket.onerror = () => {
                console.log("websocket發生錯誤");
            }
        }
    }

    //手動關閉連接配接
    function closeSocket() {
        socket.close();
    }

    //發送消息到伺服器
    function sendMsg(msg) {
        //參數不一定要是字元串類型,可以是任意類型(二進制資料)
        socket.send(msg);
    }

</script>
           

說明

1、如果要同時實作單發、群發

  • 前端可以将msg寫成對象,設定發送類型、接收方等屬性,将對象轉換為json字元串進行發送,然後在服務端的onMessage()中解析
  • 也可以多寫幾個endpoint,一個endpoint作為單發、一個作為群發

2、getBasicRemote()、getAsyncRemote()的差別

getBasicRemote()是同步的,getAsyncRemote()是異步的,getBasicRemote()會抛出異常。

eg. sendText(),sendText() 前後發送2次消息

如果是同步的,會等第一個sendText()執行完畢才執行第二個sendText();如果是異步的,第一個sendText()開始執行後就繼續往下執行代碼。

3、sendText()發送文本内容,sendBinary()發送二進制資料

4、方式一簡單,但功能單一、能接收的資料類型有限,适合隻群發、不單發,消息内容簡單的場景。

方式二

後端使用@MessageMapping、@SendTo指定接受、推送位址,前端使用sockjs+stomp。

sockjs封裝了websocket,stomp是消息隊列模式。

配置類

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注冊端點
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注冊endpoint(服務端點),用于接收用戶端連接配接,用戶端的SockJS通過端點連接配接到websocket
        // setAllowedOrigins是配置跨域,withSockJS是啟用SockJS支援
        // 可注冊多個端點
        registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
    }


    /**
     * 配置伺服器接收消息、推送消息的位址字首
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 用戶端訂閱位址字首,用于用戶端訂閱服務端的某個
        // registry.enableSimpleBroker("/topic");

        // 服務端接收位址字首,用于服務端接收用戶端的消息
        // registry.setApplicationDestinationPrefixes("/app");
    }

}
           

實體類

@Getter
@Setter
@ToString
@AllArgsConstructor
public class Msg implements Serializable {

    private String msgContent;

    private String fromUserId;

    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    private Date sendTime;

}
           

處理消息的controller

@Controller
public class MsgController {
    
    @MessageMapping("/app/serverReceive")  //@MessageMapping指定伺服器接收消息的位址,此方法隻處理發給該位址的消息
    @SendTo("/topic/serverPush")  //@SendTo指定消息的推送位址,會把return傳回的資料推送到指定的位址
    public Msg msgHandler(Msg message){
        return message;
    }

}
           

前端

要引入2個核心的js檔案:sockjs.js、stomp.js

<noscript>您使用的浏覽器不支援websocket</noscript>

<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

<script type="text/javascript">
    let stompClient = null;

    //連接配接到websocket
    function connect() {
        //SockJS連接配接的是端點endpoint
        let socket = new SockJS('/socket');
        stompClient = Stomp.over(socket);

        //參數:請求頭設定,連接配接成功的回調函數,連接配接失敗的回調函數
        stompClient.connect({},frame => {
            console.log("連接配接成功");
            //stompClient訂閱的是推送位址。參數:訂閱位址、回調函數
            stompClient.subscribe('/topic/serverPush',resp => {
                // console.log(resp.body)
            });
        },error => {
            console.log("連接配接失敗")
        });
    }


    //斷開連接配接
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        console.log("已斷開連接配接");
    }


    //發送消息
    function sendMsg() {
        //消息可以是對象
        let msg={ 'fromUserId': $('#fromUserId').val(),'msgContent': $('#msgContent').val(),'sendTime':new Date()};
        //參數:伺服器接收位址,請求頭設定,消息内容
        stompClient.send("/app/serverReceive", {}, JSON.stringify(msg));
    }

</script>
           

說明

1、不管在配置類中配不配置訂閱位址的字首,@SendTo()中都要寫全全路徑,不能省略字首。

如果在配置中配置了接受位址的字首,@MessageMapping中要省略字首;如果沒配置接受位址的字首,@MessageMapping中必須要寫全路徑。

2、fromUserId給其它使用者推送消息,推送時fromUserId本身也會收到自己發出的消息,但這個消息是本地的msg,其它人收到的msg是服務端推送的msg,可能在原msg的基礎上做了修改。

消息最好在本地就配置好,盡量不要在服務端修改消息,以保證每個使用者收到的消息是一緻的(主要是發送方和接受方收到的消息一緻)。

3、此種方式可以接受多種類型的消息内容,主要是可以接受對象形式的消息内容,但@SendTo适合群發,實作單點發送有點麻煩。此種方式與方式一相比,優點是可以接收對象形式的消息。

方式三(推薦)

在方式二的基礎上改進,使用SimpMessagingTemplate代替@SendTo注解。

配置類

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注冊端點
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
    }


    /**
     * 配置伺服器接收消息、推送消息的位址字首
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 用戶端訂閱位址字首,用于用戶端訂閱服務端的某個
        // 需要加上點對點的字首
        registry.enableSimpleBroker("/topic","/user");

        // 服務端接收位址字首,用于服務端接收用戶端的消息
        registry.setApplicationDestinationPrefixes("/app");

        //點對點使用的訂閱字首
        registry.setUserDestinationPrefix("/user");
    }

}
           

實體類

@Getter
@Setter
@ToString
@AllArgsConstructor
public class Msg implements Serializable {

    private String msgContent;

    private String fromUserId;

    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    private Date sendTime;

}
           

處理消息的controller

@Controller
public class MsgController {

    @Autowired
    private SimpMessagingTemplate template;

    /**
     * 群發
     */
    @MessageMapping("/toAll")
    public void toAll(Msg msg) {
        //convertAndSend代替@SendTo指定目标位址
        template.convertAndSend("/topic/toAll", msg);
    }


    /**
     * 點對點
     */
    @MessageMapping("/toOne")
    // @Scheduled(fixedDelay = 1000L)  //可以使用定時器實作定時推送
    public void toOne(Msg msg) {
        //參數:目前使用者的辨別,目标位址,消息内容。會将消息推送到目前使用者的頻道中,所有訂閱了目前使用者的用戶端都會收到消息
        template.convertAndSendToUser(msg.getFromUserId(), "/toOne", msg);
    }


}
           

前端

<noscript>您使用的浏覽器不支援websocket</noscript>

<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

<script type="text/javascript">
    let stompClient = null;

    //連接配接到websocket
    function connect() {
        //SockJS連接配接的是端點endpoint
        let socket = new SockJS('/socket');
        stompClient = Stomp.over(socket);

        //參數:請求頭設定,連接配接成功的回調函數,連接配接失敗的回調函數
        stompClient.connect({},frame => {
            console.log("連接配接成功");
            //訂閱全體消息
            stompClient.subscribe('/topic/toAll',resp => {
                // console.log(resp.body)
            });
            //訂閱點對點消息,/user/toUserId/toOne 此處訂閱的是目标使用者的消息
            stompClient.subscribe('/user/' + $("#toUserId").val() + '/toOne', resp => {
                // console.log("ok:"+resp.body);
            });
        },error => {
            console.log("連接配接失敗")
        });
    }


    //斷開連接配接
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        console.log("已斷開連接配接");
    }


    //群發消息
    function sendMsgToAll() {
        let msg={ 'fromUserId': $('#fromUserId').val(),'msgContent': $('#msgContent').val(),'sendDate':new Date()};
        //參數:伺服器接收位址,請求頭設定,消息内容
        stompClient.send("/app/toAll", {}, JSON.stringify(msg));
    }

    //點對點
    function sendMsgToOne() {
        let msg={ 'fromUserId': $('#fromUserId').val(),'msgContent': $('#msgContent').val(),'sendDate':new Date()};
        //伺服器接收位址天@MessageMapping映射的即可
        stompClient.send("/app/toOne", {}, JSON.stringify(msg));
    }
    
</script>
           

說明

SimpMessagingTemplate比@SendTo更加靈活,支援多種發送方式,即可以實作群發,又可以實作點對點,且可以接收對象類型的消息,十分強大。

WebSocket的監聽器

WebSocket的監聽器可以監聽以下事件

  • SessionSubscribeEvent 訂閱
  • SessionUnsubscribeEvent 取消訂閱
  • SessionDisconnectEvent 斷開連接配接
  • SessionConnectEvent 建立連接配接

使用示例

建立類listener.ConnectEventListener

@Component  //放到spring容器中
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> {

    @Override
    public void onApplicationEvent(SessionConnectEvent event) {
        StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(event.getMessage());
        System.out.println("事件類型:"+headerAccessor.getCommand().getMessageType());
    }

}
           

需要實作ApplicationListener接口,泛型指定要監聽的事件類型。一個類隻能監聽一個事件,如果要監聽多個事件,需要寫多個類。

WebSocket的攔截器

WebSocket的一次通信隻需要1次握手,HandshakeInterceptor 握手攔截器可以在握手前後進行攔截,做一些處理。

建立類intecepter.MyHandShakeIntecepter

public class MyHandShakeIntecepter implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request,ServerHttpResponse response,WebSocketHandler wsHandler,Map<String, Object> attributes) {
        ServletServerHttpRequest req = (ServletServerHttpRequest) request;
        HttpSession session = req.getServletRequest().getSession();
        // 傳回的boolean辨別是否繼續往下執行
        return true;
    }


    @Override
    public void afterHandshake(ServerHttpRequest request,ServerHttpResponse response, WebSocketHandler wsHandler,Exception exception) {
        ServletServerHttpRequest req = (ServletServerHttpRequest) request;
        HttpSession session = req.getServletRequest().getSession();
    }


}
           

需要在websocket的配置中添加要使用的攔截器

/**
 * 注冊端點
 */
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/socket")
            .addInterceptors(new MyHandShakeIntecepter())  //攔截器
            .setAllowedOrigins("*")   
            .withSockJS();
}
           

繼續閱讀