使用WebSocket構(gòu)建實(shí)時(shí)聊天

HTTP協(xié)議的局限性

HTTP協(xié)議的生命周期是通過(guò)Request和Response來(lái)界定的心傀,而Response是被動(dòng)的(服務(wù)端不能主動(dòng)與客戶(hù)端通信)炬太,收到 一次請(qǐng)求才會(huì)返回一次響應(yīng)。而當(dāng)服務(wù)端需要主動(dòng)和客戶(hù)端進(jìn)行通信隆敢,或者需要建立全雙工通信(保持在一個(gè)連接中)時(shí)发皿,HTTP就力不從心了。
在Websocket出現(xiàn)之前拂蝎,實(shí)現(xiàn)全雙工通信的方式主要是ajax輪詢(xún)和long poll穴墅,這樣是非常消耗性能的。

Websocket

WebSocket是HTML5 新增加的特性之一,目前主流瀏覽器大都提供了對(duì)其的支持玄货。其特點(diǎn)是可以在客戶(hù)端和服務(wù)端之間建立全雙工通信皇钞,一些特殊場(chǎng)景,例如實(shí)時(shí)通信松捉、在線(xiàn)游戲夹界、多人協(xié)作等,WebSocket都可以作為解決方案隘世。
Spring自4.0版本后增加了WebSocket支持可柿,本例就使用Spring WebSocket構(gòu)建一個(gè)簡(jiǎn)單實(shí)時(shí)聊天的應(yīng)用。

服務(wù)端配置

WebSocketHandler

Spring WebSocket提供了一個(gè)WebSocketHandler接口丙者,這個(gè)接口提供了WebSocket連接建立后生命周期的處理方法复斥。

public interface WebSocketHandler {

    /**
     * 成功連接WebSocket后執(zhí)行
     *
     * @param session session
     * @throws Exception Exception
     */
    void afterConnectionEstablished(WebSocketSession session) throws Exception;

    /**
     * 處理收到的WebSocketMessage
     * (參照org.springframework.web.socket.handler.AbstractWebSocketHandler)
     *
     * @param session session
     * @param message message
     * @throws Exception Exception
     */
    void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;

    /**
     * 處理傳輸錯(cuò)誤
     *
     * @param session   session
     * @param exception exception
     * @throws Exception Exception
     */
    void handleTransportError(WebSocketSession session, Throwable exception) throws Exception; 

    /**
     * 在兩端WebSocket connection都關(guān)閉或transport error發(fā)生后執(zhí)行
     *
     * @param session     session
     * @param closeStatus closeStatus
     * @throws Exception Exception
     */
    void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception; 
    
    /**
     * Whether the WebSocketHandler handles partial messages. If this flag is set to
     * {@code true} and the underlying WebSocket server supports partial messages,
     * then a large WebSocket message, or one of an unknown size may be split and
     * maybe received over multiple calls to
     * {@link #handleMessage(WebSocketSession, WebSocketMessage)}. The flag
     * {@link WebSocketMessage#isLast()} indicates if
     * the message is partial and whether it is the last part.
     */
    boolean supportsPartialMessages();
}
WebSocketSession

WebSocketSession不同于HttpSession,每次斷開(kāi)連接(正常斷開(kāi)或發(fā)生異常斷開(kāi))都會(huì)重新起一個(gè)WebSocketSession械媒。
這個(gè)抽象類(lèi)提供了一系列對(duì)WebSocketSession及傳輸消息的處理方法:

    /**
    * WebSocketSession id
     */
    String getId();

    /**
    * 獲取該session屬性的Map
    */
    Map<String, Object> getAttributes();

    /**
    * 發(fā)送WebSocketMessage(TextMessage或BinaryMessage)
     */
    void sendMessage(WebSocketMessage<?> message) throws IOException;

    /**
     * 判斷是否在連接
    */
    boolean isOpen();

    /**
     * 關(guān)閉連接
    */
    void close() throws IOException;
WebSocketMessage<T>

spring WebSocket提供了四種WebSocketMessage的實(shí)現(xiàn):TextMessage(文本類(lèi)消息)永票、BinaryMessage(二進(jìn)制消息)、PingMessage滥沫、PongMessage(后兩者用于心跳檢測(cè)侣集,在一端收到了Ping消息的時(shí)候,該端點(diǎn)必須發(fā)送Pong消息給對(duì)方兰绣,以檢測(cè)該連接是否存在和有效)世分。

    // 通過(guò)getPayload();方法獲取WebSocketMessage的有效信息
    T getPayload();

