Spring Boot WebSocket入門

一寡具、概述

相比 HTTP 協(xié)議來說,WebSocket 協(xié)議對大多數(shù)后端開發(fā)者是比較陌生的璧亚。相比來說版姑,WebSocket 協(xié)議重點是提供了服務(wù)端主動向客戶端發(fā)送數(shù)據(jù)的能力柱搜,這樣我們就可以完成實時性較高的需求。例如說剥险,聊天 IM 即使通訊功能聪蘸、消息訂閱服務(wù)、網(wǎng)頁游戲等等表制。

同時健爬,因為 WebSocket 使用 TCP 通信,可以避免重復(fù)創(chuàng)建連接么介,提升通信質(zhì)量和效率娜遵。

二、方案

本文采用Tomcat WebSocket方案實現(xiàn)WebSocket壤短。

三设拟、Tomcat WebSocket 快速入門

使用 Tomcat WebSocket 搭建一個 WebSocket 的示例。提供如下消息的功能支持:

  1. 身份認(rèn)證請求
  2. 私聊消息
  3. 群聊消息

考慮到讓示例更加易懂久脯,我們先做成全局有且僅有一個大的聊天室纳胧,即建立上 WebSocket 的連接,都自動動進(jìn)入該聊天室的功能帘撰。

3.1 引入依賴

在 [pom.xml] 文件中躲雅,引入相關(guān)依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>tomcat-websocket</artifactId>

    <dependencies>
        <!-- 實現(xiàn)對 WebSocket 相關(guān)依賴的引入骡和,方便~ -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!-- 引入 Fastjson 相赁,實現(xiàn)對 JSON 的序列化,因為后續(xù)我們會使用它解析消息 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.67</version>
        </dependency>

    </dependencies>

</project>

3.2 WebsocketServerEndpoint

創(chuàng)建 [WebsocketServerEndpoint] 類慰于,定義 Websocket 服務(wù)的端點(EndPoint)钮科。代碼如下:

// WebsocketServerEndpoint.java

@Controller
@ServerEndpoint("/")
public class WebsocketServerEndpoint {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        logger.info("[onOpen][session({}) 接入]", session);
    }

    @OnMessage
    public void onMessage(Session session, String message) {
        logger.info("[onOpen][session({}) 接收到一條消息({})]", session, message); // 生產(chǎn)環(huán)境下,請設(shè)置成 debug 級別
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        logger.info("[onClose][session({}) 連接關(guān)閉婆赠。關(guān)閉原因是({})}]", session, closeReason);
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        logger.info("[onClose][session({}) 發(fā)生異常]", session, throwable);
    }

}
  • 在類上绵脯,添加 @Controller 注解,保證創(chuàng)建一個 WebsocketServerEndpoint Bean 休里。
  • 在類上蛆挫,添加 JSR-356 定義的 @ServerEndpoint 注解,標(biāo)記這是一個 WebSocket EndPoint 妙黍,路徑為 / 悴侵。
  • WebSocket 一共有四個事件,分別對應(yīng)使用 JSR-356 定義的 @OnOpen拭嫁、@OnMessage可免、@OnClose抓于、@OnError 注解。

這是最簡版的 WebsocketServerEndpoint 的代碼浇借,只是實現(xiàn)了4個事件的日志輸出捉撮。在下文,我們會慢慢把代碼補(bǔ)全妇垢。

3.3 WebSocketConfiguration

創(chuàng)建 [WebsocketServerEndpoint]配置類巾遭。代碼如下:

@Configuration
// @EnableWebSocket // 無需添加該注解,因為我們并不是使用 Spring WebSocket
public class WebSocketConfiguration {

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

}

#serverEndpointExporter()方法中闯估,創(chuàng)建ServerEndpointExporter的Bean 灼舍。該 Bean 的作用,是掃描添加有@ServerEndpoint注解的 Bean 睬愤。

3.4 Application

創(chuàng)建 [Application.java]類,配置 @SpringBootApplication 注解即可纹安。代碼如下:

// Application.java

@SpringBootApplication
public class Application {

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

}

執(zhí)行 Application 啟動該示例項目尤辱。

考慮到可能不會或者不愿意寫前端代碼,所以我們直接使用 WEBSOCKET 在線測試工具 厢岂。測試 WebSocket 連接光督,如下圖:

WebSocket在線測試客戶端

