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ì)象:
可以看出嚷硫,應(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ā)送的信息
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屬性:
這種限制與限制跨域是類(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í)聊天
苦逼的IE同志說(shuō)不出話(huà)來(lái),只算到IE11可能不支持WebSocket扔水,沒(méi)想到他其實(shí)是不支持contenteditable="plaintext-only"(后來(lái)又發(fā)現(xiàn)火狐也不支持)把夸。
心跳檢測(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踢出磅网。