HandshakeInterceptor

HandshakeInterceptor接口是WebSocket連接握手過(guò)程的攔截器,通過(guò)實(shí)現(xiàn)該接口可以對(duì)握手過(guò)程進(jìn)行管理缀辩。值得注意的是臭埋,beforeHandshake中的attributes與WebSocketSession中通過(guò)getAttributes();返回的Map是同一個(gè)Map,我們可以在其中放入一些用戶(hù)的特定信息臀玄。

public interface HandshakeInterceptor {

    /**
     * 握手前
     */
    boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
            WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception;

    /**
     * 握手后
     */
    void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
            WebSocketHandler wsHandler, Exception exception);

}

WebSocketConfigurer

通過(guò)實(shí)現(xiàn)WebSocketConfigurer接口瓢阴,可以注冊(cè)相應(yīng)的WebSocket處理器、路徑健无、允許域荣恐、SockJs支持。

public interface WebSocketConfigurer {

    /**
     * 注冊(cè)WebSocketHandler
     */
    void registerWebSocketHandlers(WebSocketHandlerRegistry registry);

}

客戶(hù)端配置

核心API

url為指定的WebSocket注冊(cè)路徑累贤,當(dāng)協(xié)議為http時(shí)叠穆,使用ws://,當(dāng)協(xié)議為https臼膏,使用wss://硼被。

    var path = window.location.hostname + ":****/" + window.location.pathname.split("/")[1];
    var websocket = new WebSocket('ws://' + path + '/****Handler');

    // 新建連接
    websocket.onopen = function () {
        // ...
    };

    // 收到消息
    websocket.onmessage = function (event) {
       // ...
    };

    // 傳輸錯(cuò)誤
    websocket.onerror = function () {
       // ...
    };

    // 關(guān)閉
    websocket.onclose = function () {
        // ...
    };

    // onbeforeunload,窗口刷新渗磅、關(guān)閉事件前執(zhí)行
    window.onbeforeunload = function () {
        // ...
    };

    // 發(fā)送消息
    websocket.send();

onmessage的event對(duì)象:


onmessage event 對(duì)象

可以看出嚷硫,應(yīng)使用event.data獲取服務(wù)端發(fā)送的消息检访。

SockJs支持