至此,最簡單的一個 WebSocket 項目的骨架塔粒,我們已經(jīng)搭建完成结借。下面,我們開始改造卒茬,把相應(yīng)的邏輯補(bǔ)全船老。

3.5 消息

在 HTTP 協(xié)議中,是基于 Request/Response 請求響應(yīng)的同步模型圃酵,進(jìn)行交互柳畔。在 Websocket 協(xié)議中,是基于 Message 消息的異步模型郭赐,進(jìn)行交互薪韩。這一點,是很大的不同的捌锭,等會看到具體的消息類俘陷,感受會更明顯。

因為 WebSocket 協(xié)議观谦,不像 HTTP 協(xié)議有 URI 可以區(qū)分不同的 API 請求操作拉盾,所以我們需要在 WebSocket 的 Message 里,增加能夠標(biāo)識消息類型豁状,這里我們采用 type 字段盾剩。所以在這個示例中雷激,我們采用的 Message 采用 JSON 格式編碼,格式如下:

{
 type: "", // 消息類型
 body: {} // 消息體
}
  • type 字段告私,消息類型屎暇。通過該字段,我們知道使用哪個 MessageHandler 消息處理器驻粟。關(guān)于 MessageHandler 根悼,我們在下面段落詳細(xì)解析。
  • body 字段蜀撑,消息體挤巡。不同的消息類型,會有不同的消息體酷麦。
  • Message 采用 JSON 格式編碼矿卑,主要考慮便捷性,胖友實際項目下沃饶,也可以考慮 Protobuf 等更加高效且節(jié)省流量的編碼格式母廷。

3.5.1 Message

基礎(chǔ)消息體,所有消息體都要實現(xiàn)該接口糊肤。代碼如下:

// Message.java

public interface Message {
}

目前作為一個標(biāo)記接口琴昆,未定義任何操作。

3.5.2 認(rèn)證相關(guān) Message

創(chuàng)建 [AuthRequest]類馆揉,用戶認(rèn)證請求业舍。代碼如下:

// AuthRequest.java

public class AuthRequest implements Message {

    public static final String TYPE = "AUTH_REQUEST";

    /**
     * 認(rèn)證 Token
     */
    private String accessToken;
    
    // ... 省略 set/get 方法
    
}

TYPE 靜態(tài)屬性,消息類型為 AUTH_REQUEST 升酣。
accessToken 屬性舷暮,認(rèn)證 Token 。在 WebSocket 協(xié)議中噩茄,我們也需要認(rèn)證當(dāng)前連接脚牍,用戶身份是什么。一般情況下巢墅,我們采用用戶調(diào)用 HTTP 登錄接口诸狭,登錄成功后返回的訪問令牌 accessToken 。
雖然說君纫,WebSocket 協(xié)議是基于 Message 模型驯遇,進(jìn)行交互。但是蓄髓,這并不意味著它的操作叉庐,不需要響應(yīng)結(jié)果。例如說会喝,用戶認(rèn)證請求陡叠,是需要用戶認(rèn)證響應(yīng)的玩郊。所以,我們創(chuàng)建 [AuthResponse]\類枉阵,作為用戶認(rèn)證響應(yīng)译红。代碼如下:

// AuthResponse.java

public class AuthResponse implements Message {

    public static final String TYPE = "AUTH_RESPONSE";

    /**
     * 響應(yīng)狀態(tài)碼
     */
    private Integer code;
    /**
     * 響應(yīng)提示
     */
    private String message;
    
    // ... 省略 set/get 方法
    
}

TYPE 靜態(tài)屬性,消息類型為 AUTH_REQUEST 兴溜。實際上侦厚,我們在每個 Message 實現(xiàn)類上,都增加了 TYPE 靜態(tài)屬性拙徽,作為消息類型刨沦。下面,我們就不重復(fù)贅述了膘怕。
code 屬性想诅,響應(yīng)狀態(tài)碼。
message 屬性岛心,響應(yīng)提示来破。

在本示例中,用戶成功認(rèn)證之后鹉梨,會廣播用戶加入群聊的通知 Message 讳癌,使用 [UserJoinNoticeRequest] 穿稳。代碼如下:

// UserJoinNoticeRequest.java

public class UserJoinNoticeRequest implements Message {

    public static final String TYPE = "USER_JOIN_NOTICE_REQUEST";

    /**
     * 昵稱
     */
    private String nickname;
    
    // ... 省略 set/get 方法

}

