** 준비 - 로그인 구현
PageController
@Controller
@RequestMapping("/page")
public class PageController {
// 의존성 주입
@Autowired
private SqlSession sqlSession;
// 홈 Mapping
@GetMapping("/home")
public String home() {
return "home";
}
// 로그인 Mapping
@PostMapping("/login")
public String login(@ModelAttribute MemberDto memberDto, HttpSession session) {
// 입력받은 회원 아아디로 단일 조회
MemberDto findDto = sqlSession.selectOne("member.get", memberDto.getMemberId());
// 입력한 아이디를 갖는 회원이 없을 경우
if(findDto == null) {
// 홈 Mapping으로 강제 이동(redirect)
return "redirect:home";
}
// 입력한 비밀번호가 조회한 회원 정보의 비밀번호와 일치하는지 여부
boolean judge = memberDto.getMemberPw().equals(findDto.getMemberPw());
// 입력한 비밀번호가 조회한 회원 정보의 비밀번호와 일치한다면
if(judge) {
// HttpSession에 해당 회원의 아이디, 닉네임, 회원등급 저장
session.setAttribute("loginId", findDto.getMemberId());
session.setAttribute("loginNick", findDto.getMemberNick());
session.setAttribute("loginAuth", findDto.getMemberGrade());
}
// 홈 Mapping으로 강제 이동(redirect)
return "redirect:home";
}
// 로그아웃 Mapping
@GetMapping("/logout")
public String logout(HttpSession session) {
// 세션 무효화
session.invalidate();
// 홈 페이지(home.jsp)로 연결
return "redirect:home";
}
}
home.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri = "http://java.sun.com/jsp/jstl/core" %>
<h1>웹소켓 예제 모음</h1>
<!-- 로그인/로그아웃 화면 -->
<c:choose>
<c:when test = "${loginId == null}">
<form action = "login" method = "post">
<input type = "text" name = "memberId">
<input type = "password" name = "memberPw">
<button type = "submit">로그인</button>
</form>
</c:when>
<c:otherwise>
<h2><a href = "logout">로그아웃</a></h2>
</c:otherwise>
</c:choose>
<!-- 상태 판정 출력 -->
<h2>아이디 : ${loginId}</h2>
<h2>닉네임 : ${loginNick}</h2>
<h2>권한 : ${loginAuth}</h2>
<!-- 모든 예제로 이동할 수 있는 링크 -->
<h2><a href = "basic">기본 연결</a></h2>
<h2><a href = "multi">다중 사용자</a></h2>
<h2><a href = "message">텍스트 메시지</a></h2>
<h2><a href = "json">JSON 메시지</a></h2>
<h2><a href = "sockjs">SockJS</a></h2>
웹소켓을 이용한 실시간 채팅 구현(4) - 로그인한 회원 정보 표시
** HttpSessionHandshakeInterceptor의 동작
- HttpSession에 저장된 값을 WebSocketSession으로 넘겨준다
MemberWebSocketServer
@Slf4j
@Service
public class MemberWebsocketServer extends TextWebSocketHandler {
// 웹소켓 사용자 저장소 - Set(중복 방지)
private Set<WebSocketSession> users = new CopyOnWriteArraySet<>(); // 동기화된 Set
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// HttpSessionHandshakeInterceptor의 동작을 확인하기 위한 로그 출력
log.debug("session = {}", session.getAttributes());
// HttpSessionHandshakeInterceptor에 의해 HttpSession에서 WebSocketSession로 가져온 값을 Map 형태로 반환
Map<String, Object> attributes = session.getAttributes();
// Map에 저장된 값 반환
String loginId = (String)attributes.get("loginId");
String loginNick = (String)attributes.get("loginNick");
String loginAuth = (String)attributes.get("loginAuth");
// Map에서 반환한 값 확인
log.debug("{}, {}, {}", loginId, loginNick, loginAuth);
// 사용자 저장소에 해당 사용자 추가
users.add(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// WebSocketSession의 정보 출력
log.debug("session = {}", session);
// 사용자 저장소에서 해당 사용자 삭제
users.remove(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 로그인한 회원의 로그인 정보 반환
Map<String, Object> attributes = session.getAttributes();
String loginId = (String)attributes.get("loginId");
String loginNick = (String)attributes.get("loginNick");
String loginAuth = (String)attributes.get("loginAuth");
// 반환된 정보를 확인하기 위한 로그 출력
log.debug("{}, {}, {}", loginId, loginNick, loginAuth);
// 회원인지(로그인한 상태인지) 판정
boolean available = loginAuth != null;
// 비회원이라면
if(available == false) {
log.warn("비회원 채팅 금지");
return;
}
// 회원이 보낸 메시지의 전송 데이터 확인을 위한 로그 출력
log.debug("메세지 - {}", message.getPayload());
// ObjectMapper의 인스턴스 생성
ObjectMapper mapper = new ObjectMapper();
// 1. JSON을 Java Object로 변환 - readValue()
// 1) JSON을 Map 형태로 변환
//Map json = mapper.readValue(message.getPayload(), Map.class);
//log.debug("json = {}", json);
// 2) JSON을 클래스의 인스턴스 형태로 변환
MessageVO json = mapper.readValue(message.getPayload(), MessageVO.class);
log.debug("json = {}", json);
// VO에 시간 설정
json.setTime(new Date());
// VO에 로그인 정보(id, nickname, auth) 설정
json.setId(loginId);
json.setNickname(loginNick);
json.setAuth(loginAuth);
// 2. Java Object를 JSON 형태로 변환 - writeValue()
// 설정된 VO를 JSON 형태의 문자열로 변환
String payload = mapper.writeValueAsString(json);
// JSON 형태의 문자열로 메시지 생성
TextMessage jsonMessage = new TextMessage(payload);
// 연결된 모든 사용자에게 메시지 전송
for(WebSocketSession user : users) {
//user.sendMessage(json);
user.sendMessage(jsonMessage);
}
}
}
WebSocketServerConfiguration
- HttpSessionHandshakeInterceptor는 HttpSession의 값을 WebSocketSession로 넘겨준다
@Configuration
@EnableWebSocket // 웹소켓 활성화
public class WebSocketServerConfiguration implements WebSocketConfigurer {
// 의존성 주입
@Autowired
private MemberWebsocketServer memberWebsocketServer;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// HttpSessionHandshakeInterceptor는 HttpSession을 WebSocketSession으로 넘겨준다
registry.addHandler(memberWebsocketServer, "/ws/member")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}
}
PageController
@Controller
@RequestMapping("/page")
public class PageController {
@GetMapping("/member")
public String member() {
return "member";
}
}
member.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<style>
.chat-message {
padding: 1em;
border: 1px solid black;
border-radius : 1em;
}
</style>
<h1>회원 채팅 예제</h1>
<button class="btn-connect">연결</button>
<button class="btn-disconnect">종료</button>
<hr>
<input type="text" id="message-input">
<button type="button" id="message-send">전송</button>
<hr>
<div id="message-list"></div>
<!-- Moment CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<!-- SockJS CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
<!-- JQuery CDN -->
<script src="https://code.jquery.com/jquery-3.6.1.js"></script>
<script>
$(function(){
// 초기 웹소켓은 연결 해제 상태
disconnectState();
// 1. 연결버튼을 누를 때 - 웹소켓 연결 생성
$(".btn-connect").click(function(){
// 웹소켓 연결 주소 설정
// - SockJS를 사용하면 주소를 http로 시작하도록 할 수 있다
var uri = "${pageContext.request.contextPath}/ws/member";
// 웹소켓 연결 생성
// - 접속시 WebSocket이 아닌 SockJS 객체를 생성
socket = new SockJS(uri); // 변수로 만들지 말고 window에 붙여서 만듬
// 웹소켓에 대한 이벤트 설정
// 1) 웹소켓 연결이 생성될 때
socket.onopen = function(){
console.log("open");
connectState(); // 연결 상태일 때 connectState() 실행
};
// 2) 웹소켓 연결 중 연결이 해제될 때
socket.onclose = function(){
console.log("close");
disconnectState(); // 해제 상태일 때 disconnectState() 실행
};
// 3) 웹소켓 연결 중 연결에 에러가 발생할 때
socket.onerror = function(){
console.log("error");
disconnectState(); // 에러 상태일 때 disconnectState() 실행
};
// 4) 웹소켓 연결 중 메시지가 수신될 때
socket.onmessage = function(e){
// 문자열을 JSON 형태로 수신 - JSON.parse()
var data = JSON.parse(e.data);
//console.log(data);
// p 태그 지정 - CSS에서 디자인을 하기 위해 클래스 부여
var p = $("<p>").addClass("chat-message");
// p 태그 지정 - 닉네임[등급] 형태
var w = $("<p>").text(data.nickname + "[" + data.auth + "]");
// 수신한 JSON의 time 형태 변경
var time = moment(data.time).format("YYYY-MM-DD hh:mm");
// p 태그 지정 - (시간) 형태
var t = $("<p>").text("(" + time + ")");
// p 태그 지정 - 메시지 표시
var c = $("<p>").text(data.text);
// 지정된 p 태그들을 연결
p.append(w).append(c).append(t);
// 메시지를 표시하는 div 안에 연결된 p 태그 생성
$("#message-list").append(p);
// 스크롤 하단으로 이동
var height = $(document).height();
$(window).scrollTop(height);
};
});
// 2. 종료 버튼을 누를 때 - 웹소켓 연결 해제
$(".btn-disconnect").click(function(){
//웹소켓 연결 종료
//window.socket.close();
socket.close();
});
// 3. 전송 버튼을 누를 때 - 웹소켓에 연결된 모든 사용자에게 메시지 전달
$("#message-send").click(function(){
// 입력창의 값 지정
var text = $("#message-input").val();
// 입력창에 값이 입력되지 않았을 경우 return
if(text.length == 0) return;
// JSON으로 변환해서 전송
// - JSON.stringify(객체) : 객체를 문자열로
// - JSON.parse(문자열) : 문자열을 객체로
// 입력값을 JSON 형태로 설정
var data = {
text : text
};
// 입력창의 값 전송
socket.send(JSON.stringify(data));
// 전송 후 입력창 초기화(비우기)
$("#message-input").val("");
});
// 웹소켓 연결이 만들어진 상태일 때
function connectState(){
$(".btn-connect").prop("disabled", true); // 연결버튼 비활성화
$(".btn-disconnect").prop("disabled", false); // 종료버튼 비활성화 해제
}
// 웹소켓 연결이 해제된 상태일 때
function disconnectState(){
$(".btn-connect").prop("disabled", false); // 연결버튼 비활성화 해제
$(".btn-disconnect").prop("disabled", true); // 종료버튼 비활성화
}
});
</script>
웹소켓을 이용한 실시간 채팅 구현(5) - 그룹 채팅
그룹 채팅의 구조
User
- 웹소켓에 연결된 모든 사용자는 WebSocketSession을 가지고 있다
- 웹소켓 서버에서 수신한 메시지를 특정 사용자에게 보내는 메소드 존재
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
// 사용자 정보
private String memberId; // 사용자 아이디
private String memberNick; // 사용자 닉네임
private String memberGrade; // 사용자 등급
private WebSocketSession session; // 사용자의 WebSocketSession
// 사용자 아이디 비교
public boolean is(String memberId) {
// 매개변수로 입력된 memberId가 null이면 false를 반환
if(memberId == null) return false;
// User 클래스의 인스턴스의 memberId 값이 null이면 false를 반환
if(this.memberId == null) return false;
// User 클래스의 인스턴스의 memberId 값이 매개변수로 입력된 memberId와 같은지 판정
// - 특정 방에 참가하고있는 모든 참가자 중 해당 아이디를 가진 참가자 검색을 위함
return this.memberId.equals(memberId);
}
// 메시지 전송
// - 웹소켓 연결을 통해 웹소켓 서버가 수신한 메시지를 사용자에게 전송
public void send(TextMessage message) throws IOException {
session.sendMessage(message);
}
}
Room
- User 객체를 저장하는 방 참가자 저장소 존재
- enter(User user) : 사용자가 방 입장시 방 참가 저장소에 해당 사용자의 User 객체 저장
- leave(User user) : 사용자가 방 퇴장시 방 참가 저장소에서 해당 사용자의 User 객체 제거
- broadcast(TextMessage message) : 웹소켓 서버에서 수신한 메시지를 특정 방에 참가한 모든 사용자에게 전송
(방 참가 저장소에 메시지를 보낸 회원의 User 객체가 포함된 방의 이름을 찾아 해당 방의 모든 참가자에게 메시지 전송)
@Slf4j
public class Room {
// 방 참가자 저장소 - Set (중복 방지)
// - 특정 방에 참가한 모든 사용자의 User 클래스의 인스턴스 저장
private Set<User> users = new CopyOnWriteArraySet<>();
// 방 입장
public void enter(User user) {
// 방 참가자 저장소에 해당 사용자를 추가
users.add(user);
log.debug("--> 방 입장 : {}", user);
}
// 방 퇴장
public void leave(User user) {
// 방 참가자 저장소에서 해당 사용자를 제거
users.remove(user);
log.debug("--> 방 퇴장 : {}", user);
}
// 방 참가자 중 특정 아이디를 가진 사용자가 존재하는지 여부 반환
public User search(String memberId) {
for(User user : users) { // 방 참가자 저장소에 아이디가 저장된 모든 사용자에 대해
// 방 참가자 저장소에 입력된 memberId를 아이디로 하는 사용자이 있으면
if(user.is(memberId)) return user; // 해당 사용자의 User 클래스의 인스턴스 반환
}
// 기본적으로 null을 반환
return null;
}
// 방 참가자 수 반환
public int size() {
// 방 참가자 저장소의 길이(참가자 수) 반환
return users.size();
}
// 메시지 전송
// - 웹소켓 연결을 통해 웹소켓 서버에서 방 참가자 저장소에 아이디가 저장된 모든 사용자에게 메시지 전송
public void broadcast(TextMessage message) throws IOException {
for(User user : users) { // 방 참가자 저장소에 아이디가 저장된 모든 사용자에 대해
user.send(message); // 웹소켓 서버가 수신한 메시지를 전송
}
}
}
Channel
- Room 객체를 저장하는 방 저장소 존재
- join(User user, String name) : 특정 이름(name)의 Room 객체를 반환하여 Room 클래스의 enter(User user) 메소드 실행
- exit(User user, String name) : 특정 이름(name)의 Room 객체를 반환하여 Room 클래스의 leave(User user) 메소드 실행
- send(User user, TextMessage message) : 메시지를 전송한 사용자가 포함된 방 이름을 반환하여 해당 방의 방 저장소에서 해당 방의 Room 객체를 반환한 후 Room 클래스의 broadcast(TextMessage message) 메소드 실행
public class Channel {
// 방 저장소
// - 방 이름으로 Room 클래스의 인스턴스를 관리하는 저장소
// - 동기화된 Map 사용
Map<String, Room> rooms = Collections.synchronizedMap(new HashMap<>());
// 방 입장
public void join(User user, String name) {
if(!rooms.containsKey(name)) { // 해당 이름(name)의 방이 없으면
rooms.put(name, new Room()); // 해당 이름으로 방을 생성
}
// 특정 이름(name)의 방에 특정 사용자(user) 입장
// - rooms.get(name)까지의 결과로 해당 이름(name)의 Room 클래스의 인스턴스가 반환된다
rooms.get(name).enter(user);
}
// 방 이름 반환
// - 특정 사용자가 참가하고 있는 방의 이름 반환
public String find(User user) {
// 방 저장소의 Key인으로 다음 작업을 수행
for(String name : rooms.keySet()) { // keySet() - Map의 Key만 반환
// 방 저장소에서 Room 클래스의 인스턴스를 반환
Room room = rooms.get(name);
// 반환한 인스턴스의 방 참가자 저장소에서 특정 아이디를 가진 참가자가 존재하면
if(room.search(user.getMemberId()) != null) {
// 해당 방의 이름을 반환
return name;
}
}
// 기본적으로 null을 반환
return null;
}
// 방 퇴장
public void exit(User user) {
// 특정 사용자가 참가하고 있는 방의 이름을 반환
String name = this.find(user);
// 해당 이름의 방에서 퇴장
// - rooms.get(name)의 결과로 해당 이름(name)을 가진 Room 클래스의 인스턴스 반환
rooms.get(name).leave(user);
// 해당 이름의 방에 참가하고 있는 참가자 수가 0인 경우
if(rooms.get(name).size() == 0) {
// 해당 이름의 방 제거
rooms.remove(name);
}
}
// 특정 방의 참가자 전체에게만 메시지 전송
public void send(User user, TextMessage message) throws IOException {
// 특정 사용자가 참가하고 있는 방의 이름을 반환
String name = this.find(user);
// 참가하고 있는 방이 있다면 (name이 null이 아닌 경우)
if(name != null) {
// 해당 이름의 방에 메시지 전송
// - rooms.get(name)의 결과로 해당 이름(name)을 가진 Room 클래스의 인스턴스 반환
// - broadcast(message) 메소드는 해당 방 참가자 저장소에 아이디가 저장된 모든 사용자에게 메시지 전송
rooms.get(name).broadcast(message);
}
}
}
MemberGroupChatServer
- 웹소켓 서버에는 기본적으로 WaitingRoom과 Channel 객체가 존재
- WaitingRoom 객체는 신규로 웹소켓 연결이 생성된 사용자의 User 객체를 일시적으로 저장
- Channel 객체는 Room 객체를 저장
1) 웹소켓 연결 생성시
- 웹소켓 연결이 생성되면 신규로 연결된 사용자의 WebSocketSession을 이용하여 User 객체 생성
- User 객체 생성 후 WaitingRoom의 방 참가자 저장소에 User 객체를 저장
2) 웹소켓 연결 해제시
- 웹소켓 연결이 해제되면 해제된 사용자의 WebSocketSession을 이용하여 User 객체 생성
- User 객체를 통해 Channel에 포함된 Room 및 WaitingRoom의 방 참가자 저장소에서 해당 User 객체를 제거
3) 웹소켓 서버로 메시지 전송시
- 사용자가 특정 방 참가를 위한 메시지인지, 특정 방에 메시지를 전송하기 위한 메시지인지 판정
- 방 참가를 위한 메시지인 경우 해당 사용자의 User 객체를 WaitingRoom의 방 참가자 저장소에서 제거하고
참가하려는 방의 방 참가자 저장소에 추가
- 특정 방에 메시지 전송을 위한 메시지인 경우 메시지에 필요한 정보를 추가하여 웹소켓 서버에서
해당 방에 참가한 모든 사용자에게 메시지 전송
@Slf4j
@Service
public class MemberGroupChatServer extends TextWebSocketHandler {
// 대기실
// - 회원의 웹소켓 연결이 생성되면 대기실 참가자 저장소에 해당 회원을 참가자로서 저장
private Room waitingRoom = new Room();
// 채널
// - 방들의 이름이 저장된 저장소를 포함
private Channel channel = new Channel();
// 웹소켓 연결 신규 생성시
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// WebSocketSession에 저장된 값을 Map 형태로 반환
// - 웹소켓 연결이 생성된 회원의 아이디, 닉네임, 등급을 Key-Value 형태로 반환
Map<String, Object> attr = session.getAttributes();
// 웹소켓 연결이 생성된 사용자의 WebSocketSession에 저장된 값으로 User 클래스의 인스턴스 생성
User user = User.builder()
.memberId((String)attr.get("loginId"))
.memberNick((String)attr.get("loginNick"))
.memberGrade((String)attr.get("loginAuth"))
.session(session)
.build();
// 신규 참가자 대기실 입장
// - 웹소켓 연결이 생성된 회원의 User 클래스의 인스턴스를 Room 클래스의 방 참가자 저장소인 Set<User> users에 추가
waitingRoom.enter(user);
// 대기실 인원 확인을 위한 로그 출력
log.debug("대기실 입장 - 현재 {}명", waitingRoom.size());
}
// 웹소켓 연결 해제시
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// WebSocketSession에 저장된 값을 Map 형태로 반환
// - 웹소켓 연결이 생성된 회원의 아이디, 닉네임, 등급을 Key-Value 형태로 반환
Map<String, Object> attr = session.getAttributes();
// 웹소켓 연결이 생성된 사용자의 WebSocketSession에 저장된 값으로 User 클래스의 인스턴스 생성
User user = User.builder()
.memberId((String)attr.get("loginId"))
.memberNick((String)attr.get("loginNick"))
.memberGrade((String)attr.get("loginAuth"))
.session(session)
.build();
// 참가자 채널 퇴장
// - 웹소켓 연결이 해제된 회원이 참가하고있던 방 이름을 찾아 해당 방의 방 참가자 저장소인 Set<User> users에서 해당 회원의 User 클래스의 인스턴스를 제거
channel.exit(user);
// 참가자 대기실 퇴장
// - 웹소켓 연결이 해제된 회원의 User 클래스의 인스턴스를 Room 클래스의 방 참가자 저장소인 Set<User> users에 제거
waitingRoom.leave(user);
// 사용자 퇴장 확인을 위한 로그 출력
log.debug("사용자 퇴장");
}
// 웹소켓 서버에 메시지 전송시
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// WebSocketSession에 저장된 값을 Map 형태로 반환
// - 웹소켓 연결이 생성된 회원의 아이디, 닉네임, 등급을 Key-Value 형태로 반환
Map<String, Object> attr = session.getAttributes();
// 웹소켓 연결이 생성된 사용자의 WebSocketSession에 저장된 값으로 User 클래스의 인스턴스 생성
User user = User.builder()
.memberId((String)attr.get("loginId"))
.memberNick((String)attr.get("loginNick"))
.memberGrade((String)attr.get("loginAuth"))
.session(session)
.build();
// ObjectMapper의 인스턴스 생성
ObjectMapper mapper = new ObjectMapper();
// JSON을 VO 형태로 변환
ReceiveVO receiveVO = mapper.readValue(message.getPayload(), ReceiveVO.class);
// 변환된 VO의 형태를 확인하기 위한 로그 출력
log.debug("receiveVO = {}", receiveVO);
// 사용자가 특정 방에 참가하는 것인지, 특정 방에 메시지를 보내는 것인지 판정
if(receiveVO.getType() == 1) { // VO의 type 필드의 값이 1인 경우 (방 참가)
// 대기실의 방 참가자 저장소에서 해당 사용자의 User 클래스의 인스턴스 제거
waitingRoom.leave(user);
// 참가하려는 방의 방 참가자 저장소에 해당 사용자의 User 클래스의 인스턴스 추가
channel.join(user, receiveVO.getRoom());
// 해당 사용자의 특정 방 참가를 확인하기 위한 로그 출력
log.debug("{} 방에 {} 입장", receiveVO.getRoom(), user.getMemberId());
}
else if(receiveVO.getType() == 2) { // VO의 type 필드의 값이 2인 경우 (메시지 전송)
// 메시지 출력 형식에 맞도록 메시지에 필요한 정보 추가
MessageVO vo = MessageVO.builder()
.id(user.getMemberId()) // 전송자 아이디
.nickname(user.getMemberNick()) // 전송자 닉네임
.auth(user.getMemberGrade()) // 전송자 회원 등급
.text(receiveVO.getText()) // 전송자 메시지 내용
.time(new Date()) // 전송 일자
.build();
// VO를 JSON 형태의 문자열로 변환
String json = mapper.writeValueAsString(vo);
// JSON 형태의 문자열로 메시지 생성
TextMessage msg = new TextMessage(json);
// 해당 사용자가 참가하고있는 방의 이름을 찾아 해당 방에만 메시지 전송
channel.send(user, msg);
}
}
}
ReceiveVO
- 웹소켓 서버로 보내는 메시지의 유형 판정을 위한 VO
- 방 입장을 위한 메시지일 경우 type, room 필드의 값이 존재하며 text는 null
- 메시지 전송을 위한 메시지일 경우 type, text 필드의 값이 존재하며 room은 null
@JsonIgnoreProperties // 필드의 값이 모두 있지 않아도 처리가 가능하도록
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ReceiveVO {
// 필드
private int type; // 유형 - 방 참가 / 메시지 전송을 구별하기 위함
private String room; // 방 이름 - 방 참가를 위한 필드
private String text; // 메시지 내용 - 메시지 전송을 위한 필드
}
WebSocketServerConfiguration
@Configuration
@EnableWebSocket // 웹소켓 활성화
public class WebSocketServerConfiguration implements WebSocketConfigurer {
// 의존성 주입
@Autowired
private MemberGroupChatServer memberGroupChatServer;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// HttpSessionHandshakeInterceptor는 HttpSession을 WebSocketSession으로 넘겨준다
registry..addHandler(memberGroupChatServer, "/ws/group")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}
}
PageController
@Controller
@RequestMapping("/page")
public class PageController {
@GetMapping("/group/{room}")
public String group(@PathVariable String room, Model model) {
model.addAttribute("room", room);
return "group";
}
}
group.jsp
- 웹소켓 연결이 생성되면 웹소켓 서버로 방 입장을 위한 메시지 전송
- 사용자가 입력창에 입력을 한 후 전송 버튼을 누르면 웹소켓 서버로 메시지 전송을 위한 메시지를 전송
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<style>
.chat-message {
padding: 1em;
border: 1px solid black;
border-radius : 1em;
}
</style>
<h1>그룹 채팅 예제</h1>
<h2>방 이름 : ${room} </h2>
<hr>
<input type="text" id="message-input">
<button type="button" id="message-send">전송</button>
<hr>
<div id="message-list"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
$(function(){
//1. 페이지에 연결되면 바로 웹소켓 연결 생성
// 웹소켓 연결 주소 설정
var uri = "${pageContext.request.contextPath}/ws/group";
// 웹소켓 연결 생성
// - 접속시 WebSocket이 아닌 SockJS 객체를 생성
socket = new SockJS(uri);
// 웹소켓에 대한 이벤트 설정
// 1) 웹소켓 연결이 생성될 때
socket.onopen = function(){
//console.log("open");
// 웹소켓에 연결되면 바로 웹소켓 서버로 '방 입장' 메시지 전송
var data = {
type : 1,
room : "${room}"
}
socket.send(JSON.stringify(data));
};
// 2) 웹소켓 연결 중 연결이 해제될 때
socket.onclose = function(){
//console.log("close");
};
// 3) 웹소켓 연결 중 연결에 에러가 발생할 때
socket.onerror = function(){
//console.log("error");
};
// 4) 웹소켓 연결 중 메시지가 수신될 때
socket.onmessage = function(e){
// 문자열을 JSON 형태로 수신 - JSON.parse()
var data = JSON.parse(e.data);
// p 태그 지정 - CSS에서 디자인을 하기 위해 클래스 부여
var p = $("<p>").addClass("chat-message");
// p 태그 지정 - 닉네임[등급] 형태
var w = $("<p>").text(data.nickname + "[" + data.auth + "]");
// 수신한 JSON의 time 형태 변경
var time = moment(data.time).format("YYYY-MM-DD hh:mm");
// p 태그 지정 - (시간) 형태
var t = $("<p>").text("("+time+")");
// p 태그 지정 - 메시지 표시
var c = $("<p>").text(data.text);
// 지정된 p 태그들을 연결
p.append(w).append(c).append(t);
// 메시지를 표시하는 div 안에 연결된 p 태그 생성
$("#message-list").append(p);
// 메시지가 발생할 때마다 스크롤 하단으로 이동
var height = $(document).height();
$(window).scrollTop(height);
};
// 2.
$(window).on("beforeunload", function(){
//웹소켓 연결 종료
//window.socket.close();
socket.close();
});
// 3. 전송 버튼을 누를 때
// - 웹소켓 서버로 '사용자가 입력한' 메시지 전송
$("#message-send").click(function(){
// 입력창에 입력된 값을 지정
var text = $("#message-input").val();
// 입력창에 입력된 값이 없는 상태에서 전송 버튼을 누르면 return
if(text.length == 0) return;
// JSON으로 변환해서 전송
// - JSON.stringify(객체) : 객체를 문자열로
// - JSON.parse(문자열) : 문자열을 객체로
// 입력값을 JSON 형태로 설정
var data = {
type : 2,
text : text
};
// 입력창의 값 전송
socket.send(JSON.stringify(data));
// 전송 후 입력창 초기화(비우기)
$("#message-input").val("");
});
});
</script>
'국비교육 > 국비교육' 카테고리의 다른 글
day78 - 1117 (0) | 2022.11.18 |
---|---|
day77 - 1116 (0) | 2022.11.16 |
day75 - 1114 (0) | 2022.11.14 |
day73 - 1110 (0) | 2022.11.12 |
day72 - 1109 (0) | 2022.11.12 |