有的瀏覽器不支持WebSocket,使用SockJs可以模擬WebSocket仔掸。

    if (window.WebSocket) {
        console.log('Support WebSocket.');
        websocket = new WebSocket('ws://' + path + '/****Handler');
    } else {
        console.log('Not Support WebSocket!);
        websocket = new SockJS('http://' + path + '/****Handler')
    }

實(shí)現(xiàn)思路

以下使用WebSocket構(gòu)建一個(gè)實(shí)時(shí)聊天應(yīng)用烛谊。
1.客戶(hù)端與服務(wù)端通信只使用TextMessage(文本類(lèi)消息),客戶(hù)端只能發(fā)送聊天文本嘉汰,服務(wù)端可以單播和廣播消息,包括聊天文本状勤、上線(xiàn)鞋怀、下線(xiàn)、掉線(xiàn)持搜、用戶(hù)列表信息密似、認(rèn)證信息和服務(wù)器時(shí)間。
2.以HttpSession來(lái)唯一區(qū)別用戶(hù)葫盼,而不是WebSocketSession残腌。
3.核心思路是當(dāng)新的WebSocketSession建立時(shí),將其加入一個(gè)集合贫导,當(dāng)該session失效時(shí)(close抛猫、error)將其從集合中刪除,當(dāng)服務(wù)端需要單播或廣播消息時(shí)孩灯,以這個(gè)集合為根據(jù)闺金。

服務(wù)端實(shí)現(xiàn)

工程搭建

新建Spring Boot項(xiàng)目,添加必要依賴(lài)峰档。

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

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

    <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>

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

    <!-- 熱部署工具 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>

創(chuàng)建服務(wù)端響應(yīng)對(duì)象

(其實(shí)在WebSocket中已經(jīng)沒(méi)有了請(qǐng)求败匹、響應(yīng)之分,但習(xí)慣上將客戶(hù)端發(fā)送的消息稱(chēng)為請(qǐng)求讥巡,服務(wù)端發(fā)送的消息稱(chēng)為響應(yīng))

/**
 * 服務(wù)端響應(yīng)
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatResponse {

    // 返回類(lèi)型
    private String type;

    // 來(lái)源用戶(hù)HttpSessionId
    private String httpSessionId;

    // 來(lái)源用戶(hù)host
    private String host;

    // 來(lái)源用戶(hù)昵稱(chēng)
    private String username;

    // 有效信息
    private Object payload;

    public ChatResponse() {
    }

    public ChatResponse(String httpSessionId, String host, String username) {
        this.httpSessionId = httpSessionId;
        this.host = host;
        this.username = username;
    }

    // getter掀亩、setter...
}

響應(yīng)對(duì)象枚舉

/**
 * 服務(wù)端響應(yīng)類(lèi)型枚舉
 */
public enum ResponseTypeEnum {

    ONLINE("online", "上線(xiàn)提示"),
    OFFLINE("offline", "下線(xiàn)提示"),
    AUTHENTICATE("authenticate", "認(rèn)證信息"),
    LIST("list", "用戶(hù)列表"),
    ERROR("error", "連接異常"),
    CHAT("chat", "聊天文本"),
    TIME("time", "服務(wù)器時(shí)間");

    // 響應(yīng)關(guān)鍵字
    private String key;

    // 類(lèi)型說(shuō)明
    private String info;

    ResponseTypeEnum(String key, String info) {
        this.key = key;
        this.info = info;
    }

    public String getKey() {
        return key;
    }

    public String getInfo() {
        return info;
    }
}

從chrome的WS控制臺(tái),我們可以看到發(fā)送的信息


WS console 顯示

WebSocketHandler實(shí)現(xiàn)

/**
 * WebSocket處理器
 * 用于處理WebSocketSession的生命周期欢顷、單播消息槽棍、廣播消息
 */
@Service
@EnableScheduling
public class ChatHandler implements WebSocketHandler {

    // 用于存放所有連接的WebSocketSession
    private static CopyOnWriteArraySet<WebSocketSession> webSocketSessions = new CopyOnWriteArraySet<>();

    // 用戶(hù)存放所有在線(xiàn)用戶(hù)信息
    private static CopyOnWriteArraySet<Map<String, Object>> sessionAttributes = new CopyOnWriteArraySet<>();

    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    private static final Logger log = LoggerFactory.getLogger(ChatHandler.class);

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 成功連接WebSocket后執(zhí)行
     *
     * @param session session
     * @throws Exception Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 成功連接后將該連接加入集合
        webSocketSessions.add(session);
        sessionAttributes.add(session.getAttributes());
        log.info("session {} open, attributes: {}.", session.getId(), session.getAttributes());

        // 單播消息返回給該用戶(hù)認(rèn)證信息,httpSessionId是用戶(hù)認(rèn)證唯一標(biāo)準(zhǔn)
        this.unicast(session, ResponseTypeEnum.AUTHENTICATE.getKey());

        // 廣播通知該用戶(hù)上線(xiàn)
        this.broadcast(session, ResponseTypeEnum.ONLINE.getKey());

        // 廣播刷新在線(xiàn)列表
        this.broadcast(ResponseTypeEnum.LIST.getKey(), sessionAttributes);
    }

    /**
     * 處理收到的WebSocketMessage抬驴,根據(jù)需求只處理TextMessage
     * (參照org.springframework.web.socket.handler.AbstractWebSocketHandler)
     *
     * @param session session
     * @param message message
     * @throws Exception Exception
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        if (message instanceof TextMessage) {
            // 廣播聊天信息
            this.broadcast(session, ResponseTypeEnum.CHAT.getKey(), ((TextMessage) message).getPayload());
        } else if (message instanceof BinaryMessage) {
            // 對(duì)BinaryMessage不作處理
        } else if (message instanceof PongMessage) {
            // 對(duì)PongMessage不作處理
        } else {
            throw new IllegalStateException("Unexpected WebSocket message type: " + message);
        }
    }

    /**
     * 處理WebSocketMessage transport error
     *
     * @param session   session
     * @param exception exception
     * @throws Exception Exception
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 對(duì)于異常連接刹泄,關(guān)閉并從webSocket移除Sessions中
        if (session.isOpen()) {
            session.close();
        }
        webSocketSessions.remove(session);
        sessionAttributes.remove(session.getAttributes());
        log.error("session {} error, errorMessage: {}.", session.getId(), exception.getMessage());

        // 廣播異常掉線(xiàn)信息
        this.broadcast(session, ResponseTypeEnum.ERROR.getKey());

        // 廣播刷新在線(xiàn)列表
        this.broadcast(ResponseTypeEnum.LIST.getKey(), sessionAttributes);
    }

    /**
     * 在兩端WebSocket connection都關(guān)閉或transport error發(fā)生后執(zhí)行
     *
     * @param session     session
     * @param closeStatus closeStatus
     * @throws Exception Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        boolean removeNow = webSocketSessions.remove(session);
        sessionAttributes.remove(session.getAttributes());
        log.info("session {} close, closeStatus: {}.", session.getId(), closeStatus);

        if (removeNow) {
            // 廣播下線(xiàn)信息
            this.broadcast(session, ResponseTypeEnum.OFFLINE.getKey());
        }

        // 廣播刷新在線(xiàn)列表
        this.broadcast(ResponseTypeEnum.LIST.getKey(), sessionAttributes);
    }

    /**
     * Whether the WebSocketHandler handles partial messages. If this flag is set to
     * {@code true} and the underlying WebSocket server supports partial messages,
     * then a large WebSocket message, or one of an unknown size may be split and
     * maybe received over multiple calls to
     * {@link #handleMessage(WebSocketSession, WebSocketMessage)}. The flag
     * {@link WebSocketMessage#isLast()} indicates if
     * the message is partial and whether it is the last part.
     */
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 封裝response并轉(zhuǎn)為json字符串
     *
     * @param session session
     * @param type    type
     * @param payload payload
     * @return json response
     * @throws Exception Exception
     */
    private String getResponse(WebSocketSession session, String type, Object payload) throws Exception {
        ChatResponse chatResponse;

        if (null == session) {
            chatResponse = new ChatResponse();
        } else {
            Map<String, Object> attributes = session.getAttributes();
            String httpSessionId = (String) attributes.get("httpSessionId");
            String host = (String) attributes.get("host");
            String username = (String) attributes.get("username");

            chatResponse = new ChatResponse(httpSessionId, host, username);
        }

        chatResponse.setType(type);
        chatResponse.setPayload(payload);

        // 轉(zhuǎn)為json字符串
        return objectMapper.writeValueAsString(chatResponse);
    }

    /**
     * 向單個(gè)WebSocketSession單播消息
     *
     * @param session session
     * @param type    type
     * @param payload payload
     * @throws Exception Exception
     */
    private void unicast(WebSocketSession session, String type, Object payload) throws Exception {
        String response = this.getResponse(session, type, payload);
        session.sendMessage(new TextMessage(response));
    }

    /**
     * 單播系統(tǒng)消息
     *
     * @param session session
     * @param type    type
     * @throws Exception Exception
     */
    private void unicast(WebSocketSession session, String type) throws Exception {
        this.unicast(session, type, null);
    }

    /**
     * 因某個(gè)WebSocketSession變動(dòng),向所有連接的WebSocketSession廣播消息
     *
     * @param session 變動(dòng)的WebSocketSession
     * @param type    com.njfu.chat.enums.ResponseTypeEnum 消息類(lèi)型
     * @param payload 消息內(nèi)容
     * @throws Exception Exception
     */
    private void broadcast(WebSocketSession session, String type, Object payload) throws Exception {
        String response = this.getResponse(session, type, payload);

        // 廣播消息
        for (WebSocketSession webSocketSession : webSocketSessions) {
            webSocketSession.sendMessage(new TextMessage(response));
        }
    }

    /**
     * 用于多播系統(tǒng)消息
     *
     * @param session session
     * @param type    type
     * @throws Exception Exception
     */
    private void broadcast(WebSocketSession session, String type) throws Exception {
        this.broadcast(session, type, null);
    }

    /**
     * 用于無(wú)差別廣播消息
     *
     * @param type    type
     * @param payload payload
     * @throws Exception Exception
     */
    private void broadcast(String type, Object payload) throws Exception {
        this.broadcast(null, type, payload);
    }

    /**
     * 定時(shí)任務(wù)怎爵,每5分鐘發(fā)送一次服務(wù)器時(shí)間
     * @throws Exception Exception
     */
    @Scheduled(cron = "0 0-59/5 * * * ?")
    private void sendServerTime() throws Exception {
        this.broadcast(ResponseTypeEnum.TIME.getKey(), simpleDateFormat.format(new Date()));
    }
}

