【websocket】spring boot 集成 websocket 的四種方式

集成 websocket 的四種方案

1. 原生注解

pom.xml

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

WebSocketConfig

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author buhao
 * @version WebSocketConfig.java, v 0.1 2019-10-18 15:45 buhao
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpoint() {
        return new ServerEndpointExporter();
    }
}

說明:

這個配置類很簡單顿天,通過這個配置 spring boot 才能去掃描后面的關于 websocket 的注解

WsServerEndpoint

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.ws;

import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author buhao
 * @version WsServerEndpoint.java, v 0.1 2019-10-18 16:06 buhao
 */
@ServerEndpoint("/myWs")
@Component
public class WsServerEndpoint {

    /**
     * 連接成功
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("連接成功");
    }

    /**
     * 連接關閉
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        System.out.println("連接關閉");
    }

    /**
     * 接收到消息
     *
     * @param text
     */
    @OnMessage
    public String onMsg(String text) throws IOException {
        return "servet 發(fā)送:" + text;
    }
}

說明

這里有幾個注解需要注意一下橄务,首先是他們的包都在 **javax.websocket **下误债。并不是 spring 提供的票编,而 jdk 自帶的月腋,下面是他們的具體作用蟀架。

  1. @ServerEndpoint
  2. 通過這個 spring boot 就可以知道你暴露出去的 ws 應用的路徑,有點類似我們經常用的@RequestMapping榆骚。比如你的啟動端口是8080片拍,而這個注解的值是ws,那我們就可以通過 ws://127.0.0.1:8080/ws 來連接你的應用
  3. @OnOpen
  4. 當 websocket 建立連接成功后會觸發(fā)這個注解修飾的方法妓肢,注意它有一個 Session 參數
  5. @OnClose
  6. 當 websocket 建立的連接斷開后會觸發(fā)這個注解修飾的方法捌省,注意它有一個 Session 參數
  7. @OnMessage
  8. 當客戶端發(fā)送消息到服務端時,會觸發(fā)這個注解修改的方法碉钠,它有一個 String 入參表明客戶端傳入的值
  9. @OnError
  10. 當 websocket 建立連接時出現異常會觸發(fā)這個注解修飾的方法纲缓,注意它有一個 Session 參數

另外一點就是服務端如何發(fā)送消息給客戶端卷拘,服務端發(fā)送消息必須通過上面說的 Session 類,通常是在@OnOpen 方法中祝高,當連接成功后把 session 存入 Map 的 value栗弟,key 是與 session 對應的用戶標識,當要發(fā)送的時候通過 key 獲得 session 再發(fā)送工闺,這里可以通過 session.getBasicRemote_().sendText(_)** 來對客戶端發(fā)送消息乍赫。

2. Spring封裝

pom.xml

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

HttpAuthHandler

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.handler;

import cn.coder4j.study.example.websocket.config.WsSessionManager;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.time.LocalDateTime;

/**
 * @author buhao
 * @version MyWSHandler.java, v 0.1 2019-10-17 17:10 buhao
 */
@Component
public class HttpAuthHandler extends TextWebSocketHandler {