3.5.3 發(fā)送消息相關(guān) Message

創(chuàng)建 [SendToOneRequest] 類存皂,發(fā)送給指定人的私聊消息的 Message。代碼如下:

// SendToOneRequest.java

public class SendToOneRequest implements Message {

    public static final String TYPE = "SEND_TO_ONE_REQUEST";

    /**
     * 發(fā)送給的用戶
     */
    private String toUser;
    /**
     * 消息編號
     */
    private String msgId;
    /**
     * 內(nèi)容
     */
    private String content;
    
    // ... 省略 set/get 方法
    
}

創(chuàng)建 [SendToAllRequest]類逢艘,發(fā)送給所有人的群聊消息的 Message旦袋。代碼如下:

// SendToAllRequest.java

public class SendToAllRequest implements Message {

    public static final String TYPE = "SEND_TO_ALL_REQUEST";

    /**
     * 消息編號
     */
    private String msgId;
    /**
     * 內(nèi)容
     */
    private String content;
    
    // ... 省略 set/get 方法
     
}

在服務(wù)端接收到發(fā)送消息的請求,需要異步響應(yīng)發(fā)送是否成功它改。所以疤孕,創(chuàng)建 [SendResponse]類,發(fā)送消息響應(yīng)結(jié)果的 Message 央拖。代碼如下:

// SendResponse.java

public class SendResponse implements Message {

    public static final String TYPE = "SEND_RESPONSE";

    /**
     * 消息編號
     */
    private String msgId;
    /**
     * 響應(yīng)狀態(tài)碼
     */
    private Integer code;
    /**
     * 響應(yīng)提示
     */
    private String message;
    
    // ... 省略 set/get 方法
    
}
  • 重點看 msgId 字段祭阀,消息編號∠式洌客戶端在發(fā)送消息专控,通過使用 UUID 算法,生成全局唯一消息編號遏餐。這樣伦腐,服務(wù)端通過 SendResponse 消息響應(yīng),通過 msgId 做映射失都。

在服務(wù)端接收到發(fā)送消息的請求柏蘑,需要轉(zhuǎn)發(fā)消息給對應(yīng)的人幸冻。所以,創(chuàng)建 [SendToUserRequest]類咳焚,發(fā)送消息給一個用戶的 Message 洽损。代碼如下:

// SendResponse.java

public class SendToUserRequest implements Message {

    public static final String TYPE = "SEND_TO_USER_REQUEST";

    /**
     * 消息編號
     */
    private String msgId;
    /**
     * 內(nèi)容
     */
    private String content;
    
    // ... 省略 set/get 方法
     
}
  • 相比 SendToOneRequest 來說,少一個 toUser 字段黔攒。因為趁啸,我們可以通過 WebSocket 連接,已經(jīng)知道發(fā)送給誰了督惰。

3..6 消息處理器

每個客戶端發(fā)起的 Message 消息類型不傅,我們會聲明對應(yīng)的 MessageHandler 消息處理器。這個就類似在 SpringMVC 中赏胚,每個 API 接口對應(yīng)一個 Controller 的 Method 方法访娶。

3.6.1 MessageHandler

創(chuàng)建 [MessageHandler] 接口,消息處理器接口觉阅。代碼如下:

// MessageHandler.java

public interface MessageHandler<T extends Message> {

    /**
     * 執(zhí)行處理消息
     *
     * @param session 會話
     * @param message 消息
     */
    void execute(Session session, T message);

    /**
     * @return 消息類型崖疤,即每個 Message 實現(xiàn)類上的 TYPE 靜態(tài)字段
     */
    String getType();

}
  • 定義了泛型 <T> ,需要是 Message 的實現(xiàn)類典勇。
  • 定義的兩個接口方法劫哼。

3.6.2 AuthMessageHandler

創(chuàng)建 [AuthMessageHandler]類,處理 AuthRequest 消息割笙。代碼如下:

// AuthMessageHandler.java

@Component
public class AuthMessageHandler implements MessageHandler<AuthRequest> {

