이전글
https://greed-yb.tistory.com/280
[SpringBoot] WebSocket 채팅방 구현(1) - 구현 화면
사용 중Bootstrap v4.6.0DataTables org.springframework.boot spring-boot-starter-websocket CSS 및 Class 명까지 그대로 올리니 나중에 구현할 때 감안할 것 직원 목록 및 채팅방 목록을 DataTables 로 구현하였으니DataTable
greed-yb.tistory.com
WebSocket 의 경우 새로고침 또는 메뉴 이동 시 client 접속이 해제되었다가 재연결을 하는데
이전에 만들었던 코드에서는 같은 session Id 를 중복으로 넣거나 최신 session Id 를 가져오는 코드가 없어서
이전에 만들었던 구현 코드를 개선하여 다시 작업하였다
이전 코드의 문제점으로는 상대방의 session Id 가 변경되면 메시지를 전송할 수가 없다는 점이다
WebSocketConfig
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
public WebSocketConfig(WebSocketHandler webSocketHandler){
this.webSocketHandler = webSocketHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/{username}/test.do").setAllowedOrigins("*");
}
}
Spring framework mvc 에서 동작하고 있어서 대상 파라미터 값이 중간에 들어가 있다
.do 뒤로 /{파라미터} 는 구조상 동작하지 않는다
WebSocketHandler
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Component
public class WebSocketHandler extends TextWebSocketHandler {
// 접속중인 유저의 session 및 채팅방 정보를 담기위한 변수
List<HashMap<String, Object>> rls = new ArrayList<>();
// 메시지 send 할 때 동작
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 사용자가 전송한 데이터를 JSON 으로 변경
String payload = message.getPayload();
JSONObject obj = jsonToObjectParser(payload);
// 메시지를 받는 상대방 id
String id = String.valueOf(obj.get("userId"));
System.out.println("메시지 받는 상대방 id : " + id);
// 상대방 id를 가지고 session 정보를 찾아 담기 위한 변수
HashMap<String, Object> temp = new HashMap<>();
if (rls.size() > 0) {
for (int i = 0; i < rls.size(); i++) {
HashMap<String, Object> sessionId = rls.get(i);
if (sessionId.size() > 1 && sessionId.get("userId").equals(id)) {
// sessionId가 실제로 WebSocketSession 객체인지 확인
temp = sessionId;
temp.put("userId", id);
break;
}
}
}
boolean sent = false;
for (String key : temp.keySet()) {
if (key.equals("userId")) {
continue;
}
WebSocketSession wss = (WebSocketSession) temp.get(key);
if (wss != null) {
try {
wss.sendMessage(new TextMessage(obj.get("roomDetail").toString()));
sent = true;
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 위 방식으로 메시지를 보내지 못했을 경우, findSessionByUserId() 를 이용하여 최신 sessionId 조회
if (!sent) {
WebSocketSession receiverSession = findSessionByUserId(id);
if (receiverSession != null && receiverSession.isOpen()) {
try {
receiverSession.sendMessage(new TextMessage(obj.get("roomDetail").toString()));
System.out.println("메시지 재전송");
} catch (Exception e) {
System.err.println("메시지 전송 중 오류");
e.printStackTrace();
}
} else {
System.err.println("userId [" + id + "]에 대한 세션이 존재하지 않거나 닫혀 있음");
}
}
}
// client 접속
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("웹소켓 연결됨: " + session.getId());
super.afterConnectionEstablished(session);
boolean flag = false;
String url = session.getUri().toString();
URI uri = new URI(url);
String path = uri.getPath(); // "/ws/TEST/test.do"
String[] parts = path.split("/");
String userId = parts[2];
System.out.println("afterConnectionEstablished : " + userId);
int idx = rls.size();
if (rls.size() > 0) {
for (int i = 0; i < rls.size(); i++) {
String id = (String) rls.get(i).get(i);
if (id != null && id.equals(session.getId())) {
flag = true;
idx = i;
break;
}
}
}
if (flag) {
HashMap<String, Object> map = rls.get(idx);
map.put(session.getId(), session);
} else {
HashMap<String, Object> map = new HashMap<>();
map.put(session.getId(), session);
map.put("userId", userId);
rls.add(map);
}
System.err.println("client 접속");
rls.removeIf(map -> userId.equals(map.get("userId"))); // 중복 제거
HashMap<String, Object> backupMap = new HashMap<>();
backupMap.put("userId", userId);
backupMap.put(session.getId(), session);
rls.add(backupMap);
System.err.println("client 재접속 userId 세션 추가 완료: " + userId);
}
// client 접속 해제
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.err.println(session + " client 접속 해제");
Iterator<HashMap<String, Object>> iter = rls.iterator();
while (iter.hasNext()) {
HashMap<String, Object> map = iter.next();
for (Map.Entry<String, Object> entry : map.entrySet()) {
// userId 키는 제외하고, 나머지는 WebSocketSession 이라고 가정
if (!entry.getKey().equals("userId")) {
WebSocketSession savedSession = (WebSocketSession) entry.getValue();
if (savedSession.getId().equals(session.getId())) {
iter.remove(); // 안전하게 삭제
System.out.println("세션 제거 완료: " + session.getId());
break;
}
}
}
}
super.afterConnectionClosed(session, status);
}
// send 로 전달한 메세지 변환
private static JSONObject jsonToObjectParser(String jsonStr) {
JSONParser parser = new JSONParser();
JSONObject obj = null;
try {
obj = (JSONObject) parser.parse(jsonStr);
} catch (ParseException e) {
e.printStackTrace();
}
return obj;
}
// 메시지 전송 받는 userId 의 최신 session 조회
public WebSocketSession findSessionByUserId(String userId) {
for (HashMap<String, Object> sessionMap : rls) {
if (userId.equals(sessionMap.get("userId"))) {
for (Map.Entry<String, Object> entry : sessionMap.entrySet()) {
if (!entry.getKey().equals("userId")) {
return (WebSocketSession) entry.getValue();
}
}
}
}
return null;
}
}
Javascript
<script type="text/javascript">
var socket;
// 로그인한 사용자 ID
var username = 'TEST';
// 웹소켓 연결
function widgetWebsocket(username){
socket = new WebSocket("ws://본인IP:port/ws/"+ username + "/test.do");
socket.onopen = () => {
console.log("WebSocket connection opened");
};
socket.onerror = (error) => {
console.error("WebSocket error", error);
};
socket.onclose = (event) => {
console.log("WebSocket closed", event);
};
socket.onmessage = (msg) => {
console.log("메세지 전달받는중");
let message = msg.data;
console.log("message : " + message);
// 필요에 따라 로직 구현부
}
}
// 메시지 전송
function apiTestBtn(){
// 메시지 전송할 대상들
var targetUserId = [];
targetUserId.push("balju01");
targetUserId.push("sigong01");
targetUserId.push("TEST");
var senMsg = "웹소켓 테스트 메시지 전송";
if(socket == null){
widgetWebsocket(username);
}
for (var i = 0; i < targetUserId.length; i++) {
console.log("wwwww : " + i);
let widgetData = new Object();
widgetData.userId = targetUserId[i];
widgetData.roomDetail = senMsg;
// 채팅방 웹소켓에 전송
socket.send(JSON.stringify(widgetData));
}
}
$(function() {
// 웹소켓 연결
widgetWebsocket(username);
// 30분마다 웹소켓 상태 체크 및 재연결
setInterval(function() {
console.log("웹소켓 연결 여부 확인 중");
if (!socket || socket.readyState === WebSocket.CLOSED) {
console.log("웹소켓이 닫혀 있음. 재연결 시도...");
widgetWebsocket(username);
}
}, 30 * 60 * 1000); // 30분 - 30 * 60 * 1000
});
</script>
로그인 사용자 username 은 본인 구조에 맞게 바인딩 시켜주자
TEST
chrome 에서 계정1('sigong01') 을 로그인
Edge 에서 계정2('TEST') 를 로그인
전송 버튼을 클릭하면
임의의 하드코딩한 계정 3개에 메시지를 전송한다
현재 로그인한 사용자가 본인을 포함한 2명이라
전송 버튼 클릭 시 본인에게도 메시지를 전송하였기 때문에 테스트 폼에 전달받은 메시지가 출력되었고
sigong01 계정의 브라우저에 전달한 메시지를 확인할 수 있다
회사에서 중간중간 작성하던 글이라 내용이나 설명이 많이 부족할 수 있다...
'개발 > Spring' 카테고리의 다른 글
[SpringBoot] React + TypeScript + JPA 프로젝트 (14) - Quill 적용하기 (0) | 2024.11.25 |
---|---|
[SpringBoot] React + TypeScript + JPA 프로젝트 (13) - Chart.js 적용하기 (0) | 2024.11.24 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (12) - 로그아웃 하기 (0) | 2024.11.23 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (11) - 게시판 만들기(삭제하기) (0) | 2024.11.22 |
[SpringBoot] React + TypeScript + JPA 프로젝트 (10) - 게시판 만들기(수정하기) (0) | 2024.11.21 |
댓글