    /**
     * socket 建立成功事件
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Object token = session.getAttributes().get("token");
        if (token != null) {
            // 用戶連接成功,放入在線用戶緩存
            WsSessionManager.add(token.toString(), session);
        } else {
            throw new RuntimeException("用戶登錄已經失效!");
        }
    }

    /**
     * 接收消息事件
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 獲得客戶端傳來的消息
        String payload = message.getPayload();
        Object token = session.getAttributes().get("token");
        System.out.println("server 接收到 " + token + " 發(fā)送的 " + payload);
        session.sendMessage(new TextMessage("server 發(fā)送給 " + token + " 消息 " + payload + " " + LocalDateTime.now().toString()));
    }

    /**
     * socket 斷開連接時
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Object token = session.getAttributes().get("token");
        if (token != null) {
            // 用戶退出斤寂,移除緩存
            WsSessionManager.remove(token.toString());
        }
    }

}

說明

通過繼承 TextWebSocketHandler 類并覆蓋相應方法耿焊,可以對 websocket 的事件進行處理,這里可以同原生注解的那幾個注解連起來看

  1. afterConnectionEstablished 方法是在 socket 連接成功后被觸發(fā)遍搞,同原生注解里的 @OnOpen 功能
  2. **afterConnectionClosed **方法是在 socket 連接關閉后被觸發(fā)罗侯,同原生注解里的 @OnClose 功能
  3. **handleTextMessage **方法是在客戶端發(fā)送信息時觸發(fā),同原生注解里的 @OnMessage 功能

WsSessionManager

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author buhao
 * @version WsSessionManager.java, v 0.1 2019-10-22 10:24 buhao
 */
@Slf4j
public class WsSessionManager {
    /**
     * 保存連接 session 的地方
     */
    private static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>();

    /**
     * 添加 session
     *
     * @param key
     */
    public static void add(String key, WebSocketSession session) {
        // 添加 session
        SESSION_POOL.put(key, session);
    }

    /**
     * 刪除 session,會返回刪除的 session
     *
     * @param key
     * @return
     */
    public static WebSocketSession remove(String key) {
        // 刪除 session
        return SESSION_POOL.remove(key);
    }

    /**
     * 刪除并同步關閉連接
     *
     * @param key
     */
    public static void removeAndClose(String key) {
        WebSocketSession session = remove(key);
        if (session != null) {
            try {
                // 關閉連接
                session.close();
            } catch (IOException e) {
                // todo: 關閉出現異常處理
                e.printStackTrace();
            }
        }
    }

    /**
     * 獲得 session
     *
     * @param key
     * @return
     */
    public static WebSocketSession get(String key) {
        // 獲得 session
        return SESSION_POOL.get(key);
    }
}

說明

這里簡單通過 **ConcurrentHashMap **來實現了一個 session 池溪猿,用來保存已經登錄的 web socket 的 session钩杰。前文提過,服務端發(fā)送消息給客戶端必須要通過這個 session诊县。

MyInterceptor

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.HashMap;
import java.util.Map;

/**
 * @author buhao
 * @version MyInterceptor.java, v 0.1 2019-10-17 19:21 buhao
 */
@Component
public class MyInterceptor implements HandshakeInterceptor {