HandshakeInterceptor實(shí)現(xiàn)

/**
 * WebSocketHandshake攔截器
 */
@Service
public class ChatHandshakeInterceptor implements HandshakeInterceptor {

    private static final Logger log = LoggerFactory.getLogger(ChatHandshakeInterceptor.class);

    /**
     * 握手前
     * 為連接的WebsocketSession配置屬性
     *
     * @param request    the current request
     * @param response   the current response
     * @param wsHandler  the target WebSocket handler
     * @param attributes attributes from the HTTP handshake to associate with the WebSocket
     *                   session; the provided attributes are copied, the original map is not used.
     * @return whether to proceed with the handshake ({@code true}) or abort ({@code false}) 通過(guò)true/false決定是否連接
     *
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {
        // 獲取HttpSession
        ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
        HttpSession session = servletRequest.getServletRequest().getSession();

        // 在握手前驗(yàn)證是否存在用戶(hù)信息特石,不存在時(shí)拒絕連接
        String username = (String) session.getAttribute("username");

        if (null == username) {
            log.error("Invalid User!");
            return false;
        } else {
            // 將用戶(hù)信息放入WebSocketSession中
            attributes.put("username", username);
            // httpSessionId用于唯一確定連接客戶(hù)端的身份
            attributes.put("httpSessionId", session.getId());
            attributes.put("host", request.getRemoteAddress().getHostString());
            return true;
        }

    }

    /**
     * 握手后
     *
     * @param request   the current request
     * @param response  the current response
     * @param wsHandler the target WebSocket handler
     * @param exception an exception raised during the handshake, or {@code null} if none
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                               Exception exception) {
    }
}

WebSocketConfigurer實(shí)現(xiàn)

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Value("${origin}")
    private String origin;

    @Autowired
    private ChatHandler chatHandler;

    @Autowired
    private ChatHandshakeInterceptor chatHandshakeInterceptor;

    /**
     * 注冊(cè)WebSocket處理器
     * 配置處理器、攔截器鳖链、允許域姆蘸、SockJs支持
     *
     * @param registry registry
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        // 設(shè)置允許域墩莫,當(dāng)請(qǐng)求的RequestHeaders中的Origin不在允許范圍內(nèi),禁止連接
        String[] allowedOrigins = {origin};

        registry.addHandler(chatHandler, "/chatHandler")
                .addInterceptors(chatHandshakeInterceptor)
                .setAllowedOrigins(allowedOrigins);

        // 當(dāng)瀏覽器不支持WebSocket逞敷,使用SockJs支持
        registry.addHandler(chatHandler, "/sockjs-chatHandler")
                .addInterceptors(chatHandshakeInterceptor)
                .setAllowedOrigins(allowedOrigins)
                .withSockJS();
    }

}

通過(guò)setAllowedOrigins(String... origins);方法可以限制訪(fǎng)問(wèn)狂秦,查看WebSocket Request Headers的Origin屬性:


image.png

這種限制與限制跨域是類(lèi)似的,不同的是端口號(hào)不在其限制范圍內(nèi)推捐×盐剩可以通過(guò)setAllowedOrigins("*");的方式設(shè)置允許所有域。

Controller

@Controller
public class ChatController {

    /**
     * index頁(yè)
     *
     * @return page
     */
    @RequestMapping("/")
    public String index() {
        return "chat";
    }

