java websocket教程

定義

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ù)器是有要求的.以下是被支持的版本

image.png

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)

  • 廣播思路

    1. 瀏覽器訂閱主題: /topic
    2. 服務(wù)器發(fā)送消息到主題/topic
    3. 所有訂閱的瀏覽器都能收到消息
  • 點(diǎn)對點(diǎn)的思路(瀏覽器A->B)

    1. 瀏覽器B訂閱主題: /user/B/topic
    2. 瀏覽器A發(fā)送消息到主題: /user/B/topic
    3. 瀏覽器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
  • 最后顯示如下
image.png

思考題

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

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容