Spring Boot學習分享(七)——整合WebSocket開發多個聊天室多人線上實時通訊
阿新 • • 發佈:2019-01-06
(一)使用原生WebSocket的註解編寫服務端
由於想要實現可以統計具體線上人數,而因為想要設計成多個房間同時進行,如果使用Spring自帶的以Stomp為協議的WebSocket實現則比較繁瑣,必須配置攔截器,由於攔截器可以得到的引數比較少,必須注入其它的輔助類來進行房間的確認以及當前人數的統計併發送,所以個人覺得還是直接使用原生的WebSocket進行程式設計爽一點,不過也可能是我沒有想到更好的解決方案,SpringBoot的官方文件也沒怎麼仔細鑽研=。=
下面是具體的實現程式碼,這裡使用註解進行程式設計,是在網上一個博主的程式碼上進行更改的。
使用前的一點配置,不加上的話可能將無法成功註冊WebSocket服務
/**
* 對WebSocket進行配置
* 這裡使用Stomp協議
*
* @author MDY
*/
@Configuration
public class WebSocketConfig {
/**
* 是用程式設計式WebSocket需要的一個bean
* 聽說是註冊埠。。
*/
@Bean
public ServerEndpointExporter aaaserverEndpointExporter() {
return new ServerEndpointExporter();
}
由於使用的是SpringBoot,自動裝載bean的時候可能因為順序的原因出現各種問題,像是沒有得到註解裝飾
下面的程式碼可能會出現bean值為空的情況。
/**
* 加上RestController註解,使其可以響應檔案上傳連結
* 如果不註冊成控制層則必須加一個元件掃描,否則將無法註冊
* 所有的message都將使用json進行傳輸
* 可以根據連結的直接得到roomName
* 關於服務層的注入,只能通過Spring容器的上下文來進行賦值,否則會得到空值
* ChatMessage為自己定義的進行資訊傳輸的類,這裡全部將其轉為JSON,方便解析
*
* @author MDY
*/
@ServerEndpoint(value = "/consult/{roomName}" )
@RestController
@Log4j2//這個註解是Lombok的,可以簡化程式碼,不用可以忽略
public class ConsultWebSocket {
// 使用map來收集session,key為roomName,value為同一個房間的使用者集合
private static final Map<String, Set<Session>> rooms = new ConcurrentHashMap<>();
//快取session對應的使用者
private static final Map<String, String> users = new ConcurrentHashMap<>();
//用來快取聊天記錄的
private ChatCacheService chatCacheService;
//進行檔案上傳具體實現細節的
private FileService fileService;
/**
* 連線建立後將上線的使用者廣播給組員
*/
@OnOpen
public void connect(@PathParam("roomName") String roomName, Session session) throws IOException {
//目前使用隨機名稱,可以整合自己的session管理,如shiro之類的
String name = randomName();
// 將session按照房間名來儲存,將各個房間的使用者隔離
if (!rooms.containsKey(roomName)) {
// 建立房間不存在時,建立房間
Set<Session> room = new HashSet<>();
// 新增使用者
room.add(session);
rooms.put(roomName, room);
} else {
// 房間已存在,直接新增使用者到相應的房間
rooms.get(roomName).add(session);
}
users.put(session.getId(), name);
//向上線的人傳送當前線上的人的列表
List<ChatMessage> userList = new LinkedList<>();
rooms.get(roomName)
.stream()
.map(Session::getId)
.forEach(s -> {
ChatMessage chatMessage = new ChatMessage();
chatMessage.setDate(new Date());
chatMessage.setUserName("sys");
chatMessage.setChatContent(users.get(s) + "線上");
userList.add(chatMessage);
});
session.getBasicRemote().sendText(JSON.toJSONString(userList));
//向房間的所有人廣播誰上線了
ChatMessage chatMessage = new ChatMessage();
chatMessage.setDate(new Date());
chatMessage.setUserName("sys");
chatMessage.setChatContent(users.get(session.getId()) + "上線了");
broadcast(roomName, JSON.toJSONString(chatMessage));
}
@OnClose
public void disConnect(@PathParam("roomName") String roomName, Session session) {
rooms.get(roomName).remove(session);
ChatMessage chatMessage = new ChatMessage();
chatMessage.setDate(new Date());
chatMessage.setUserName("sys");
chatMessage.setChatContent(users.get(session.getId()) + "下線了");
users.remove(session.getId());
broadcast(roomName, JSON.toJSONString(chatMessage));
log.info("<<<<<<<<<<<<<a client has disconnected!>>>>>>>>>>>>>>");
}
/**
* @param msg 前臺傳回來的資料應為json資料
*/
@OnMessage
public void receiveMsg(@PathParam("roomName") String roomName,
String msg, Session session) {
// 此處應該有html過濾,進行資料加工
msg = users.get(session.getId()) + ":" + msg;
// 接收到資訊後進行廣播
broadcast(roomName, msg);
}
/**
* 傳送圖片,視訊,語音等
*
* @param name 使用者名稱
* @param roomName 房間id
* @param file 上傳的檔案
*/
@PostMapping("/consult/{roomName}/{name}")
public void file(@PathVariable("name") String name, @PathVariable("roomName") String roomName, MultipartFile file) {
ChatMessage chatMessage = new ChatMessage();
chatMessage.setDate(new Date());
chatMessage.setUserName(name);
chatMessage.setChatContent(fileService.upload(file, roomName));
broadcast(roomName, JSON.toJSONString(chatMessage));
}
// 按照房間名進行廣播
private void broadcast(String roomName, String msg) {
rooms.get(roomName).forEach(s -> {
try {
s.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
});
//將聊天記錄加入快取
//這裡需要將此服務層的bean手動注入
if (chatCacheService == null) {
chatCacheService = ApplicationContextRegister.getApplicationContext().getBean(ChatCacheService.class);
}
chatCacheService.cacheMsg(msg, roomName, CacheType.CONSULT);
}
//隨機姓名
private String randomName() {
Random random = new Random();
String str = "";
int hightPos, lowPos;
for (int i = 0; i < 4; ++i) {
hightPos = (176 + Math.abs(random.nextInt(39)));
lowPos = (161 + Math.abs(random.nextInt(93)));
byte[] b = new byte[2];
b[0] = (Integer.valueOf(hightPos)).byteValue();
b[1] = (Integer.valueOf(lowPos)).byteValue();
try {
str += new String(b, "GB2312");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return str;
}
}
下面則是前端頁面的簡單實現,從網上直接找到的
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>網路聊天室</title>
</head>
<style type="text/css">
.msg_board {
width: 644px;
height: 200px;
border: solid 1px darkcyan;
padding: 5px;
overflow-y: scroll;
// 文字長度大於div寬度時換行顯示
word-break: break-all;
}
/*set srcoll start*/
::-webkit-scrollbar
{
width: 10px;
height: 10px;
background-color: #D6F2FD;
}
::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
/*border-radius: 5px;*/
background-color: #D6F2FD;
}
::-webkit-scrollbar-thumb
{
height: 20px;
/*border-radius: 10px;*/
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #89D7F7;
}
/*set srcoll end*/
</style>
<body>
<label>房間名</label>
<input id="input_roomName" size="10" maxlength="10">
<input type="button" value="進入聊天室" onclick="initWebSocket()" />
<input type="button" value="退出聊天室" onclick="closeWs()" /><br>
<div class="msg_board"></div>
<input id="input_msg" size="43" maxlength="40">
<input type="button" value="傳送" onclick="send_msg()" />
</body>
<script type="text/javascript">
var webSocket;
function send_msg() {
if (webSocket != null) {
var input_msg = document.getElementById("input_msg").value.trim();
if (input_msg == "") {
return;
}
webSocket.send(input_msg);
// 清除input框裡的資訊
document.getElementById("input_msg").value = "";
} else {
alert("您已掉線,請重新進入聊天室...");
}
};
function closeWs() {
webSocket.close();
};
function initWebSocket() {
var roomName = document.getElementById("input_roomName").value;
// 房間名不能為空
if (roomName == null || roomName == "") {
alert("請輸入房間名");
return;
}
if ("WebSocket" in window) {
if (webSocket == null) {
//連結和埠號必須要正確,如何還是報404錯誤,那就是WebSocket服務沒有啟動
var url = "ws://localhost:8010/consult/" + roomName;
// 開啟一個 web socket
webSocket = new WebSocket(url);
} else {
alert("您已進入聊天室...");
}
webSocket.onopen = function () {
alert("已進入聊天室,暢聊吧...");
};
webSocket.onmessage = function (evt) {
var msg_board = document.getElementsByClassName("msg_board")[0];
var received_msg = evt.data;
var old_msg = msg_board.innerHTML;
msg_board.innerHTML = old_msg + received_msg + "<br>";
// 讓滾動塊往下移動
msg_board.scrollTop = msg_board.scrollTop + 40;
};
webSocket.onclose = function () {
// 關閉 websocket,清空資訊板
alert("連線已關閉...");
webSocket = null;
document.getElementsByClassName("msg_board")[0].innerHTML = "";
};
}
else {
// 瀏覽器不支援 WebSocket
alert("您的瀏覽器不支援 WebSocket!");
}
}
</script>
</html>
最後是Java的簡單超實現,這裡就不整合SpringBoot了
所需的依賴
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.8</version>
</dependency>
簡單實現,沒什麼比較坑的地方,直接用就可以了
WebSocketClient webSocketClient = new WebSocketClient(URI.create("ws://localhost:8010/consult/637"), new Draft_6455()) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("連結建立成功");
}
@Override
public void onMessage(String s) {
System.out.println(s);
}
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("連結斷開");
}
@Override
public void onError(Exception e) {
System.out.println(e.getMessage());
}
};
webSocketClient.connect();
}