 @Override
 public void execute(Session session, AuthRequest message) {
 // 如果未傳遞 accessToken 
 if (StringUtils.isEmpty(message.getAccessToken())) {
 WebSocketUtil.send(session, AuthResponse.TYPE,
 new AuthResponse().setCode(1).setMessage("認(rèn)證 accessToken 未傳入"));
 return;
 }

 // 添加到 WebSocketUtil 中
 WebSocketUtil.addSession(session, message.getAccessToken()); // 考慮到代碼簡化权烧,我們先直接使用 accessToken 作為 User

 // 判斷是否認(rèn)證成功。這里伤溉,假裝直接成功
 WebSocketUtil.send(session, AuthResponse.TYPE, new AuthResponse().setCode(0));

 // 通知所有人般码,某個人加入了。這個是可選邏輯乱顾,僅僅是為了演示
 WebSocketUtil.broadcast(UserJoinNoticeRequest.TYPE,
 new UserJoinNoticeRequest().setNickname(message.getAccessToken())); // 考慮到代碼簡化板祝,我們先直接使用 accessToken 作為 User
 }

 @Override
 public String getType() {
 return AuthRequest.TYPE;
 }

}
  • 關(guān)于 WebSocketUtil 類,我們在 [3.7 WebSocketUtil]中詳解走净。

3.6.3 SendToOneRequest

創(chuàng)建 [SendToOneHandler]類券时,處理 SendToOneRequest 消息。代碼如下:

// SendToOneRequest.java

@Component
public class SendToOneHandler implements MessageHandler<SendToOneRequest> {

    @Override
    public void execute(Session session, SendToOneRequest message) {
        // 這里伏伯,假裝直接成功
        SendResponse sendResponse = new SendResponse().setMsgId(message.getMsgId()).setCode(0);
        WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);

        // 創(chuàng)建轉(zhuǎn)發(fā)的消息
        SendToUserRequest sendToUserRequest = new SendToUserRequest().setMsgId(message.getMsgId())
                .setContent(message.getContent());
        // 廣播發(fā)送
        WebSocketUtil.send(message.getToUser(), SendToUserRequest.TYPE, sendToUserRequest);
    }

    @Override
    public String getType() {
        return SendToOneRequest.TYPE;
    }

}

3.6.4 SendToAllHandler

創(chuàng)建 [SendToAllHandler]類橘洞,處理 SendToAllRequest 消息。代碼如下:

// SendToAllRequest.java

@Component
public class SendToAllHandler implements MessageHandler<SendToAllRequest> {

    @Override
    public void execute(Session session, SendToAllRequest message) {
        // 這里舵鳞,假裝直接成功
        SendResponse sendResponse = new SendResponse().setMsgId(message.getMsgId()).setCode(0);
        WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);

        // 創(chuàng)建轉(zhuǎn)發(fā)的消息
        SendToUserRequest sendToUserRequest = new SendToUserRequest().setMsgId(message.getMsgId())
                .setContent(message.getContent());
        // 廣播發(fā)送
        WebSocketUtil.broadcast(SendToUserRequest.TYPE, sendToUserRequest);
    }

    @Override
    public String getType() {
        return SendToAllRequest.TYPE;
    }

}

3.7 WebSocketUtil

主要提供兩方面的功能:

  1. Session 會話的管理震檩;
  2. 多種發(fā)送消息的方式。
    代碼如下:
// WebSocketUtil.java

