1. 程式人生 > 其它 >SpringBoot整合WebSocket實現線上聊天室

SpringBoot整合WebSocket實現線上聊天室

前言

WebSocket也是一種應用層協議,也是建立在TCP協議之上,類似HTTP,並且相容HTTP。相比HTTP,它可以實現雙向通訊,如聊天室場景,使用HTTP就必須客戶端輪訓查詢伺服器有沒有新的訊息,而使用WebSocket就可以伺服器直接通知客戶端。

Tomcat支援

Tomcat自7.0.5版本開始支援WebSocket,並實現了WebSocket規範(JSR356)。JSR356規定WebSokcet應用由一系列Endpoint組成,類似於Servlet,Tomcat支援兩種定義Endpoint的方式。

註解方式

import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/chat")
public class ChatEndpoint {

  @OnOpen
  public void onOpen(Session session) {
    System.out.println(session.getId());
  }

  @OnClose
  public void onClose(Session session) {
    System.out.println(session.getId());
  }

  @OnError
  public void onError(Session session) {
    System.out.println(session.getId());
  }

  @OnMessage
  public void onMessage(Session session, String msg) throws IOException {
    System.out.println(msg);
    //向客戶端傳送訊息
    session.getBasicRemote().sendText("this is " + session.getId());
  }
}

使用註解@ServerEndpoint宣告Endpoint類,並配置請求路徑。

程式設計方式

import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler.Whole;
import javax.websocket.Session;

public class ChatEndpoint2 extends Endpoint {

  @Override
  public void onOpen(Session session, EndpointConfig config) {
    System.out.println(session.getId());
    //新增訊息處理器
    session.addMessageHandler(new Whole<>() {
      @Override
      public void onMessage(Object message) {
        System.out.println(message);
      }
    });
  }

  @Override
  public void onClose(Session session, CloseReason closeReason) {
    System.out.println(closeReason.getReasonPhrase());
  }

  @Override
  public void onError(Session session, Throwable throwable) {
    System.out.println(throwable.getMessage());
  }
}

定義一個類繼承Endpoint。

import java.util.HashSet;
import java.util.Set;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;

public class MyServerApplicationConfig implements ServerApplicationConfig {

  @Override
  public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> scanned) {
    //根據查詢到的Endpoint實現類建立ServerEndpointConfig
    Set<ServerEndpointConfig> result = new HashSet<>();
    if (scanned.contains(ChatEndpoint2.class)) {
      result.add(ServerEndpointConfig.Builder.create(ChatEndpoint2.class, "/chat").build());
    }
    return result;
  }

  @Override
  public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
    //用來過濾使用註解ServerEndpoint定義的Endpoint
    return scanned;
  }
}

還需要定義一個ServerApplicationConfig的實現類,用來配置Endpoint的請求路徑。

Endpoint生命週期

Endpoint的生命週期方法如下:

  • onOpen:當開啟一個新的會話時呼叫,這是客戶端與伺服器握手成功後呼叫的方法,等同於註解@OnOpen。
  • onClose:會話關閉時呼叫,等同於註解@OnClose。
  • onError:傳輸過程異常時呼叫,等同於註解@OnError。

原理

Tomcat會在啟動時查詢所有使用註解@ServerEndpoint宣告的類和Endpoint的子類,建立WebSocketContainer(類似ServletContext)並將所有Endpoint注入其中。
但如果使用嵌入式的Tomcat(如SpringBoot內嵌的Tomcat)就不會進行此查詢,具體原因可以看Embedded Tomcat does not honor ServletContainerInitializers。根本原因是因為SpringBoot建立Tomcat的Context時沒有新增ContextConfig這個LifecycleListener(不清楚是出於什麼考慮),ContextConfig會使用java的SPI技術查詢所有ServletContainerInitializer的實現類。

SpringBoot支援

新增依賴

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

SpringBoot自動注入了一個TomcatWebSocketServletWebServerCustomizer,向Context中添加了WsContextListener監聽器

它也會建立WebSocketContainer並初始化。

配置處理器

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;

public class ChatHandler2 extends TextWebSocketHandler {

  @Override
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    System.out.println(session.getId());
  }

  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    System.out.println(session.getId());
  }

  @Override
  public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {

  }

  @Override
  protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    System.out.println(message.getPayload());
  }

}

類似於繼承Endpoint的類實現。

新增配置

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;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(new ChatHandler2(), "/chat");
  }

}

一定要新增@EnableWebSocket註解,它會處理所有WebSocketConfigurer的實現類。並且會建立WebSocketHandlerMapping的Bean類,之前我們處理HTTP請求時使用的是RequestMappingHandlerMapping,WebSocket請求是在HTTP請求的基礎上升級的,在握手階段還是HTTP請求,所以還是會交給SpringMVC的DispatcherServlet處理。

實現原理


使用的HandlerAdapter實現為HttpRequestHandlerAdapter,它會持有一個WebSocketHttpRequestHandler物件(其中封裝了HandshakeHandler握手處理器和我們自己定義的ChatHandler2)。實際的握手處理器實現為DefaultHandshakeHandler,核心的處理邏輯就是握手的過程,其中會建立StandardWebSocketHandlerAdapter物件。

可以看到StandardWebSocketHandlerAdapter就是一個Endpoint,最終會將這個Endpoint新增到WebSocketContainer中,後續的WebSocket請求就交給Endpoint來處理了。

聊天室實現效果

專案地址springboot_chatroom,頁面部分參考ChatRoom專案。

參考

WebSocket 教程
學習WebSocket協議—從頂層到底層的實現原理(修訂版)
Embedded Tomcat does not honor ServletContainerInitializers
Tomcat實現Web Socket
websocket之三:Tomcat的WebSocket實現