Websocket初識與聊天室Demo
寫在前面
前段時間有個面試,被問到如果要做web端登錄保持一端可用,即在多個瀏覽器登錄時,要將前一次登錄的信息及時踢出。當時我說了兩種方案:第一種 是用ajax輪詢服務器,第二種就是websocket。第一種是我剛畢業那會的實施方案,有過相關經驗,第二種是之前瞭解過,但沒有實際的開發經驗。面試完後,我就想深入去了解一下websocket,然後寫一些案例鞏固一下。
什麼是websocket
WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket通信協議於2011年被IETF定爲標準RFC 6455,並由RFC7936補充規範。WebSocket API也被W3C定爲標準。
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。(百度百科)
** 其實,websocket就是http5 後升級出來的一個協議,它即有http協議的特性,也有socket協議的特性。是兩者的交集。http是無狀態的協議,且只能主動去請求服務器的數據,websocket則保持連接,服務器可以主動推送數據給瀏覽器。**
WebSocket案例一:簡單聊天室
在大學學JAVA的時候,那時候剛接觸了socket編碼,然後就寫了簡易的聊天室。不過基本上是通過win 的cmd命令來進行,今天,也準備用這個小案來實現一下。
主要技術棧爲:
- JDK 1.8
- maven 3.3.9
- springboot 2.2.7.RELEASE
- springboot websocket
Maven 依賴
SpringBoot2.0 + 對WebSocket 已經支持 ,直接添加相關依賴即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig配置
package com.keyingbo.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketChatRoomServer
服務端接收請求的主要配置類:
-
因爲WebSocket是類似客戶端服務端的形式(採用ws協議),那麼這裏的WebSocketServer其實就相當於一個ws協議的Controller
-
直接
@ServerEndpoint("/websocketChatRoom/{userId}")
、@Component
啓用即可,然後在裏面實現@OnOpen
開啓連接,@onClose
關閉連接,@onMessage
接收消息等方法。 -
新建一個ConcurrentHashMap webSocketMap 用於接收當前userId的WebSocket,方便之間對userId進行推送消息。
package com.keyingbo.websocket.server;
import com.alibaba.fastjson.JSON;
import com.keyingbo.websocket.dto.UserMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 簡易聊天室服務端代碼
* @ServerEndpoint
* 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端,
* 註解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
*/
@ServerEndpoint("/websocketChatRoom/{userId}")
@Component
public class WebSocketChatRoomServer {
static Logger logger = LoggerFactory.getLogger(WebSocketChatRoomServer.class);
//靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
//concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以爲用戶標識
private static ConcurrentHashMap<String, WebSocketChatRoomServer> webSocketSet = new ConcurrentHashMap<String, WebSocketChatRoomServer>();
//與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session WebSocketsession;
//當前發消息的人員userId
private String userId = "";
/**
* 連接建立成功調用的方法*/
@OnOpen
public void onOpen(@PathParam(value = "userId") String param, Session WebSocketsession, EndpointConfig config) {
userId = param;
//log.info("authKey:{}",authKey);
this.WebSocketsession = WebSocketsession;
webSocketSet.put(param, this);//加入map中
int cnt = OnlineCount.incrementAndGet(); // 在線數加1
logger.info("有連接加入,當前連接數爲:{}", cnt);
UserMessage userMessage = new UserMessage();
userMessage.setType("system");
userMessage.setMessage("連接成功");
sendMessage(this.WebSocketsession, userMessage);
}
/**
* 連接關閉調用的方法
*/
@OnClose
public void onClose() {
if (!userId.equals("")){
webSocketSet.remove(userId);//從set中刪除
int cnt = OnlineCount.decrementAndGet();
logger.info("有連接關閉,當前連接數爲:{}", cnt);
}
}
/**
* 收到客戶端消息後調用的方法
*
* @param message 客戶端發送過來的消息*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("來自客戶端的消息:{}",message);
UserMessage userMessage = JSON.parseObject(message , UserMessage.class);
userMessage.setType("usermsg");
broadCastInfo(userMessage);
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("發生錯誤:{},Session ID: {}",error.getMessage(),session.getId());
error.printStackTrace();
}
/**
* 發送消息,實踐表明,每次瀏覽器刷新,session會發生變化。
* @param message
*/
public void sendMessage(Session session, UserMessage message) {
try {
session.getBasicRemote().sendText(JSON.toJSONString(message));
//session.getBasicRemote().sendText(String.format("%s",message));
} catch (IOException e) {
logger.error("發送消息出錯:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 羣發消息
* @param message
* @throws IOException
*/
public void broadCastInfo(UserMessage message) {
for (String key : webSocketSet.keySet()) {
Session session = webSocketSet.get(key).WebSocketsession;
//&& !userId.equals(key)
if(session != null && session.isOpen() ){
sendMessage(session, message);
}
}
}
/**
* 指定Session發送消息
* @param message
* @throws IOException
*/
public void sendToUser(String userId, UserMessage message) {
WebSocketChatRoomServer webSocketServer = webSocketSet.get(userId);
if ( webSocketServer != null && webSocketServer.WebSocketsession.isOpen()){
sendMessage(webSocketServer.WebSocketsession, message);
}
else{
logger.warn("當前用戶不在線:{}",userId);
}
}
}
webSocketDemo.html
然後,就是客戶端的demo:
主要的邏輯就是建立websocket連接,以及綁定相關的事件。直接看代碼:
<!DOCTYPE html>
<html>
<head>
<title>簡易聊天Demo</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
<link href="https://cdn.bootcss.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
<link href="../css/webSocketDemo.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="title">簡易聊天demo</div>
<div class="content">
<div class="show-area"></div>
<div class="write-area">
<div>
<button class="btn btn-default send">發送</button>
</div>
<div><input name="name" id="name" type="text" placeholder="input your name"></div>
<div>
<textarea name="message" id="message" cols="38" rows="4" placeholder="input your message..."></textarea>
</div>
</div>
</div>
</div>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script>
$(function () {
var lockReconnect = false; //避免重複連接
var userId = "user" + new Date().getMilliseconds();
var wsurl = 'ws://127.0.0.1:8080/websocketChatRoom/user' + userId;
var websocket;
var i = 0;
if (window.WebSocket) {
doConnect();//連接
function send() {
var name = $('#name').val();
var message = $('#message').val();
if (!name) {
alert('請輸入用戶名!');
return false;
}
if (!message) {
alert('發送消息不能爲空!');
return false;
}
var msg = {
message: message,
name: name
};
try {
websocket.send(JSON.stringify(msg));
} catch (ex) {
console.log(ex);
}
}
//按下enter鍵發送消息
$(window).keydown(function (event) {
if (event.keyCode == 13) {
console.log('user enter');
send();
}
});
//點發送按鈕發送消息
$('.send').bind('click', function () {
send();
});
}
else {
alert('該瀏覽器不支持web socket');
}
function getJSON(str) {
if (typeof str == 'string') {
try {
return JSON.parse(str);
} catch(e) {
return null;
}
}
}
function reconnect() {
if (lockReconnect) return;
lockReconnect = true;
//沒連接上會一直重連,設置延遲避免請求過多
setTimeout(function() {
try {
doConnect();
lockReconnect = false;
} catch (e) {
reconnect()
}
}, 2000);
}
function doConnect() {
websocket = new WebSocket(wsurl);
//連接建立
websocket.onopen = function (evevt) {
console.log("Connected to WebSocket server.");
$('.show-area').append('<p class="bg-info message"><i class="glyphicon glyphicon-info-sign"></i>Connected to WebSocket server!</p>');
}
//收到消息
websocket.onmessage = function (event) {
var msg = getJSON(event.data); //解析收到的json消息數據
var type = msg.type; // 消息類型
var umsg = msg.message; //消息文本
var uname = msg.name; //發送人
i++;
if (type == 'usermsg') {
$('.show-area').append('<p class="bg-success message"><i class="glyphicon glyphicon-user"></i><a name="' + i + '"></a><span class="label label-primary">' + uname + ' : </span>' + umsg + '</p>');
}
if (type == 'system') {
$('.show-area').append('<p class="bg-warning message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>' + umsg + '</p>');
}
$('#message').val('');
window.location.hash = '#' + i;
}
//發生錯誤
websocket.onerror = function (event) {
i++;
console.log("Connected to WebSocket server error");
$('.show-area').append('<p class="bg-danger message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>Connect to WebSocket server error.</p>');
//重連
reconnect();
}
//連接關閉
websocket.onclose = function (event) {
i++;
console.log('websocket Connection Closed. ');
$('.show-area').append('<p class="bg-warning message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>websocket Connection Closed.</p>');
//斷線重連
reconnect();
//window.location.hash = '#' + i;
}
}
});
</script>
</body>
</html>
結語
以上案例,我本地已經跑通,具體的源碼地址爲:
源碼地址
啓動後,本案例訪問步驟說明:
- 打開多個瀏覽器或者多個頁籤輸入:http://localhost:8080/html/webSocketDemo.html
- 在各自頁面輸入相關的信息,點擊發送可以看到效果。
最後說下:本案的代碼有參考網上的大佬,而且時間比較倉促,寫的有點粗糙,見諒,有問題私聊,謝謝。