public class WebSocketUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);

    // ========== 會話相關(guān) ==========

    /**
     * Session 與用戶的映射
     */
    private static final Map<Session, String> SESSION_USER_MAP = new ConcurrentHashMap<>();
    /**
     * 用戶與 Session 的映射
     */
    private static final Map<String, Session> USER_SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 添加 Session 。在這個方法中抛虏,會添加用戶和 Session 之間的映射
     *
     * @param session Session
     * @param user 用戶
     */
    public static void addSession(Session session, String user) {
        // 更新 USER_SESSION_MAP
        USER_SESSION_MAP.put(user, session);
        // 更新 SESSION_USER_MAP
        SESSION_USER_MAP.put(session, user);
    }

    /**
     * 移除 Session 博其。
     *
     * @param session Session
     */
    public static void removeSession(Session session) {
        // 從 SESSION_USER_MAP 中移除
        String user = SESSION_USER_MAP.remove(session);
        // 從 USER_SESSION_MAP 中移除
        if (user != null && user.length() > 0) {
            USER_SESSION_MAP.remove(user);
        }
    }

    // ========== 消息相關(guān) ==========

    /**
     * 廣播發(fā)送消息給所有在線用戶
     *
     * @param type 消息類型
     * @param message 消息體
     * @param <T> 消息類型
     */
    public static <T extends Message> void broadcast(String type, T message) {
        // 創(chuàng)建消息
        String messageText = buildTextMessage(type, message);
        // 遍歷 SESSION_USER_MAP ,進(jìn)行逐個發(fā)送
        for (Session session : SESSION_USER_MAP.keySet()) {
            sendTextMessage(session, messageText);
        }
    }

    /**
     * 發(fā)送消息給單個用戶的 Session
     *
     * @param session Session
     * @param type 消息類型
     * @param message 消息體
     * @param <T> 消息類型
     */
    public static <T extends Message> void send(Session session, String type, T message) {
        // 創(chuàng)建消息
        String messageText = buildTextMessage(type, message);
        // 遍歷給單個 Session 迂猴,進(jìn)行逐個發(fā)送
        sendTextMessage(session, messageText);
    }

    /**
     * 發(fā)送消息給指定用戶
     *
     * @param user 指定用戶
     * @param type 消息類型
     * @param message 消息體
     * @param <T> 消息類型
     * @return 發(fā)送是否成功你那個
     */
    public static <T extends Message> boolean send(String user, String type, T message) {
        // 獲得用戶對應(yīng)的 Session
        Session session = USER_SESSION_MAP.get(user);
        if (session == null) {
            LOGGER.error("[send][user({}) 不存在對應(yīng)的 session]", user);
            return false;
        }
        // 發(fā)送消息
        send(session, type, message);
        return true;
    }

    /**
     * 構(gòu)建完整的消息
     *
     * @param type 消息類型
     * @param message 消息體
     * @param <T> 消息類型
     * @return 消息
     */
    private static <T extends Message> String buildTextMessage(String type, T message) {
        JSONObject messageObject = new JSONObject();
        messageObject.put("type", type);
        messageObject.put("body", message);
        return messageObject.toString();
    }

    /**
     * 真正發(fā)送消息
     *
     * @param session Session
     * @param messageText 消息
     */
    private static void sendTextMessage(Session session, String messageText) {
        if (session == null) {
            LOGGER.error("[sendTextMessage][session 為 null]");
            return;
        }
        RemoteEndpoint.Basic basic = session.getBasicRemote();
        if (basic == null) {
            LOGGER.error("[sendTextMessage][session 的  為 null]");
            return;
        }
        try {
            basic.sendText(messageText);
        } catch (IOException e) {
            LOGGER.error("[sendTextMessage][session({}) 發(fā)送消息{}) 發(fā)生異常",
                    session, messageText, e);
        }
    }

}

3.8 完善 WebsocketServerEndpoint

修改 [WebsocketServerEndpoint]的代碼慕淡,完善其功能。

3.8.1 初始化 MessageHandler 集合

實現(xiàn) [InitializingBean]接口沸毁,在 #afterPropertiesSet() 方法中峰髓,掃描所有 MessageHandler Bean ,添加到 MessageHandler 集合中息尺。代碼如下:

// WebsocketServerEndpoint.java

/**
 * 消息類型與 MessageHandler 的映射
 *
 * 注意携兵,這里設(shè)置成靜態(tài)變量。雖然說 WebsocketServerEndpoint 是單例搂誉,但是 Spring Boot 還是會為每個 WebSocket 創(chuàng)建一個 WebsocketServerEndpoint Bean 徐紧。
 */
private static final Map<String, MessageHandler> HANDLERS = new HashMap<>();

@Autowired
private ApplicationContext applicationContext;

@Override
public void afterPropertiesSet() throws Exception {
    // 通過 ApplicationContext 獲得所有 MessageHandler Bean
    applicationContext.getBeansOfType(MessageHandler.class).values() // 獲得所有 MessageHandler Bean
            .forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler)); // 添加到 handlers 中
    logger.info("[afterPropertiesSet][消息處理器數(shù)量:{}]", HANDLERS.size());
}

通過這樣的方式,可以避免手動配置 MessageHandler 與消息類型的映射炭懊。

3.8.2 onOpen

重新實現(xiàn)#onOpen(Session session, EndpointConfig config)方法并级,實現(xiàn)連接時,使用accessToken參數(shù)進(jìn)行用戶認(rèn)證侮腹。代碼如下:

// WebsocketServerEndpoint.java