    /**
     * 握手前
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param attributes
     * @return
     * @throws Exception
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("握手開始");
        // 獲得請求參數
        HashMap<String, String> paramMap = HttpUtil.decodeParamMap(request.getURI().getQuery(), "utf-8");
        String uid = paramMap.get("token");
        if (StrUtil.isNotBlank(uid)) {
            // 放入屬性域
            attributes.put("token", uid);
            System.out.println("用戶 token " + uid + " 握手成功讲弄!");
            return true;
        }
        System.out.println("用戶登錄已失效");
        return false;
    }

    /**
     * 握手后
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param exception
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        System.out.println("握手完成");
    }

}

說明

通過實現 HandshakeInterceptor 接口來定義握手攔截器,注意這里與上面 Handler 的事件是不同的依痊,這里是建立握手時的事件避除,分為握手前與握手后,而 Handler 的事件是在握手成功后的基礎上建立 socket 的連接胸嘁。所以在如果把認證放在這個步驟相對來說最節(jié)省服務器資源瓶摆。它主要有兩個方法 beforeHandshake 與 **afterHandshake **,顧名思義一個在握手前觸發(fā)性宏,一個在握手后觸發(fā)群井。

WebSocketConfig

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import cn.coder4j.study.example.websocket.handler.HttpAuthHandler;
import cn.coder4j.study.example.websocket.interceptor.MyInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * @author buhao
 * @version WebSocketConfig.java, v 0.1 2019-10-17 15:43 buhao
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private HttpAuthHandler httpAuthHandler;
    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
                .addHandler(httpAuthHandler, "myWS")
                .addInterceptors(myInterceptor)
                .setAllowedOrigins("*");
    }
}

說明

通過實現 WebSocketConfigurer 類并覆蓋相應的方法進行 websocket 的配置。我們主要覆蓋 registerWebSocketHandlers 這個方法毫胜。通過向 WebSocketHandlerRegistry 設置不同參數來進行配置书斜。其中 *addHandler 方法添加我們上面的寫的 ws 的 handler 處理類,第二個參數是你暴露出的 ws 路徑酵使。addInterceptors 添加我們寫的握手過濾器荐吉。setAllowedOrigins("") **這個是關閉跨域校驗,方便本地調試凝化,線上推薦打開稍坯。

3. TIO

pom.xml

 <dependency>
     <groupId>org.t-io</groupId>
     <artifactId>tio-websocket-spring-boot-starter</artifactId>
     <version>3.5.5.v20191010-RELEASE</version>
</dependency>

application.xml

tio:
  websocket:
    server:
      port: 8989

說明

這里只配置了 ws 的啟動端口,還有很多配置搓劫,可以通過結尾給的鏈接去尋找

MyHandler

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.handler;

import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;
import org.tio.http.common.HttpRequest;
import org.tio.http.common.HttpResponse;
import org.tio.websocket.common.WsRequest;
import org.tio.websocket.server.handler.IWsMsgHandler;

/**
 * @author buhao
 * @version MyHandler.java, v 0.1 2019-10-21 14:39 buhao
 */
@Component
public class MyHandler implements IWsMsgHandler {
    /**
     * 握手
     *
     * @param httpRequest
     * @param httpResponse
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
        return httpResponse;
    }

    /**
     * 握手成功
     *
     * @param httpRequest
     * @param httpResponse
     * @param channelContext
     * @throws Exception
     */
    @Override
    public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
        System.out.println("握手成功");
    }

    /**
     * 接收二進制文件
     *
     * @param wsRequest
     * @param bytes
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        return null;
    }

    /**
     * 斷開連接
     *
     * @param wsRequest
     * @param bytes
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        System.out.println("關閉連接");
        return null;
    }

    /**
     * 接收消息
     *
     * @param wsRequest
     * @param s
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
        System.out.println("接收文本消息:" + s);
        return "success";
    }
}

說明

這個同上個例子中的 handler 很像瞧哟,也是通過實現接口覆蓋方法來進行事件處理,實現的接口是IWsMsgHandler枪向,它的方法功能如下

  1. handshake
  2. 在握手的時候觸發(fā)
  3. onAfterHandshaked
  4. 在握手成功后觸發(fā)
  5. onBytes
  6. 客戶端發(fā)送二進制消息觸發(fā)
  7. onClose
  8. 客戶端關閉連接時觸發(fā)
  9. onText
  10. 客戶端發(fā)送文本消息觸發(fā)

StudyWebsocketExampleApplication

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */

package cn.coder4j.study.example.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.tio.websocket.starter.EnableTioWebSocketServer;

@SpringBootApplication
@EnableTioWebSocketServer
public class StudyWebsocketExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudyWebsocketExampleApplication.class, args);
    }
}

說明

這個類的名稱不重要勤揩,它其實是你的 spring boot 啟動類,只要記得加上@EnableTioWebSocketServer注解就可以了

STOMP

pom.xml

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

WebSocketConfig

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.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 org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author buhao
 * @version WebSocketConfig.java, v 0.1 2019-10-21 16:32 buhao
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 配置客戶端嘗試連接地址
        registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 設置廣播節(jié)點
        registry.enableSimpleBroker("/topic", "/user");
        // 客戶端向服務端發(fā)送消息需有/app 前綴
        registry.setApplicationDestinationPrefixes("/app");
        // 指定用戶發(fā)送(一對一)的前綴 /user/
        registry.setUserDestinationPrefix("/user/");
    }
}