    /**
     * 驗(yàn)證是否存在用戶(hù)信息
     * 根據(jù)HttpSession唯一確定用戶(hù)身份
     *
     * @param session session
     * @return json
     */
    @RequestMapping("/verifyUser")
    public @ResponseBody
    String verifyUser(HttpSession session) {
        return (String) session.getAttribute("username");
    }

    /**
     * 新增用戶(hù)信息
     *
     * @param session  session
     * @param username username
     */
    @RequestMapping("/addUser")
    public @ResponseBody
    void addUser(HttpSession session, String username) {
        session.setAttribute("username", username);
    }
}

客戶(hù)端實(shí)現(xiàn)

html

    <div class="chat-body">
        <div class="chat-area" id="area"></div>
            <div class="chat-bar">
                <div class="chat-bar-head">在線(xiàn)列表</div>
                <div class="chat-bar-list"></div>
            </div>
        <!-- contenteditable and plaintext only -->
        <div class="chat-input" contenteditable="plaintext-only"></div>
        <div class="chat-control">
            <span class="chat-size"></span>
            <button class="btn btn-primary btn-sm" id="view-online">在線(xiàn)列表</button>
            <button class="btn btn-primary btn-sm" id="send">發(fā)送</button>
        </div>
    </div>

js

// 驗(yàn)證session中是否有用戶(hù)信息牛柒,若有堪簿,進(jìn)行WebSocket連接,若無(wú)皮壁,新增用戶(hù)信息