@OnOpen
public void onOpen(Session session, EndpointConfig config) {
    logger.info("[onOpen][session({}) 接入]", session);
    // <1> 解析 accessToken
    List<String> accessTokenValues = session.getRequestParameterMap().get("accessToken");
    String accessToken = !CollectionUtils.isEmpty(accessTokenValues) ? accessTokenValues.get(0) : null;
    // <2> 創(chuàng)建 AuthRequest 消息類型
    AuthRequest authRequest = new AuthRequest().setAccessToken(accessToken);
    // <3> 獲得消息處理器
    MessageHandler<AuthRequest> messageHandler = HANDLERS.get(AuthRequest.TYPE);
    if (messageHandler == null) {
        logger.error("[onOpen][認(rèn)證消息類型嘲碧,不存在消息處理器]");
        return;
    }
    messageHandler.execute(session, authRequest);
}
  • <1> 處,解析 ws:// 地址上的 accessToken 的請求參父阻。例如說:ws://127.0.0.1:8080?accessToken=guo 愈涩。
  • <2> 處,創(chuàng)建 AuthRequest 消息類型至非,并設(shè)置 accessToken 屬性钠署。
  • <3> 處糠聪,獲得 AuthRequest 消息類型對應(yīng)的 MessageHandler 消息處理器荒椭,然后調(diào)用 MessageHandler#execute(session, message) 方法,執(zhí)行處理用戶認(rèn)證請求舰蟆。

打開三個瀏覽器創(chuàng)建趣惠,分別設(shè)置服務(wù)地址如下:
ws://127.0.0.1:8080/?accessToken=guo
ws://127.0.0.1:8080/?accessToken=xiu
ws://127.0.0.1:8080/?accessToken=zhi
然后,逐個點擊「開啟連接」按鈕身害,進(jìn)行 WebSocket 連接味悄。最終效果如下圖:

3個在線測試地址

可以看到AuthResponse的消息和UserJoinNoticeRequest的消息:
收到的消息

3.8.3 onMessage

重新實現(xiàn) #onMessage(Session session, String message) 方法,實現(xiàn)不同的消息塌鸯,轉(zhuǎn)發(fā)給不同的 MessageHandler 消息處理器侍瑟。代碼如下:

// WebsocketServerEndpoint.java

@OnMessage
public void onMessage(Session session, String message) {
    logger.info("[onOpen][session({}) 接收到一條消息({})]", session, message); // 生產(chǎn)環(huán)境下,請設(shè)置成 debug 級別
    try {
        // <1> 獲得消息類型
        JSONObject jsonMessage = JSON.parseObject(message);
        String messageType = jsonMessage.getString("type");
        // <2> 獲得消息處理器
        MessageHandler messageHandler = HANDLERS.get(messageType);
        if (messageHandler == null) {
            logger.error("[onMessage][消息類型({}) 不存在消息處理器]", messageType);
            return;
        }
        // <3> 解析消息
        Class<? extends Message> messageClass = this.getMessageClass(messageHandler);
        // <4> 處理消息
        Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
        messageHandler.execute(session, messageObj);
    } catch (Throwable throwable) {
        logger.info("[onMessage][session({}) message({}) 發(fā)生異常]", session, throwable);
    }
}
  • <1> 處,獲得消息類型涨颜,從 "type" 字段中费韭。
  • <2> 處,獲得消息類型對應(yīng)的 MessageHandler 消息處理器庭瑰。
  • <3> 處星持,調(diào)用 #getMessageClass(MessageHandler handler) 方法,通過 MessageHandler 中弹灭,通過解析其類上的泛型督暂,獲得消息類型對應(yīng)的 Class 類。代碼如下:
// WebsocketServerEndpoint.java

private Class<? extends Message> getMessageClass(MessageHandler handler) {
    // 獲得 Bean 對應(yīng)的 Class 類名穷吮。因為有可能被 AOP 代理過逻翁。
    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(handler);
    // 獲得接口的 Type 數(shù)組
    Type[] interfaces = targetClass.getGenericInterfaces();
    Class<?> superclass = targetClass.getSuperclass();
    while ((Objects.isNull(interfaces) || 0 == interfaces.length) && Objects.nonNull(superclass)) { // 此處,是以父類的接口為準(zhǔn)
        interfaces = superclass.getGenericInterfaces();
        superclass = targetClass.getSuperclass();
    }
    if (Objects.nonNull(interfaces)) {
        // 遍歷 interfaces 數(shù)組
        for (Type type : interfaces) {
            // 要求 type 是泛型參數(shù)
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                // 要求是 MessageHandler 接口
                if (Objects.equals(parameterizedType.getRawType(), MessageHandler.class)) {
                    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                    // 取首個元素
                    if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) {
                        return (Class<Message>) actualTypeArguments[0];
                    } else {
                        throw new IllegalStateException(String.format("類型(%s) 獲得不到消息類型", handler));
                    }
                }
            }
        }
    }
    throw new IllegalStateException(String.format("類型(%s) 獲得不到消息類型", handler));
}
  • <4> 處捡鱼,調(diào)用 MessageHandler#execute(session, message) 方法卢未,執(zhí)行處理請求。