說明
  1. 通過實現 WebSocketMessageBrokerConfigurer 接口和加上@EnableWebSocketMessageBroker來進行 stomp 的配置與注解掃描秘蛔。
  2. 其中覆蓋 registerStompEndpoints 方法來設置暴露的 stomp 的路徑陨亡,其它一些跨域、客戶端之類的設置深员。
  3. 覆蓋 **configureMessageBroker **方法來進行節(jié)點的配置负蠕。
  4. 其中 **enableSimpleBroker **配置的廣播節(jié)點,也就是服務端發(fā)送消息倦畅,客戶端訂閱就能接收消息的節(jié)點遮糖。
  5. 覆蓋**setApplicationDestinationPrefixes **方法,設置客戶端向服務端發(fā)送消息的節(jié)點叠赐。
  6. 覆蓋 setUserDestinationPrefix 方法欲账,設置一對一通信的節(jié)點。

WSController

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.controller;

import cn.coder4j.study.example.websocket.model.RequestMessage;
import cn.coder4j.study.example.websocket.model.ResponseMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author buhao
 * @version WSController.java, v 0.1 2019-10-21 17:22 buhao
 */
@Controller
public class WSController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/hello")
    @SendTo("/topic/hello")
    public ResponseMessage hello(RequestMessage requestMessage) {
        System.out.println("接收消息:" + requestMessage);
        return new ResponseMessage("服務端接收到你發(fā)的:" + requestMessage);
    }

    @GetMapping("/sendMsgByUser")
    public @ResponseBody
    Object sendMsgByUser(String token, String msg) {
        simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg);
        return "success";
    }

    @GetMapping("/sendMsgByAll")
    public @ResponseBody
    Object sendMsgByAll(String msg) {
        simpMessagingTemplate.convertAndSend("/topic", msg);
        return "success";
    }

    @GetMapping("/test")
    public String test() {
        return "test-stomp.html";
    }
}

說明
  1. 通過 @MessageMapping 來暴露節(jié)點路徑芭概,有點類似 @RequestMapping赛不。注意這里雖然寫的是 hello ,但是我們客戶端調用的真正地址是** /app/hello罢洲。 因為我們在上面的 config 里配置了registry.setApplicationDestinationPrefixes("/app")**踢故。
  2. @SendTo這個注解會把返回值的內容發(fā)送給訂閱了 /topic/hello 的客戶端,與之類似的還有一個@SendToUser 只不過他是發(fā)送給用戶端一對一通信的惹苗。這兩個注解一般是應答時響應的殿较,如果服務端主動發(fā)送消息可以通過 simpMessagingTemplate類的convertAndSend方法。注意 simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg) 鸽粉,聯系到我們上文配置的registry.setUserDestinationPrefix("/user/"),這里客戶端訂閱的是/user/{token}/msg,千萬不要搞錯斜脂。

Session 共享的問題

上面反復提到一個問題就是,服務端如果要主動發(fā)送消息給客戶端一定要用到 session触机。而大家都知道的是 session 這個東西是不跨 jvm 的帚戳。如果有多臺服務器,在 http 請求的情況下儡首,我們可以通過把 session 放入緩存中間件中來共享解決這個問題片任,通過 spring session 幾條配置就解決了。但是 web socket 不可以蔬胯。他的 session 是不能序列化的对供,當然這樣設計的目的不是為了為難你,而是出于對 http 與 web socket 請求的差異導致的。
目前網上找到的最簡單方案就是通過 redis 訂閱廣播的形式产场,主要代碼跟第二種方式差不多鹅髓,你要在本地放個 map 保存請求的 session。也就是說每臺服務器都會保存與他連接的 session 于本地京景。然后發(fā)消息的地方要修改窿冯,并不是現在這樣直接發(fā)送,而通過 redis 的訂閱機制确徙。服務器要發(fā)消息的時候醒串,你通過 redis 廣播這條消息,所有訂閱的服務端都會收到這個消息鄙皇,然后本地嘗試發(fā)送芜赌。最后肯定只有有這個對應用戶 session 的那臺才能發(fā)送出去。