var websocket;

/**
 * 建立WebSocket連接
 */
function getConnect() {
    var path = window.location.hostname + ":7090/" + window.location.pathname.split("/")[1];
    if (window.WebSocket) {
        console.log('Support WebSocket.');
        websocket = new WebSocket('ws://' + path + '/chatHandler');
    } else {
        console.log('Not Support WebSocket! It\'s recommended to use chrome!');
        bootbox.alert({
            title: '提示',
            message: '您的瀏覽器不支持WebSocket椭更,請(qǐng)切換到chrome獲取最佳體驗(yàn)!'
        });
        websocket = new SockJS('http://' + path + '/sockjs-chatHandler')
    }

    // 配置WebSocket連接生命周期
    websocket.onopen = function () {
        console.log('WebSocket open!');
    };

    websocket.onmessage = function (event) {
        handleMessage(event);
    };

    websocket.onerror = function () {
        console.log('WebSocket error!');
        bootbox.alert({
            title: '提示',
            message: 'WebSocket連接異常蛾魄,請(qǐng)刷新頁(yè)面虑瀑!',
            callback: function () {
                window.location.reload();
            }
        });
    };

    websocket.onclose = function () {
        console.log('WebSocket close!');
        bootbox.alert({
            title: '提示',
            message: 'WebSocket連接斷開(kāi),請(qǐng)刷新頁(yè)面滴须!',
            callback: function () {
                window.location.reload();
            }
        });
    };

    window.onbeforeunload = function () {
        websocket.close();
    };
}

// 本地httpSessionId
var localSessionId;

/**
 * 處理收到的服務(wù)端響應(yīng)舌狗,根據(jù)消息類(lèi)型調(diào)用響應(yīng)處理方法
 */
function handleMessage(event) {
    var response = JSON.parse(event.data);

    // 獲取消息類(lèi)型
    var type = response.type;
    // 獲取httpSessionId
    /** @namespace response.httpSessionId */
    var httpSessionId = response.httpSessionId;
    // 獲取host
    var host = response.host;
    // 獲取username
    var username = response.username;
    // 獲取payload
    /** @namespace response.payload */
    var payload = response.payload;

    switch (type) {
        case 'chat':
            handleChatMessage(httpSessionId, username, payload);
            break;
        case 'online':
            console.log('online: ' + username);
            handleSystemMessage(username, type);
            break;
        case 'offline':
            console.log('offline: ' + username);
            handleSystemMessage(username, type);
            break;
        case 'error':
            console.log('error: ' + username);
            handleSystemMessage(username, type);
            break;
        case 'time':
            console.log('time: ' + payload);
            handleSystemMessage(null, type, payload);
            break;
        case 'list':
            handleUserList(payload);
            break;
        case 'authenticate':
            console.log('authenticate: ' + httpSessionId);
            localSessionId = httpSessionId;
            break;
        default:
            bootbox.alert({
                title: '提示',
                message: 'Unexpected message type.'
            });
            handleSystemMessage(null, type);
    }
}

/**
 * 處理聊天文本信息
 * 將本地用戶(hù)消息與其它用戶(hù)消息區(qū)分
 */
