실시간 채팅창과 실시간 쪽지보내기(받는 사람 화면의 경우 토스트 메시지 팝업 출현)을 구현하고자 했다.
한 3일동안 매일 4시간정도 붙들었나...
소켓 자체는 정해진 형식이 있어서 구글링에 어려움은 없었는데 스프링에 sockt과 sockjs 라이브러리를 추가하는게 정말 너무!!! 안되어서 애먹었다. 스프링 의존성 추가...널 의존하지 않으면 안되겠니😮
나를 가장 힘들게 했던 의존성 주입.
소켓 관련 거의 모든 블로그 글들을 따라해봤지만 계속 의존성 주입에 실패하다가 이 분의 블로그 글 덕에 성공했다. 정말 감사합니다...정말 정말 감사합니다...
그리고 전체적인 로직?클래스와 패키지, 메소드 구성은 이 글의 도움을 받았다. 감사합니다!
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<beans:bean id="echoHandler" class="com.chat.config.EchoHandler"/>
<websocket:handlers>
<websocket:mapping handler="echoHandler" path="/echo"/>
<websocket:sockjs/>
</websocket:handlers>
path는 /echo-ws
로 바꾸던지...jsp화면에서 sockjs를 어떻게 설정하는지에 따라 바꿔야함.
package com.chat.config;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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;
public class EchoHandler extends TextWebSocketHandler {
//로그인 한 전체 session 리스트
List<WebSocketSession> sessions = new ArrayList<WebSocketSession>();
// 현재 로그인 중인 개별 유저
Map<String, WebSocketSession> users = new ConcurrentHashMap<String, WebSocketSession>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String senderId = getMemberId(session); // 접속한 유저의 http세션을 조회하여 id를 얻는 함수
if(senderId!=null) { // 로그인 값이 있는 경우만
log(senderId + " 연결 됨");
users.put(senderId, session); // 로그인중 개별유저 저장
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String senderId = getMemberId(session);
// 특정 유저에게 보낼 메시지 내용 추출
String msg = message.getPayload();
if(msg != null) {
String[] strs = msg.split(",");
log(strs.toString());
if(strs != null && strs.length == 4) {
String type = strs[0];
String target = strs[1]; // m_id 저장
String content = strs[2];
String url = strs[3];
WebSocketSession targetSession = users.get(target); // 메시지를 받을 세션 조회
// 실시간 접속시
if(targetSession!=null) {
TextMessage tmpMsg = new TextMessage("<a target='_blank' href='"+ url +"'>[<b>" + type + "</b>] " + content + "</a>" );
targetSession.sendMessage(tmpMsg);
}
}
}
}
//연결이 끊어진 후
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String senderId = session.getId();
if(senderId!=null) { // 로그인 값이 있는 경우만
log(senderId + " 연결 종료됨");
users.remove(senderId);
sessions.remove(session);
}
}
// 에러 발생시
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log(session.getId() + " 익셉션 발생: " + exception.getMessage());
}
// 로그 메시지
private void log(String logmsg) {
System.out.println(new Date() + " : " + logmsg);
}
// 웹소켓에 id 가져오기
// 접속한 유저의 http세션을 조회하여 id를 얻는 함수
private String getMemberId(WebSocketSession session) {
Map<String, Object> httpSession = session.getAttributes();
String m_id = (String) httpSession.get("id"); // 세션에 저장된 m_id 기준 조회
return m_id==null? null: m_id;
}
}
여기서 ConcurrentHashMap(클릭)이 뭔지 몰라서 해당 글을 참고했다. 쉽게 말하면 읽기 작업에는 여러 쓰레드가 동시에 읽을 수 있지만, 쓰기 작업에는 특정 세그먼트 or 버킷에 대한 Lock을 사용
한다는 것이다.
<!-- sockJS -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script>
// 전역변수 설정
var socket = null;
$(document).ready(function(){
// 웹소켓 연결
var sock = new SockJS("/echo");
socket = sock;
// 데이터를 전달 받았을때
sock.onmessage = onMessage; // toast 생성
});
// toast생성 및 추가
function onMessage(evt){
var data = evt.data;
// toast
let toast = "<div class='toast' role='alert' aria-live='assertive' aria-atomic='true'>";
toast += "<div class='toast-header'><i class='fas fa-bell mr-2'></i><strong class='mr-auto'>알림</strong>";
toast += "<small class='text-muted'>just now</small><button type='button' class='ml-2 mb-1 close' data-dismiss='toast' aria-label='Close'>";
toast += "<span aria-hidden='true'>×</span></button>";
toast += "</div> <div class='toast-body'>" + data + "</div></div>";
$("#msgStack").append(toast); // msgStack div에 생성한 toast 추가
$(".toast").toast({"animation": true, "autohide": false});
$('.toast').toast('show');
};
</script>
<body>
...
<div id="msgStack"></div>
</body>
원래 블로그 글은 이렇게 되어있었지만 좀 더 내 식(?)대로 만들기 위해 새 블로그 글을 참조해 토스트를 만들었다.
토스트 만들기는 이 글을 참조했다. 정말 감사합니다...
<!-- sockJS -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script>
// 전역변수 설정
var socket = null;
$(document).ready(function(){
// 웹소켓 연결
sock = new SockJS("<c:url value="/echo-ws"/>");
socket = sock;
// 데이터를 전달 받았을때
sock.onmessage = onMessage; // toast 생성
//----------------------------------------------
function fillWidth(elem, timer, limit) {
if (!timer) { timer = 3000; }
if (!limit) { limit = 100; }
var width = 1;
var id = setInterval(frame, timer / 100);
function frame() {
if (width >= limit) {
clearInterval(id);
} else {
width++;
elem.style.width = width + '%';
}
}
};
//-------------------------------------------
function onMessage(evt) {
//받은 메시지 내용 추출
var data = evt.data;
//메시지가 떠있을 시간 설정
var timer = 7000;
//토스트 노드 미리 만들어두기
var $elem = $("<div class='toastWrap'><span class='toast'>" + data + "<b></b><div class='timerWrap'><div class='timer'></div></div></span></div>");
//토스트 띄우기
$("#toast").append($elem); //top = prepend, bottom = append
//토스트 창이 나타난 후 벌어질 상황 설계
$elem.slideToggle(100, function() {
//남은 시간 알려주며 줄어드는 바 표시
$('.timerWrap', this).first().outerWidth($elem.find('.toast').first().outerWidth() - 10);
fillWidth($elem.find('.timer').first()[0], timer);
setTimeout(function() {
$elem.fadeOut(function() {
$(this).remove();
});
}, timer);
});
}
});
<body>
<div id="toast"></div>
</body>
// notifySend
$('#notifySendBtn').click(function(e){
let modal = $('.modal-content').has(e.target);
let type = '70';
let target = modal.find('.modal-body input').val();
let content = modal.find('.modal-body textarea').val();
let url = '${contextPath}/member/notify.do';
// 전송한 정보를 db에 저장
$.ajax({
type: 'post',
url: '${contextPath}/member/saveNotify.do',
dataType: 'text',
data: {
target: target,
content: content,
type: type,
url: url
},
success: function(){ // db전송 성공시 실시간 알림 전송
// 소켓에 전달되는 메시지
// 위에 기술한 EchoHandler에서 ,(comma)를 이용하여 분리시킨다.
socket.send("관리자,"+target+","+content+","+url);
}
});
modal.find('.modal-body textarea').val(''); // textarea 초기화
});
모달은 다시 설계해야겠다.
부트스트랩중 Varying modal content을 참고하면 좋을 것.
얼추 이정도로 하고! 플젝에서 실전으로 써먹을 때 글을 더 디벨롭하거나...머...그래야겠다. 후후.