另外堰汉,這里增加了 try-catch 代碼辽社,避免整個執(zhí)行的過程中,發(fā)生異常翘鸭。如果在 onMessage 事件的處理中滴铅,發(fā)生異常,該消息對應(yīng)的 Session 會話會被自動關(guān)閉就乓。顯然汉匙,這個不符合我們的要求。例如說生蚁,在 MessageHandler 處理消息的過程中噩翠,發(fā)生一些異常是無法避免的。

繼續(xù)基于上述創(chuàng)建的三個瀏覽器邦投,我們先點擊「清空消息」按鈕伤锚,清空下消息,打掃下上次測試展示出來的接收得到的 Message 志衣。當(dāng)然屯援,WebSocket 的連接,不需要去斷開念脯。

在第一個瀏覽器中狞洋,分別發(fā)送兩種聊天消息:

  • 一條SendToOneRequest私聊消息:
{
    type: "SEND_TO_ONE_REQUEST",
    body: {
        toUser: "xiu",
        msgId: "eaafew3c-35dd-46ee-b548-f9crdk6396fe",
        content: "一條單聊消息"
    }
}
單聊消息
  • 一條SendToAllHandler群聊消息:
{
    type: "SEND_TO_ALL_REQUEST",
    body: {
        msgId: "838e97e1-6ae9-40f9-99c3-f7127ed68888",
        content: "一條群聊消息"
    }
}
群聊消息

3.8.4 .onClose

// WebsocketServerEndpoint.java

@OnClose
public void onClose(Session session, CloseReason closeReason) {
    logger.info("[onClose][session({}) 連接關(guān)閉。關(guān)閉原因是({})}]", session, closeReason);
    WebSocketUtil.removeSession(session);
}

3.8.5 onError

// WebsocketServerEndpoint.java

@OnError
public void onError(Session session, Throwable throwable) {
    logger.info("[onClose][session({}) 發(fā)生異常]", session, throwable);
}

底線


本文源代碼使用 Apache License 2.0開源許可協(xié)議绿店,可從Gitee代碼地址通過git clone命令下載到本地或者通過瀏覽器方式查看源代碼吉懊。

其他優(yōu)秀文章:https://blog.csdn.net/moshowgame/article/details/80275084http://www.reibang.com/p/1501f1350c99

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市借嗽,隨后出現(xiàn)的幾起案子怕午,更是在濱河造成了極大的恐慌,老刑警劉巖淹魄,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件郁惜,死亡現(xiàn)場離奇詭異,居然都是意外死亡甲锡,警方通過查閱死者的電腦和手機(jī)兆蕉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缤沦,“玉大人虎韵,你說我怎么就攤上這事「追希” “怎么了包蓝?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長企量。 經(jīng)常有香客問我测萎,道長,這世上最難降的妖魔是什么届巩? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任硅瞧,我火速辦了婚禮,結(jié)果婚禮上恕汇,老公的妹妹穿的比我還像新娘腕唧。我一直安慰自己,他們只是感情好瘾英,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布枣接。 她就那樣靜靜地躺著,像睡著了一般缺谴。 火紅的嫁衣襯著肌膚如雪但惶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天瓣赂,我揣著相機(jī)與錄音榆骚,去河邊找鬼片拍。 笑死煌集,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捌省。 我是一名探鬼主播苫纤,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卷拘?” 一聲冷哼從身側(cè)響起喊废,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栗弟,沒想到半個月后污筷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡乍赫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年瓣蛀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雷厂。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡惋增,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出改鲫,到底是詐尸還是另有隱情诈皿,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響持搜,放射性物質(zhì)發(fā)生泄漏侯勉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一陨帆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦怎披、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至群井,卻和暖如春状飞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背书斜。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工诬辈, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荐吉。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓焙糟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親样屠。 傳聞我的和親對象是個殘疾皇子穿撮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354