今日目標,使用 WebSocket 傳遞房間列表資料,並即時的將其顯示在頁面上。
我們依舊需要在 Controller 負責對定義的 endpoint、broker url 做處理,類似於前面的 Controller 提到的請求分發。
package com.example.room;
import lombok.Getter;
import lombok.Setter;
import java.util.Set;
public class RoomListResponse {
@Getter @Setter
private Set<Room> rooms;
}
package com.example.room;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class RoomWsController {
@Autowired
private RoomList roomList;
@Autowired
private UserStatus userStatus;
@MessageMapping("/room-list")
@SendTo("/topic/room-list")
public RoomListResponse getAllRooms() {
RoomListResponse roomListResponse = new RoomListResponse();
roomListResponse.setRooms(roomList.getRooms());
return roomListResponse;
}
}
@MessageMapping("/room-list")
:定義接收資料的 url@SendTo("/topic/room-list")
:定義 Server 將傳遞訊息至哪個代理再來我們要在頁面上透過 JavaScript 操作 WebScoket,先建立連線再訂閱訊息代理,藉此來接收 Server 傳遞過來的資訊。
resource/static/js
底下建立 WebSockets.js
,內容為:
class WebSocket {
constructor() {
this.stompClient = null;
}
connect(connect, callback) {
let socket = new SockJS(connect);
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, function (frame) {
callback();
});
}
disconnect() {
if (this.isConnected()) {
this.stompClient.disconnect();
}
}
send(controller, json) {
setTimeout(() => {
this.stompClient.send(controller, {}, JSON.stringify(json));
}, 1000);
}
subscribe(topic, onMessage) {
setTimeout(() => {
this.stompClient.subscribe(topic, onMessage);
}, 1000);
}
isConnected() {
return this.stompClient !== null;
}
}
.send()
、.subscribe()
必須要在已經連線的時候才可以觸發,他應該放在 .connect()
的那個函數內,因為這牽涉到同步與非同步問題,避免文章過於冗長且失焦,小弟這邊用最簡單但不太靠普的方式處理 send()
、subscribe()
,在還沒連線時,就先延遲一秒(setTimeout()
),等待 connect 執行完畢才執行connect()
),並傳入對應函數來處理連線後的動作rooms.js
,這裡要做 WebSocket 連線,以及接收到訊息後顯示房間列表,完整內容為:
var websocket = new WebSocket();
websocket.connect('/connect', () => {
getRoomList()
});
function getRoomList() {
// 訂閱,並定義收到訊息該做什麼操作
websocket.subscribe("/topic/room-list", (response) => {
// 處理接收到的資料
response = JSON.parse(response.body);
let rooms = response["rooms"];
// 顯示房間列表
$("#rooms tbody").empty();
let roomList = $("#rooms tbody").html();
for (let room of rooms) {
roomList += `
<tr>
<th scope="row">${room.roomId}</th>
<td>${room.owner}</td>
<td>${room.info.number} / 4</td>
<td>
<button type="button" class="btn btn-outline-primary" onclick="joinRoom('${room.roomId}')">
加入
</button>
</td>
</tr>
`
}
$("#rooms tbody").html(roomList);
})
// 發送訊息給 Server 表示要取得房間列表的資料
websocket.send(`/room-list`, {});
}
function createRoom() {
let data = {
action : "create",
}
jq.post(
"/api/room/join",
data,
(response) => {
window.location.href = `/room/${response.roomId}`;
}
)
.fail(function(e) {
$("#alert-toast-title").text(e.responseJSON.message);
$("#alert-toast").toast('show');
})
}
function joinRoom(roomId) {
let data = {
action : "join",
roomId : roomId,
}
jq.post(
"/api/room/join",
data,
(response) => {
window.location.href = `/room/${response.roomId}`;
}
)
.fail(function(e) {
$("#alert-toast-title").text(e.responseJSON.message);
$("#alert-toast").toast('show');
})
}
$(document).ready(() => {
$("#create").click(function() {
createRoom();
})
})
joinRoom()
函數resource/static/css
底下建立 rooms.css
,內容為:
.room-list {
height: 450px;
width: 1000px;
margin: -20px auto 0 auto;
padding: 35px;
text-align: center;
z-index: 1;
}
rooms.html
引入相關的 CDN 以及剛才定義的 WebSocket.js
,僅需要修改 layout:fragment="js-and-css"
的區塊:
<div layout:fragment="js-and-css">
<!-- websockets -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"
integrity="sha512-tL4PIUsPy+Rks1go4kQG8M8/ItpRMvKnbBjQm4d2DQnFwgcBYRRN00QdyQnWSCwNMsoY/MfJY8nHp2CzlNdtZA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<!-- custom -->
<script type="text/javascript" th:src="@{/js/WebSockets.js}"></script>
<script type="text/javascript" th:src="@{/js/rooms.js}"></script>
<link th:href="@{/css/rooms.css}" rel="stylesheet">
</div>
broadcastRoomList()
的功能,完整內容為:
package com.example.room;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@Service
public class RoomService {
@Autowired
private RoomList roomList;
@Autowired
private UserStatus userStatus;
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
public boolean createRoom(String username) {
if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
return false;
}
String roomId = roomList.create(username);
this.userStatus.setUserInRoom(username, true);
this.userStatus.setUserRoomId(username, roomId);
return true;
}
public boolean joinInRoom(String username, String roomId) {
if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
return false;
}
if (roomList.getRoomById(roomId).count() == 4) {
return false;
}
Room room = roomList.getRoomById(roomId);
if (room.addGuest(username)) {
if (!this.userStatus.containsUser(username)) {
this.userStatus.initialize(username);
}
this.userStatus.setUserInRoom(username, true);
this.userStatus.setUserRoomId(username, roomId);
return true;
}
return false;
}
public void broadcastRoomList() {
RoomListResponse roomListResponse = new RoomListResponse();
roomListResponse.setRooms(roomList.getRooms());
this.simpMessagingTemplate.convertAndSend("/topic/room-list", roomListResponse);
}
}
SimpMessagingTemplate
:用於發送訊息的方法,可以設定目的代理,將資料傳送過去simpMessagingTemplate.convertAndSend()
:第一個參數是目的代理,第二個參數是資料joinRoomProcess()
,添加 broadcastRoomList()
方法,用於在有任何一個使用者建立或加入房間就傳送最新的房間列表給所有訂閱的 Client,完整內容為:
package com.example.room;
import com.example.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@Controller
public class RoomController {
@Autowired
private UserService userService;
@Autowired
private UserStatus userStatus;
@Autowired
private RoomService roomService;
@GetMapping("/rooms")
public String viewAllRoomsPage(Model model) {
if (userService.isLogin()) {
String username = userService.getUsername();
if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
return "redirect:/room/" + this.userStatus.getUserRoomId(username);
}
this.userStatus.initialize(username);
}
model.addAttribute("disableJoinRoomButton", !userService.isLogin());
return "rooms";
}
@PostMapping("/api/room/join")
@ResponseBody
public ResponseEntity<Map<String, Object>> joinRoomProcess(UserJoinRoomMessage userJoinRoomMessage) {
Map<String, Object> response = new HashMap<>();
HttpStatus httpStatus;
String message;
if (!userService.isLogin()) {
httpStatus = HttpStatus.FORBIDDEN;
message = "未登入";
response.put("message", message);
return ResponseEntity.status(httpStatus).body(response);
}
String username = userService.getUsername();
String action = userJoinRoomMessage.getAction();
String roomId = userJoinRoomMessage.getRoomId();
if (action.equals("create") && roomService.createRoom(username)) {
httpStatus = HttpStatus.OK;
message = "建立成功";
}
else if (action.equals("join") && roomService.joinInRoom(username, roomId)) {
httpStatus = HttpStatus.OK;
message = "加入成功";
}
else {
httpStatus = HttpStatus.BAD_REQUEST;
message = "Error";
}
if (httpStatus == HttpStatus.OK) {
// 發送最新的房間列表資訊
this.roomService.broadcastRoomList();
roomId = this.userStatus.getUserRoomId(username);
}
else {
roomId = "";
}
response.put("message", message);
response.put("roomId", roomId);
return ResponseEntity.status(httpStatus).body(response);
}
}