ㅁ 퇴장하기 버튼을 눌러서 웹소켓 연결 해제를 먼저 해본다.
ㅁ room.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
.chat-area{height:500px; overflow: auto;}
.chat-message{margin:10px 0px;}
.chat-message.mine{display: flex; justify-content:flex-end;}
.chat-message .send-message{
padding: 5px 7px;
border-radius: 10px;
max-width: 190px;
font-size:0.9em;
white-space: pre-line;
}
.chat-message.other .send-message{background: lightgray;}
.chat-message.mine .send-message{background: #FFE08C;}
.chat-user {
text-align:center;
border-radius:10px;
background: lightgray;
opacity: 0.5;
margin: 20px 0px;
color: black;
line-height: 30px;
}
</style>
</head>
<body>
<div class="container p-3">
<!-- Header, Nav start -->
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<!-- Header, Nav end -->
<!-- Section start -->
<section class="row m-3" style="min-height: 500px">
<div class="container border p-5 m-4 rounded">
<h2 class="m-4">채팅방</h2>
<div class="chat">
<div class="chat-area">
<div class="chat-message mine">
<div class="send-message">내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지</div>
</div>
<div class="chat-message other">
<span class="send-user">상대방</span>
<div class="send-message">남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지</div>
</div>
<div class="chat-user entry">
xxx님이 들어왔습니다.
</div>
<div class="chat-user exit">
xxx님이 나갔습니다.
</div>
</div>
<div class="input-area">
<div class="form-group">
<textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
</div>
<button type="button" class="btn btn-sm btn-secondary btn-block" onclick="sendMessage();">전송하기</button>
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="onClose();">퇴장하기</button>
</div>
</div>
</div>
<script>
const sock = new SockJS("${contextPath}/chat"); // 이 구문이 웹소켓 서버와 연결이 되는 구문이다. 이 페이지에 진입한 클라이언트가.
// - 아까 servlet-context에 /chat url 요청시에 ChatEchoHandler가 동작되게 해놨다.
// 웹소켓 서버와 연결되는 순간 ChatEchoHandler가 동작된다고 보면 된다.
// after afterConnectionEstablished 메소드가 실행된다.
// 웹소켓 서버와 연결됨(즉, ChatEchoHandler의 afterConnectionEstablished 메소드가 실행된다.)
sock.onmessage = onMessage; // 웹소켓에서 해당 클라이언트로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)하는 구문
sock.onclose = onClose; // 웹소켓과 해당 클라이언트간의 연결이 끊겼을 경우 자동으로 실행할 함수를 지정(매핑)하는 구문
// 이렇게 3줄이 기본세팅이다.
// 메세지를 출력시키는 영역의 요소
const $chatArea = $(".chat-area");
// 메세지 전송시 실행될 함수
function sendMessage() {
}
// 나에게 메세지가 왔을 때 실행될 함수
function onMessage() {
}
// 퇴장시 실행될 함수
function onClose() {
location.href = "${contextPath}"; // 이 페이지를 빠져나가면
}
</script>
</section>
<!-- Section end -->
<!-- Footer start -->
<jsp:include page="/WEB-INF/views/common/footer.jsp" />
<!-- Footer end -->
</div>
</body>
</html>
- room.jsp에서 퇴장하기 버튼을 누르면 onClose 함수가 실행되게 했었다. (아래 주석말고 변경사항 없음)
onClose 함수는 메인페이지로 이동하게 한다.
이 페이지를 빠져나가면 ChatEchoHandler의 after
ㅁ ChatEchoHandler
package com.br.spring.handler;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {
/**
* 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
*
* @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
* // param이 매개변수에 대한 설명을 작성하는 키워드다.
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
// 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
}
// 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
}
// 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
}
}
ㅁ 서버 start
- 두 개 퇴장 해봤다.
===============================================================================
ㅁ ChatEchoHandler
package com.br.spring.handler;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {
// 웹소켓 세션 객체(클라이언트)들을 저장하는 리스트
private List<WebSocketSession> sessionList = new ArrayList<>();
/**
* 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
*
* @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
* // param이 매개변수에 대한 설명을 작성하는 키워드다.
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
// 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.add(session);
}
/**
* 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
*
* @param session - 현재 웹소켓으로 데이터를 전송한 클라이언트 객체
* @param message - 전송된 데이터에 대한 정보를 가지고 있는 객체
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
log.debug("====== 메세지 들어옴 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("WebSocketMessage 객체: {}", message);
log.debug("메세지 내용: {}", message.getPayload());
}
/**
* 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.remove(session);
}
}
- 전역 필드로 websocket 세션 객체를 여러개 보관시킬 수 있는 list형 필드를 둔다. Map으로 모아도 상관 없다.
- 이렇게 한곳에 모아놔야 메세지를 모두에게 재전송할 수 있다.
- 웹소켓에 연결된 클라이언트 들에게만 메세지를 전송한다. 그러려면 퇴장하면 리스트에서 제거해줘야 한다.
- 웹소켓 세션이 끊긴 클라이언트에게 sss.sendMessage(new TextMessage(msg));를 호출하면 예외가 발생한다.
구체적으로 IllegalStateException이 발생하며, 메시지는 보낼 수 없습니다.
ㅁ room.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
.chat-area{height:500px; overflow: auto;}
.chat-message{margin:10px 0px;}
.chat-message.mine{display: flex; justify-content:flex-end;}
.chat-message .send-message{
padding: 5px 7px;
border-radius: 10px;
max-width: 190px;
font-size:0.9em;
white-space: pre-line;
}
.chat-message.other .send-message{background: lightgray;}
.chat-message.mine .send-message{background: #FFE08C;}
.chat-user {
text-align:center;
border-radius:10px;
background: lightgray;
opacity: 0.5;
margin: 20px 0px;
color: black;
line-height: 30px;
}
</style>
</head>
<body>
<div class="container p-3">
<!-- Header, Nav start -->
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<!-- Header, Nav end -->
<!-- Section start -->
<section class="row m-3" style="min-height: 500px">
<div class="container border p-5 m-4 rounded">
<h2 class="m-4">채팅방</h2>
<div class="chat">
<div class="chat-area">
<div class="chat-message mine">
<div class="send-message">내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지</div>
</div>
<div class="chat-message other">
<span class="send-user">상대방</span>
<div class="send-message">남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지</div>
</div>
<div class="chat-user entry">
xxx님이 들어왔습니다.
</div>
<div class="chat-user exit">
xxx님이 나갔습니다.
</div>
</div>
<div class="input-area">
<div class="form-group">
<textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
</div>
<button type="button" class="btn btn-sm btn-secondary btn-block" onclick="sendMessage();">전송하기</button>
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="onClose();">퇴장하기</button>
</div>
</div>
</div>
<script>
const sock = new SockJS("${contextPath}/chat"); // 이 구문이 웹소켓 서버와 연결이 되는 구문이다. 이 페이지에 진입한 클라이언트가.
// - 아까 servlet-context에 /chat url 요청시에 ChatEchoHandler가 동작되게 해놨다.
// 웹소켓 서버와 연결되는 순간 ChatEchoHandler가 동작된다고 보면 된다.
// after afterConnectionEstablished 메소드가 실행된다.
// 웹소켓 서버와 연결됨(즉, ChatEchoHandler의 afterConnectionEstablished 메소드가 실행된다.)
sock.onmessage = onMessage; // 웹소켓에서 해당 클라이언트로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)하는 구문
sock.onclose = onClose; // 웹소켓과 해당 클라이언트간의 연결이 끊겼을 경우 자동으로 실행할 함수를 지정(매핑)하는 구문
// 이렇게 3줄이 기본세팅이다.
// 메세지를 출력시키는 영역의 요소
const $chatArea = $(".chat-area");
// 메세지 전송시 실행될 함수
function sendMessage() {
sock.send($("#message").val()); // 웹소켓 측으로 메세지를 전송 (ChatEchoHandler의 handleMessage 메소드 자동 실행)
$("#message").val("");
}
// 나에게 메세지가 왔을 때 실행될 함수
function onMessage() {
}
// 퇴장시 실행될 함수
function onClose() {
location.href = "${contextPath}"; // 이 페이지를 빠져나가면
}
</script>
</section>
<!-- Section end -->
<!-- Footer start -->
<jsp:include page="/WEB-INF/views/common/footer.jsp" />
<!-- Footer end -->
</div>
</body>
</html>
ㅁ 서버 start
- 브라우저 하나로만 메세지를 보내본다. 보내면 textarea는 비워진다.
===============================================================================
ㅁ 메세지 재발송시키기
- 웹소켓의 역할은 웹소켓에 메시지가 하나 들어오는 순간, 보낸 사람을 포함해 웹소켓에 연결되어있는 모든 클라이언트들에게 일괄적으로 그 메세지를 재전송한다.
ㅁ ChatEchoHandler
package com.br.spring.handler;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.br.spring.dto.MemberDto;
import lombok.extern.slf4j.Slf4j;
@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {
// 웹소켓 세션 객체(클라이언트)들을 저장하는 리스트
private List<WebSocketSession> sessionList = new ArrayList<>();
/**
* 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
*
* @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
* // param이 매개변수에 대한 설명을 작성하는 키워드다.
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
// 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.add(session);
}
/**
* 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
*
* @param session - 현재 웹소켓으로 데이터를 전송한 클라이언트 객체
* @param message - 전송된 데이터에 대한 정보를 가지고 있는 객체
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
/*
log.debug("====== 메세지 들어옴 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("WebSocketMessage 객체: {}", message);
log.debug("메세지 내용: {}", message.getPayload());
*/
// 현재 해당 웹소켓에 연결되어있는 모든 클라이언트들(작성자본인포함)에게 현재 들어온 메세지 재발송
for(WebSocketSession sss : sessionList) {
// 메세지유형(chat/entry/exit) | 채팅방에띄워주고자하는메세지내용 | 발신자아이디 | ...(프로필이미지경로 등) <- 나중에 |으로 split.
String msg = "chat|" + message.getPayload() + "|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId();
sss.sendMessage(new TextMessage(msg)); // 웹소켓에서 클라이언트로 메세지를 보냄. 그냥 보내면 안되고 TextMessage 객체로 보내야 한다.
// room.jsp에서 onMesage 함수가 자동 실행
}
}
/**
* 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.remove(session);
}
}
ㅁ room.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
.chat-area{height:500px; overflow: auto;}
.chat-message{margin:10px 0px;}
.chat-message.mine{display: flex; justify-content:flex-end;}
.chat-message .send-message{
padding: 5px 7px;
border-radius: 10px;
max-width: 190px;
font-size:0.9em;
white-space: pre-line;
}
.chat-message.other .send-message{background: lightgray;}
.chat-message.mine .send-message{background: #FFE08C;}
.chat-user {
text-align:center;
border-radius:10px;
background: lightgray;
opacity: 0.5;
margin: 20px 0px;
color: black;
line-height: 30px;
}
</style>
</head>
<body>
<div class="container p-3">
<!-- Header, Nav start -->
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<!-- Header, Nav end -->
<!-- Section start -->
<section class="row m-3" style="min-height: 500px">
<div class="container border p-5 m-4 rounded">
<h2 class="m-4">채팅방</h2>
<div class="chat">
<div class="chat-area">
<div class="chat-message mine">
<div class="send-message">내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지</div>
</div>
<div class="chat-message other">
<span class="send-user">상대방</span>
<div class="send-message">남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지</div>
</div>
<div class="chat-user entry">
xxx님이 들어왔습니다.
</div>
<div class="chat-user exit">
xxx님이 나갔습니다.
</div>
</div>
<div class="input-area">
<div class="form-group">
<textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
</div>
<button type="button" class="btn btn-sm btn-secondary btn-block" onclick="sendMessage();">전송하기</button>
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="onClose();">퇴장하기</button>
</div>
</div>
</div>
<script>
const sock = new SockJS("${contextPath}/chat"); // 이 구문이 웹소켓 서버와 연결이 되는 구문이다. 이 페이지에 진입한 클라이언트가.
// - 아까 servlet-context에 /chat url 요청시에 ChatEchoHandler가 동작되게 해놨다.
// 웹소켓 서버와 연결되는 순간 ChatEchoHandler가 동작된다고 보면 된다.
// after afterConnectionEstablished 메소드가 실행된다.
// 웹소켓 서버와 연결됨(즉, ChatEchoHandler의 afterConnectionEstablished 메소드가 실행된다.)
sock.onmessage = onMessage; // 웹소켓에서 해당 클라이언트로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)하는 구문
sock.onclose = onClose; // 웹소켓과 해당 클라이언트간의 연결이 끊겼을 경우 자동으로 실행할 함수를 지정(매핑)하는 구문
// 이렇게 3줄이 기본세팅이다.
// 메세지를 출력시키는 영역의 요소
const $chatArea = $(".chat-area");
// 메세지 전송시 실행될 함수
function sendMessage() {
sock.send($("#message").val()); // 웹소켓 측으로 메세지를 전송 (ChatEchoHandler의 handleMessage 메소드 자동 실행)
$("#message").val("");
}
// 나에게 메세지가 왔을 때 실행될 함수
function onMessage(evt) { // 웹소켓에서 클라이언트로 보내는 메세지를 받기 위해 매개변수를 둔다.
console.log("evt", evt);
console.log("evt.data", evt.data);
}
// 퇴장시 실행될 함수
function onClose() {
location.href = "${contextPath}"; // 이 페이지를 빠져나가면
}
</script>
</section>
<!-- Section end -->
<!-- Footer start -->
<jsp:include page="/WEB-INF/views/common/footer.jsp" />
<!-- Footer end -->
</div>
</body>
</html>
ㅁ 서버 start
- 브라우저 2개로 채팅방에 입장한다.
- admin01로 메세지 2개, user01로 메세지 2개를 전송한다.
=====================================================================================
ㅁ room.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
.chat-area{height:500px; overflow: auto;}
.chat-message{margin:10px 0px;}
.chat-message.mine{display: flex; justify-content:flex-end;}
.chat-message .send-message{
padding: 5px 7px;
border-radius: 10px;
max-width: 190px;
font-size:0.9em;
white-space: pre-line;
}
.chat-message.other .send-message{background: lightgray;}
.chat-message.mine .send-message{background: #FFE08C;}
.chat-user {
text-align:center;
border-radius:10px;
background: lightgray;
opacity: 0.5;
margin: 20px 0px;
color: black;
line-height: 30px;
}
</style>
</head>
<body>
<div class="container p-3">
<!-- Header, Nav start -->
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<!-- Header, Nav end -->
<!-- Section start -->
<section class="row m-3" style="min-height: 500px">
<div class="container border p-5 m-4 rounded">
<h2 class="m-4">채팅방</h2>
<div class="chat">
<div class="chat-area">
<!-- 다 지웠다. -->
</div>
<div class="input-area">
<div class="form-group">
<textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
</div>
<button type="button" class="btn btn-sm btn-secondary btn-block" onclick="sendMessage();">전송하기</button>
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="onClose();">퇴장하기</button>
</div>
</div>
</div>
<script>
const sock = new SockJS("${contextPath}/chat"); // 이 구문이 웹소켓 서버와 연결이 되는 구문이다. 이 페이지에 진입한 클라이언트가.
// - 아까 servlet-context에 /chat url 요청시에 ChatEchoHandler가 동작되게 해놨다.
// 웹소켓 서버와 연결되는 순간 ChatEchoHandler가 동작된다고 보면 된다.
// after afterConnectionEstablished 메소드가 실행된다.
// 웹소켓 서버와 연결됨(즉, ChatEchoHandler의 afterConnectionEstablished 메소드가 실행된다.)
sock.onmessage = onMessage; // 웹소켓에서 해당 클라이언트로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)하는 구문
sock.onclose = onClose; // 웹소켓과 해당 클라이언트간의 연결이 끊겼을 경우 자동으로 실행할 함수를 지정(매핑)하는 구문
// 이렇게 3줄이 기본세팅이다.
// 메세지를 출력시키는 영역의 요소
const $chatArea = $(".chat-area");
// 메세지 전송시 실행될 함수
function sendMessage() {
sock.send($("#message").val()); // 웹소켓 측으로 메세지를 전송 (ChatEchoHandler의 handleMessage 메소드 자동 실행)
$("#message").val("");
}
// 나에게 메세지가 왔을 때 실행될 함수
function onMessage(evt) { // 웹소켓에서 클라이언트로 보내는 메세지를 받기 위해 매개변수를 둔다.
// console.log("evt", evt);
// console.log("evt.data", evt.data);
let msgArr = evt.data.split("|"); // ["메세지유형(chat|entry|exit)", "출력시킬메세지내용", "발신자아이디"];
let $chatDiv = $("<div>"); // 채팅창에 append시킬 요소 (메세지 유형별로 다르게 제작)
// $("<div>")는 기본적으로 빈 <div></div> 요소를 만듭니다.
// jQuery 대신 순수 JavaScript로 <div> 요소를 생성하고 설정하려면 let chatDiv = document.createElement("div");
if(msgArr[0] == "chat"){ // 채팅메세지일 경우
$chatDiv.addClass("chat-message")
.addClass(msgArr[2] == "${loginUser.userId}" ? "mine" : "other") // 제이쿼리가 좋은게 메소드체이닝이 가능
.append( $("<div>").addClass("send-message").text(msgArr[1]) ); // innerText로 추가하고 자손으로 추가
// 여기가진 내가 보낸 메세지든 남이 보낸 메세지든 같다.
if($chatDiv.hasClass("other")) { // class 관련해선 add remove has 3개가 있다.
$chatDiv.prepend( $("<span>").addClass("send-user").text(msgArr[2]) );
}
// short-circuit으로 단일 if문 대체도 가능. 앞이 true면 뒤가 실행.
// $chatDiv.hasClass("other") && $chatDiv.prepend( $("<span>").addClass("send-user").text(msgArr[2]) );
}else { // 입장 또는 퇴장 메세지일 경우
$chatDiv.addClass("chat-user")
.addClass(msgArr[0])
.text(msgArr[1]);
}
// 근데 계속 메세지가 추가되어도 맨 위의 메세지만 보여진다. 스크롤바가 생기고 하단으로 고정되려면.
$chatArea.append($chatDiv);
$chatArea.scrollTop( $chatArea[0].scrollHeight ); // 스크롤을 항상 하단으로 유지시켜주는 코드.
}
// 퇴장시 실행될 함수
function onClose() {
location.href = "${contextPath}"; // 이 페이지를 빠져나가면
}
</script>
</section>
<!-- Section end -->
<!-- Footer start -->
<jsp:include page="/WEB-INF/views/common/footer.jsp" />
<!-- Footer end -->
</div>
</body>
</html>
- onMessage 함수에 내용 작성.
ㅁ ChatEchoHandler
package com.br.spring.handler;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.br.spring.dto.MemberDto;
import lombok.extern.slf4j.Slf4j;
@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {
// 웹소켓 세션 객체(클라이언트)들을 저장하는 리스트
private List<WebSocketSession> sessionList = new ArrayList<>();
/**
* 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
*
* @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
* // param이 매개변수에 대한 설명을 작성하는 키워드다.
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
// 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.add(session);
for(WebSocketSession sss : sessionList) {
String msg = "entry|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 입장하였습니다.";
sss.sendMessage(new TextMessage(msg));
}
}
/**
* 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
*
* @param session - 현재 웹소켓으로 데이터를 전송한 클라이언트 객체
* @param message - 전송된 데이터에 대한 정보를 가지고 있는 객체
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
/*
log.debug("====== 메세지 들어옴 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("WebSocketMessage 객체: {}", message);
log.debug("메세지 내용: {}", message.getPayload());
*/
// 현재 해당 웹소켓에 연결되어있는 모든 클라이언트들(작성자본인포함)에게 현재 들어온 메세지 재발송
for(WebSocketSession sss : sessionList) {
// 메세지유형(chat/entry/exit) | 채팅방에띄워주고자하는메세지내용 | 발신자아이디 | ...(프로필이미지경로 등) <- 나중에 |으로 split.
String msg = "chat|" + message.getPayload() + "|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId();
sss.sendMessage(new TextMessage(msg)); // 웹소켓에서 클라이언트로 메세지를 보냄. 그냥 보내면 안되고 TextMessage 객체로 보내야 한다.
// room.jsp에서 onMesage 함수가 자동 실행
}
}
/**
* 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.remove(session);
for(WebSocketSession sss : sessionList) {
String msg = "exit|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 퇴장하였습니다.";
sss.sendMessage(new TextMessage(msg));
}
}
}
- afterConnectionEstablished 메소드에서도 메세지를 보낼 수 있다.
afterConnectionClosed 메소드에서도 메세지를 보낼 수 있다.
※
scrollHeight와 scrollTop은 JavaScript에서 스크롤 위치와 스크롤 가능한 전체 높이를 조작하는 데 사용되는 속성입니다. 채팅창과 같은 요소에서 스크롤을 맨 아래로 자동으로 유지할 때 자주 사용됩니다.
- scrollHeight
- 요소 내부에 스크롤이 가능한 전체 콘텐츠의 높이를 의미합니다.
- 예를 들어, 채팅창에 많은 메시지가 쌓이면 scrollHeight는 메시지를 모두 포함한 높이가 됩니다.
- 이 값은 스크롤바가 포함된 전체 콘텐츠 높이로, 채팅 내용이 많아질수록 scrollHeight 값도 커집니다.
- scrollTop
- 스크롤바가 현재 위치한 곳을 의미하며, 스크롤을 조작하기 위해 값을 설정할 수도 있습니다.
- scrollTop 값을 scrollHeight 값으로 설정하면, 스크롤바가 요소의 맨 아래로 이동합니다.
- 예를 들어 chatArea.scrollTop = chatArea.scrollHeight;라고 설정하면 스크롤이 항상 맨 아래를 가리키게 됩니다.
ㅁ 서버 start
- 3개로
- db에 기록을 해둬야 나중에 다시 왔을 때 대화내용이 보여진다.
어딘가에 기록해둬야 한다. db에 insert해두면 나중에 이 방에 접속했을 때 db로부터 그때 나눴던 채팅 메시지 내역을 조회해와서 뿌릴 수 있다.
db에 기록하느 ㄴ시점은 handleMessage 누군가가 메세지를 전송하ㅐㅆ을때실행되는 메소드. 여기서 db에 기록하면 된다.
- cmd에서 ipconfig 하고 다른사람이 서버키면 들어갈 수 있따.
연결별 DNS 접미사. . . . :
IPv6 주소 . . . . . . . . . : fdc4:74d5:3e94:947e:158e:ecb7:74ed:68eb
임시 IPv6 주소. . . . . . . : fdc4:74d5:3e94:947e:f190:d811:233a:fba3
링크-로컬 IPv6 주소 . . . . : fe80::299e:7730:401a:9ce6%18
IPv4 주소 . . . . . . . . . : 192.168.10.33
서브넷 마스크 . . . . . . . : 255.255.255.0
기본 게이트웨이 . . . . . . : 192.168.10.1
http://localhost:8888/spring/chat/room.do 대신
http://192.168.10.33:8888/spring/chat/room.do
========================================================================================
ㅁ room.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
.chat-area{height:500px; overflow: auto;}
.chat-message{margin:10px 0px;}
.chat-message.mine{display: flex; justify-content:flex-end;}
.chat-message .send-message{
padding: 5px 7px;
border-radius: 10px;
max-width: 190px;
font-size:0.9em;
white-space: pre-line;
}
.chat-message.other .send-message{background: lightgray;}
.chat-message.mine .send-message{background: #FFE08C;}
.chat-user {
text-align:center;
border-radius:10px;
background: lightgray;
opacity: 0.5;
margin: 20px 0px;
color: black;
line-height: 30px;
}
</style>
</head>
<body>
<div class="container p-3">
<!-- Header, Nav start -->
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<!-- Header, Nav end -->
<!-- Section start -->
<section class="row m-3" style="min-height: 500px">
<div class="container border p-5 m-4 rounded">
<h2 class="m-4">채팅방</h2>
<div class="chat">
<div class="chat-area">
<!--
<div class="chat-message mine">
<div class="send-message">내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지</div>
</div>
<div class="chat-message other">
<span class="send-user">상대방</span>
<div class="send-message">남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지</div>
</div>
-->
</div>
<div class="input-area">
<div class="form-group">
<textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
</div>
<button type="button" class="btn btn-sm btn-secondary btn-block" onclick="sendMessage();">전송하기</button>
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="onClose();">퇴장하기</button>
</div>
</div>
</div>
<script>
const sock = new SockJS("${contextPath}/chat"); // 이 구문이 웹소켓 서버와 연결이 되는 구문이다. 이 페이지에 진입한 클라이언트가.
// - 아까 servlet-context에 /chat url 요청시에 ChatEchoHandler가 동작되게 해놨다.
// 웹소켓 서버와 연결되는 순간 ChatEchoHandler가 동작된다고 보면 된다.
// after afterConnectionEstablished 메소드가 실행된다.
// 웹소켓 서버와 연결됨(즉, ChatEchoHandler의 afterConnectionEstablished 메소드가 실행된다.)
sock.onmessage = onMessage; // 웹소켓에서 해당 클라이언트로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)하는 구문
sock.onclose = onClose; // 웹소켓과 해당 클라이언트간의 연결이 끊겼을 경우 자동으로 실행할 함수를 지정(매핑)하는 구문
// 이렇게 3줄이 기본세팅이다.
// 메세지를 출력시키는 영역의 요소
const $chatArea = $(".chat-area");
// 메세지 전송시 실행될 함수
function sendMessage() {
sock.send($("#message").val()); // 웹소켓 측으로 메세지를 전송 (ChatEchoHandler의 handleMessage 메소드 자동 실행)
$("#message").val("");
}
// 나에게 메세지가 왔을 때 실행될 함수
function onMessage(evt) { // 매개볁수를 하나 둬야한다. 웹소켓에서 클라이언트로 보내는 메세지를 받기 위해
// console.log("evt", evt);
// console.log("evt.data", evt.data);
let msgArr = evt.data.split("|"); // ["메세지유형(chat|entry|exit)", "출력시킬메세지내용", "발신자아이디"];
let $chatDiv = $("<div>"); // 채팅창에 append시킬 요소 (메세지 유형별로 다르게 제작)
// $("<div>")는 기본적으로 빈 <div></div> 요소를 만듭니다.
// jQuery 대신 순수 JavaScript로 <div> 요소를 생성하고 설정하려면 let chatDiv = document.createElement("div");
if(msgArr[0] == "chat"){ // 채팅메세지일 경우
$chatDiv.addClass("chat-message")
.addClass(msgArr[2] == "${loginUser.userId}" ? "mine" : "other") // 제이쿼리가 좋은게 메소드체이닝이 가능
.append( $("<div>").addClass("send-message").text(msgArr[1]) ); // innerText로 추가하고 자손으로 추가
// 여기가진 내가 보낸 메세지든 남이 보낸 메세지든 같다.
if($chatDiv.hasClass("other")) { // class 관련해선 add remove has 3개가 있다.
$chatDiv.prepend( $("<span>").addClass("send-user").text(msgArr[2]) );
}
// short-circuit으로 단일 if문 대체도 가능. 앞이 true면 뒤가 실행.
// $chatDiv.hasClass("other") && $chatDiv.prepend( $("<span>").addClass("send-user").text(msgArr[2]) );
}else { // 입장 또는 퇴장 메세지일 경우
$chatDiv.addClass("chat-user")
.addClass(msgArr[0])
.text(msgArr[1]);
}
// 근데 계속 메세지가 추가되어도 맨 위의 메세지만 보여진다. 스크롤바가 생기고 하단으로 고정되려면.
$chatArea.append($chatDiv);
$chatArea.scrollTop( $chatArea[0].scrollHeight ); // 스크롤을 항상 하단으로 유지시켜주는 코드.
}
// 퇴장시 실행될 함수
function onClose() {
location.href = "${contextPath}"; // 이 페이지를 빠져나가면
}
$(document).ready(function(){
// enter 눌렀을 때 메세지 전송, shift+enter 눌렀을 때 줄바꿈 적용
$("#message").on("keydown", function(evt){
if(evt.keyCode == 13){
if(!evt.shiftKey){
evt.preventDefault();
sendMessage(); // 메세지 전송시키는 함수
}
}
})
})
</script>
</section>
<!-- Section end -->
<!-- Footer start -->
<jsp:include page="/WEB-INF/views/common/footer.jsp" />
<!-- Footer end -->
</div>
</body>
</html>
- textarea에서 엔터치면 메세지 가야하고 줄바꿈하면 쉬프트 하고 엔터.
그러려면 이 코드 추가.
ㅁ ChatEchoHandler
package com.br.spring.handler;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.br.spring.dto.MemberDto;
import lombok.extern.slf4j.Slf4j;
@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {
// 웹소켓 세션 객체(클라이언트)들을 저장하는 리스트
private List<WebSocketSession> sessionList = new ArrayList<>();
/**
* 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
*
* @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
* // param이 매개변수에 대한 설명을 작성하는 키워드다.
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
// 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.add(session);
for(WebSocketSession sss : sessionList) {
String msg = "entry|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 입장하였습니다.";
sss.sendMessage(new TextMessage(msg));
}
}
/**
* 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
*
* @param session - 현재 웹소켓으로 데이터를 전송한 클라이언트 객체
* @param message - 전송된 데이터에 대한 정보를 가지고 있는 객체
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
/*
log.debug("====== 메세지 들어옴 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("WebSocketMessage 객체: {}", message);
log.debug("메세지 내용: {}", message.getPayload());
*/
// 현재 해당 웹소켓에 연결되어있는 모든 클라이언트들(작성자본인포함)에게 현재 들어온 메세지 재발송
for(WebSocketSession sss : sessionList) {
// 메세지유형(chat/entry/exit) | 채팅방에띄워주고자하는메세지내용 | 발신자아이디 | ...(프로필이미지경로 등) <- 나중에 |으로 split.
String msg = "chat|" + message.getPayload() + "|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId();
sss.sendMessage(new TextMessage(msg)); // 웹소켓에서 클라이언트로 메세지를 보냄. 그냥 보내면 안되고 TextMessage 객체로 보내야 한다.
// room.jsp에서 onMesage 함수가 자동 실행
}
// 특정 회원과 채팅방을 만든다거나 해당 회원과 나눈 채팅 내역을 보존하려면 메세지를 발송할 때 마다 db에 기록해야 한다.
// 그때 실행되는 메소드가 handleMessage 메소드다.
// insert 해주는 서비스측 메소드를 여기서 실행시키면 된다.
// 해당 클래스에 Service 클래스를 DI(의존성주입)해서 채팅메세지를 insert하는 메소드를 여기서 실행하면 됨.
}
/**
* 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
/*
log.debug("====== websocket 연결됨 =====");
log.debug("WebSocketSession 객체: {}", session);
log.debug("session id: {}", session.getId());
log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
*/
sessionList.remove(session);
for(WebSocketSession sss : sessionList) {
String msg = "exit|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 퇴장하였습니다.";
sss.sendMessage(new TextMessage(msg));
}
}
}
- db에 기록하려면 handleMessage 메소드에 추가하면 된다.
- 메세지를 DB에 저장하는 실제 로직을 수행하는 ChatService라는 클래스를 @Service 어노테이션과 함께 별도로 작성합니다.
- ChatEchoHandler 클래스에서 이 ChatService를 필드로 선언하고, DI를 위해 @Autowired 어노테이션을 붙입니다.
- ChatService를 @Service로 등록하고, ChatEchoHandler에서 @Autowired를 통해 chatService 필드를 주입받아 사용하면 됩니다.