function handleChatMessage(httpSessionId, username, payload) {
    // ...
}

/**
 * 維護(hù)在線(xiàn)列表
 * @param payload
 */
function handleUserList(payload) {
   // ...
}

/**
 * 處理系統(tǒng)消息
 * @param username
 * @param type
 * @param payload
 */
function handleSystemMessage(username, type, payload) {
   // ...
}

/**
 * 發(fā)送消息
 */
// ...

效果展示

使用WebSocket構(gòu)建實(shí)時(shí)聊天

chrome視圖 可折疊用戶(hù)在線(xiàn)列表
chrome與Edge

苦逼的IE同志說(shuō)不出話(huà)來(lái),只算到IE11可能不支持WebSocket扔水,沒(méi)想到他其實(shí)是不支持contenteditable="plaintext-only"(后來(lái)又發(fā)現(xiàn)火狐也不支持)把夸。

移動(dòng)端視圖

總覽

心跳檢測(cè)

WebSocket是一個(gè)長(zhǎng)連接,需要心跳檢測(cè)機(jī)制來(lái)判斷服務(wù)端與客戶(hù)端之間建立的WebSocket連接是否存在和有效铭污。當(dāng)服務(wù)端斷開(kāi)連接時(shí)恋日,客戶(hù)端會(huì)立馬斷開(kāi)連接,并調(diào)用websocket.close嘹狞,而當(dāng)客戶(hù)端出現(xiàn)中斷網(wǎng)絡(luò)連接的情況岂膳,服務(wù)端不會(huì)立馬作出反應(yīng)(Spring WebSocket不會(huì)),而是過(guò)一段時(shí)間(推測(cè)是幾分鐘)后才將這個(gè)斷掉的WebSocketSession踢出磅网。

服務(wù)端的心跳檢測(cè)

完整項(xiàng)目下載

使用WebSocket構(gòu)建實(shí)時(shí)聊天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谈截,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子涧偷,更是在濱河造成了極大的恐慌簸喂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燎潮,死亡現(xiàn)場(chǎng)離奇詭異喻鳄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)确封,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)除呵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)再菊,“玉大人,你說(shuō)我怎么就攤上這事颜曾【腊危” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵泛豪,是天一觀的道長(zhǎng)稠诲。 經(jīng)常有香客問(wèn)我,道長(zhǎng)诡曙,這世上最難降的妖魔是什么臀叙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮岗仑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘聚请。我一直安慰自己荠雕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布驶赏。 她就那樣靜靜地躺著炸卑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪煤傍。 梳的紋絲不亂的頭發(fā)上盖文,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音蚯姆,去河邊找鬼五续。 笑死,一個(gè)胖子當(dāng)著我的面吹牛龄恋,可吹牛的內(nèi)容都是我干的疙驾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼郭毕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼它碎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起显押,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤扳肛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后乘碑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體挖息,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年兽肤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了旋讹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殖蚕。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖沉迹,靈堂內(nèi)的尸體忽然破棺而出睦疫,到底是詐尸還是另有隱情,我是刑警寧澤鞭呕,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布蛤育,位于F島的核電站,受9級(jí)特大地震影響葫松,放射性物質(zhì)發(fā)生泄漏瓦糕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一腋么、第九天 我趴在偏房一處隱蔽的房頂上張望咕娄。 院中可真熱鬧,春花似錦珊擂、人聲如沸圣勒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)圣贸。三九已至,卻和暖如春扛稽,著一層夾襖步出監(jiān)牢的瞬間吁峻,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工在张, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留用含,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓帮匾,卻偏偏與公主長(zhǎng)得像耕餐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辟狈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理肠缔,服務(wù)發(fā)現(xiàn),斷路器哼转,智...
    卡卡羅2017閱讀 134,660評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,822評(píng)論 6 342
  • 夢(mèng)的由來(lái)是個(gè)意外明未,是一個(gè)密秘。不為戀情壹蔓,不為克服苦難趟妥,也不是憤懣,更不是對(duì)于美好的一種依賴(lài)佣蓉。應(yīng)該是一種好奇披摄,一種追...
    淘猴侯孫行閱讀 434評(píng)論 2 5
  • K德宏閱讀 305評(píng)論 2 0