如何選擇

  1. 如果你在使用 tio伴逸,那推薦使用 tio 的集成缠沈。因為它已經實現了很多功能,包括上面說的通過 redis 的 session 共享违柏,只要加幾個配置就可以了博烂。但是 tio 是半開源,文檔是需要收費的漱竖。如果沒有使用禽篱,那就忘了他。
  2. 如果你的業(yè)務要求比較靈活多變馍惹,推薦使用前兩種躺率,更推薦第二種 Spring 封裝的形式。
  3. 如果只是簡單的服務器雙向通信万矾,推薦 stomp 的形式悼吱,因為他更容易規(guī)范使用。

其它

  1. websocket 在線驗證

寫完服務端代碼后想調試良狈,但是不會前端代碼怎么辦后添,點這里,這是一個在線的 websocket 客戶端薪丁,功能完全夠我們調試了遇西。

  1. stomp 驗證

這個沒找到在線版的,但是網上有很多 demo 可以下載到本地進行調試严嗜,也可以通過后文的連接找到粱檀。

  1. 另外由于篇幅有限,并不能放上所有代碼漫玄,但是測試代碼全都上傳 gitlab茄蚯,保證可以正常運行,可以在 這里 找到

參考鏈接#

  1. SpringBoot 系統 - 集成 WebSocket 實時通信
  2. WebSocket 的故事(二)—— Spring 中如何利用 STOMP 快速構建 WebSocket 廣播式消息模式
  3. SpringBoot集成WebSocket【基于純H5】進行點對點[一對一]和廣播[一對多]實時推送
  4. Spring Framework 參考文檔(WebSocket STOMP)
  5. Spring Boot中使用WebSocket總結(一):幾種實現方式詳解
  6. Spring Boot 系列 - WebSocket 簡單使用
  7. tio-websocket-spring-boot-starter

轉載:https://www.cnblogs.com/kiwifly/p/11729304.html

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市渗常,隨后出現的幾起案子壮不,更是在濱河造成了極大的恐慌,老刑警劉巖凳谦,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忆畅,死亡現場離奇詭異衡未,居然都是意外死亡尸执,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門缓醋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來如失,“玉大人,你說我怎么就攤上這事送粱⊥使螅” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵抗俄,是天一觀的道長脆丁。 經常有香客問我,道長动雹,這世上最難降的妖魔是什么槽卫? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮胰蝠,結果婚禮上歼培,老公的妹妹穿的比我還像新娘。我一直安慰自己茸塞,他們只是感情好躲庄,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著钾虐,像睡著了一般噪窘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上效扫,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天倔监,我揣著相機與錄音,去河邊找鬼荡短。 笑死丐枉,一個胖子當著我的面吹牛,可吹牛的內容都是我干的掘托。 我是一名探鬼主播瘦锹,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弯院?” 一聲冷哼從身側響起辱士,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎听绳,沒想到半個月后颂碘,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡椅挣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年头岔,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鼠证。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡峡竣,死狀恐怖,靈堂內的尸體忽然破棺而出量九,到底是詐尸還是另有隱情适掰,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布荠列,位于F島的核電站类浪,受9級特大地震影響,放射性物質發(fā)生泄漏肌似。R本人自食惡果不足惜费就,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锈嫩。 院中可真熱鬧受楼,春花似錦、人聲如沸呼寸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽对雪。三九已至河狐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瑟捣,已是汗流浹背馋艺。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迈套,地道東北人捐祠。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像桑李,于是被迫代替她去往敵國和親踱蛀。 傳聞我的和親對象是個殘疾皇子窿给,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內容