코딩 노트

실시간 채팅(WebSocket) 본문

Spring

실시간 채팅(WebSocket)

newbyeol 2023. 10. 20. 10:35

spring2 websocket 패키지 생성 

라이브러리 추가

WebSocket 

통신의 발전

1. 비동기 통신

2. pullung 방식

3. long-pulling 방식

4. websocket 방식

    - 기본 Websocket

    - SockJS(Spring가 체택한 기술) / Socket IO(NodeJS가 체택한 기술)

    - STOMP(위 두 개와 아예 다름)(난이도 있음)

2, 3번의 방식은 비동기 통신이다.

크롬 버전 확인법

websocket 패키지 생성 

DefaultWebSocketServer 클래스 생성

/*

스프링에서 웹소켓 연결을 처리하는 도구(서버)

- 상속을 통해 클래스를 구현 (WebSocketHandler / TextWebSocketHandler / BinaryWebSocketHandler)

텍스트는 글자, 바이너리는 파일이 오고 갈 때 사용한다.

상속 받은 다음 필요한 걸 고쳐쓰는 인터셉터와 비슷한 로직이다.

- 등록하여 사용한다.

- 스프링이 통신 관리는 전부 다 해주고, 진행 상태만 알려줌

- afterConnectionEstablished는 통신이 연결된 이후 실행되는 메소드 (연결이 된 이후에 알려줌) / 사용자 접속을 알려준다.

- afterConnectionClosed 통신이 종료된 이후 실행되는 메소드 (끊어진 이후에 알려줌)

서버를 만든 것이 아닌 서버를 모니터하는 걸 만듬

 

<웹소켓 서버 요약>

1. 상속을 받는다.

2. 등록을 한다.

3. 필요한 메소드들을 재정의한다.

*/

 

@Slf4j

@Service

public class DefaultWebSocketServer extends TextWebSocketHandler {

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

log.debug("사용자 접속!");

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

log.debug("사용자 접속 종료");

}

}

configuration 패키지 생성 후

WebSocketServerConfiguration 클래스 생성

//이 클래스는 생성한 웹소켓 서버를 어떤 주소에 할당하도록 설정하는 역할을 한다.

//스케줄러보다 서버에 더 무리가 가는 작업

@EnableWebSocket //기본적으로 잠겨 있다. 서버에 무리가 가기 때문

@Configuration

public class WebSocketServerConfiguration implements WebSocketConfigurer {

 

@Autowired

private DefaultWebSocketServer defaultWebSocketServer; //등록

 

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 등록할 때는 주소와 도구를 연결해야 한다. (필요하다면 추가 옵션 설정)

// (주의) 절대로 화면의 주소와 겹치면 안 된다.

registry.addHandler(defaultWebSocketServer, "/ws/default"); //defaultWebSocketServer, 어떤 주소에 연결하겠습니다를 작성(절대경로)

 

}

 

}

웹소켓 서버 요약

상속받고, 등록받고, 필요한 메소드를 만든다.(재정의)

코드 작성 후 설정에 주세요 하고 등록하면 끝이 난다.

지금까지 전화를 받아주는 서버를 만든 것이다. 

이제 전화를 거는 프론트엔드를 만들어야 한다.

WebSocket은 CrossOrigin이 없다. (보안 때문에) 그래서 무조건 같은 서버에 만들어야 한다.

 

controller 패키지 생성 후

WebSocketViewController 클래스 생성

@Controller

public class WebSocketViewController {

 

@RequestMapping("/")

public String home() {

// return "WEB-INF/views/home.jsp";

return "home"; //view resolver 기능

}

 

}

jsp를 사용하기 위해 pom.xml에 라이브러리 추가

<!-- 프로젝트에 JSP를 사용하기 위한 라이브러리(의존성) -->

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

</dependency>

<dependency>

<groupId>org.apache.tomcat.embed</groupId>

<artifactId>tomcat-embed-jasper</artifactId>

</dependency>

 

<!-- SHA(Secure Hash Algorithm) 암호화 라이브러리 -->

<dependency>

<groupId>commons-codec</groupId>

<artifactId>commons-codec</artifactId>

</dependency>

 

<!-- 스프링 시큐리티 암호화 라이브러리 -->

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-core</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-mail</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-configuration-processor</artifactId>

<optional>true</optional>

</dependency>

 

<!-- jsoup 라이브러리(html 해석 및 변경 라이브러리) -->

<dependency>

<groupId>org.jsoup</groupId>

<artifactId>jsoup</artifactId>

<version>1.16.1</version>

</dependency>

properties 설정

# project setting file

# key=value

 

#sever setting

#server.servlet.context-path=/khacademy

##server.port=9999

 

#view resolver setting

spring.mvc.view.prefix=/WEB-INF/views/

spring.mvc.view.suffix=.jsp

 

 

# database setting

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe

spring.datasource.username=C##home

spring.datasource.password=home

 

spring.datasource.hikari.data-source-properties.oracle.jdbc.timezoneAsRegion=false

 

# mybatis setting

mybatis.type-aliases-package=com.kh.spring20.dto,com.kh.spring20.vo

mybatis.mapper-locations=/mybatis/**/*-mapper.xml

mybatis.configuration.map-underscore-to-camel-case=true

 

#logging setting

logging.level.root=warn

logging.level.com.kh=debug

#logging.level.member=debug

 

logging.pattern.console=[%-5level] %msg - %c [%d{yyyy-MM-dd HH:mm:ss.S}] %n

 

# logging file setting

#logging.file.name= logs/server.log

#logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss.S} [%-5level] %msg - %c %n

#logging.logback.rollingpolicy.max-file-size=10MB

#logging.logback.rollingpolicy.file-name-pattern=${LOG_FILE}-%d{yyyy-MM-dd-HH}-%i.log

 

#custom properties setting

#custom.fileupload.home=C:/upload/profile

 

#email setting

#custom.email.host=smtp.gmail.com

#custom.email.port=587

#custom.email.username=qufquf12120

#custom.email.password=ajeprfgyoraxwuks

home.jsp 파일 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<h1>웹소켓 실습 예제</h1>

 

<h2><a href="default">기본 웹서버 예제</a></h2>

WebSocketViewController 클래스 구문 추가

@Controller

public class WebSocketViewController {

 

@RequestMapping("/")

public String home() {

// return "WEB-INF/views/home.jsp";

return "home"; //view resolver 기능

}

 

@RequestMapping("/default")

public String defaultServer() {

// return "/WEB-INF/views/default.jsp";

return "default";

}

 

}

default.jsp 파일 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>기본 웹소켓 예제</h1>

 

<button type="button" class="connect-btn">연결</button>

<button type="button" class="disconnect-btn">종료</button>

WebSocket API

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>기본 웹소켓 예제</h1>

 

<button type="button" class="connect-btn">연결</button>

<button type="button" class="disconnect-btn">종료</button>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

<script>

$(function(){

//목표 : 연결버튼을 누르면 웹소켓 연결 생성, 종료버튼을 누르면 생성한 연결 종료

$(".connect-btn").click(function(){

var uri = "ws://localhost:8080/ws/default";

window.socket = new WebSocket(uri);

});

 

$(".disconnect-btn").click(function(){

window.socket.close();

});

});

</script>

Dispatcher Servlet

사용자의 요청을 등록된 컨트롤러로 정리해주는, 사용자의 요청을 종합적으로 처리해주는 기능이다.

 

접속한 사용자의 정보가 나온다.

code = 1000번은 정상종료이다. (1000~1004)R까지 있다.

저기에 나오는 세션은 우리가 지금까지 썼던 세션이 아니다. 자료형이 다르다.

 

WebSocketServerConfiguration 클래스 구문 추가

//이 클래스는 생성한 웹소켓 서버를 어떤 주소에 할당하도록 설정하는 역할을 한다.

//스케줄러보다 서버에 더 무리가 가는 작업

@EnableWebSocket //기본적으로 잠겨 있다. 서버에 무리가 가기 때문

@Configuration

public class WebSocketServerConfiguration implements WebSocketConfigurer {

 

@Autowired

private DefaultWebSocketServer defaultWebSocketServer; //등록

 

@Autowired

private TimeWebSocketServer timeWebSocketServer; //주세요(의존성 주입) //추가

 

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 등록할 때는 주소와 도구를 연결해야 한다. (필요하다면 추가 옵션 설정)

// (주의) 절대로 화면의 주소와 겹치면 안 된다.

registry.addHandler(defaultWebSocketServer, "/ws/default"); //defaultWebSocketServer, 어떤 주소에 연결하겠습니다를 작성(절대경로)

registry.addHandler(timeWebSocketServer, "/ws/time"); //추가

}

 

}

WebSocketViewController 클래스에 구문 추가

@RequestMapping("/time")

public String timeServer() {

return "time";

}

TimeWebSocketServer 클래스 생성

@Slf4j

@Service

public class TimeWebSocketServer extends TextWebSocketHandler {

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

log.debug("사용자 접속 = {}", session);

 

//접속한 사용자에게 현재시각을 전달

TextMessage message = new TextMessage(LocalDateTime.now().toString()); //사용자에게 아무 때나(종료 됐을 때 제외) 메세지를 보낼 수 있다.

session.sendMessage(message);

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

log.debug("사용자 접속 종료 = {}", session);

log.debug("종료사유 = {}", status);

}

}

time.jsp 파일 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>타입 웹서버 예제</h1>

 

<button type="button" class="connect-btn">연결</button>

<button type="button" class="disconnect-btn">종료</button>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

 

<script>

$(function(){

$(".connect-btn").click(function(){

var uri = "ws://localhost:8080/ws/time";

window.socket = new WebSocket(uri);

});

 

$(".disconnect-btn").click(function(){

window.socket.close();

});

});

</script>

100번대는 연결이 되어있는 걸 얘기한다.

 

WebSocketServerConfiguration 클래스에 구문 추가

//이 클래스는 생성한 웹소켓 서버를 어떤 주소에 할당하도록 설정하는 역할을 한다.

//스케줄러보다 서버에 더 무리가 가는 작업

@EnableWebSocket //기본적으로 잠겨 있다. 서버에 무리가 가기 때문

@Configuration

public class WebSocketServerConfiguration implements WebSocketConfigurer {

 

@Autowired

private DefaultWebSocketServer defaultWebSocketServer; //등록

 

@Autowired

private TimeWebSocketServer timeWebSocketServer; //주세요(의존성 주입)

 

@Autowired

private GroupWebSocketServer groupWebSocketServer;

 

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 등록할 때는 주소와 도구를 연결해야 한다. (필요하다면 추가 옵션 설정)

// (주의) 절대로 화면의 주소와 겹치면 안 된다.

registry.addHandler(defaultWebSocketServer, "/ws/default"); //defaultWebSocketServer, 어떤 주소에 연결하겠습니다를 작성(절대경로)

registry.addHandler(timeWebSocketServer, "/ws/time");

registry.addHandler(groupWebSocketServer, "/ws/group");

}

 

}

GroupWebSocketServer 클래스에 구문 추가

@Slf4j

@Service

public class GroupWebSocketServer extends TextWebSocketHandler {

//사용자를 저장할 수 있는 저장소

//private Set<WebSocketSession> clients = new HashSet<>(); //동기화 처리가 안되어 있음

private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>(); //동기화 처리됨

//private Set<WebSocketSession> clients = Collections.synchronizedSet(new HashSet<>());

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

//사용자를 저장소에 추가하는 코드

clients.add(session);

log.debug("사용자 접속! 현재 {}명", clients.size());

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

//사용자를 저장소에서 제거하는 코드

clients.remove(session);

log.debug("사용자 접속 종료! 현재 {}명", clients.size());

}

 

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

// 사용자가 보낸 메세지를 처리하는 메소드

//- 접속한 모든 사용자에게 메세지를 전달(브로드캐스트, broadcast)

 

for(WebSocketSession client : clients) {

client.sendMessage(message);

}

 

}

}

WebSocketViewController 클래스에 구문 추가

@RequestMapping("/group")

public String groupServer() {

return "group";

}

group.jsp에 구문 추가

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<h1>그룹 웹서버 예제</h1>

 

<button type="button" class="connect-btn">연결</button>

<button type="button" class="disconnect-btn">종료</button>

<hr>

<input type="text" class="message-input">

<button type="button" class="send-btn">전송</button>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

 

<script>

$(function(){

$(".connect-btn").click(function(){

var uri = "ws://localhost:8080/ws/group";

window.socket = new WebSocket(uri);

});

 

$(".disconnect-btn").click(function(){

window.socket.close();

});

 

//전송 버튼을 클릭하면 입력한 메세지를 가져와서 서버로 전달

$(".send-btn").click(function(){

//var input document.querySelector(".message-input").value;

var input = $(".message-input").val();

if(input.length == 0) return;

 

window.socket.send(input);

$(".message-input").val("");

 

});

 

 

 

});

</script>

한 쪽에 전송을 하면 한 쪽에서 확인 할 수 있다.

group.jsp에 구문 추가

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>

 

<h1>그룹 웹서버 예제</h1>

 

<button type="button" class="connect-btn">연결</button>

<button type="button" class="disconnect-btn">종료</button>

<hr>

<input type="text" class="message-input">

<button type="button" class="send-btn">전송</button>

 

<div class="message-list"></div>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

 

<script>

$(function(){

$(".connect-btn").click(function(){

window.socket = new WebSocket("ws://localhost:8080/ws/group");

//연결 생성 시점에 연결에서 발생할 수 있는 상황별로 callback 함수를 지정

//- onopen - 연결이 성공한 직후에 질행하는 함수를 설정하는 자리

//- onclose - 연결이 종료된 직후에 실행하는 함수를 설정하는 자리

//- onerror - 연결에서 오류가 발생한 경우 실행하는 함수를 설정하는 자리

//- onmessage - 서버에서 메세지가 전송되는 경우 실행하는 함수를 설정하는 자리

 

socket.onmessage = function(e){

//console.log(e.data);

$("<div>").text(e.data).appendTo(".message-list");

 

Toastify({

text: e.data,

duration: 3000,

//destination: "https://github.com/apvarun/toastify-js", //누르면 가는 곳

newWindow: true,

close: true,

gravity: "bottom", // `top` or `bottom`

position: "right", // `left`, `center` or `right`

stopOnFocus: true, // Prevents dismissing of toast on hover

style: {

background: "linear-gradient(to right, #00b09b, #96c93d)",

},

onClick: function(){} // Callback after click

}).showToast();

};

 

});

 

$(".disconnect-btn").click(function(){

window.socket.close();

});

 

//전송 버튼을 클릭하면 입력한 메세지를 가져와서 서버로 전달

$(".send-btn").click(function(){

//var input document.querySelector(".message-input").value;

var input = $(".message-input").val();

if(input.length == 0) return;

 

window.socket.send(input);

$(".message-input").val("");

 

});

 

 

 

});

</script>

MemberWebSocketServer 클래스 생성

@Slf4j

@Service

public class MemberWebSocketServer extends TextWebSocketHandler {

 

private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

clients.add(session);

log.debug("사용자 접속 = {}",clients.size());

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

clients.remove(session);

log.debug("사용자 종료 = {}", clients.size());

}

 

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

for(WebSocketSession client : clients) {

client.sendMessage(message);

}

}

}

WebSocketServerConfiguraion 클래스에 위 그림의 부분 구문 추가 

//이 클래스는 생성한 웹소켓 서버를 어떤 주소에 할당하도록 설정하는 역할을 한다.

//스케줄러보다 서버에 더 무리가 가는 작업

@EnableWebSocket //기본적으로 잠겨 있다. 서버에 무리가 가기 때문

@Configuration

public class WebSocketServerConfiguration implements WebSocketConfigurer {

 

@Autowired

private DefaultWebSocketServer defaultWebSocketServer; //등록

 

@Autowired

private TimeWebSocketServer timeWebSocketServer; //주세요(의존성 주입)

 

@Autowired

private GroupWebSocketServer groupWebSocketServer;

 

@Autowired

private MemberWebSocketServer memberWebSocketServer;

 

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 등록할 때는 주소와 도구를 연결해야 한다. (필요하다면 추가 옵션 설정)

// (주의) 절대로 화면의 주소와 겹치면 안 된다.

registry.addHandler(defaultWebSocketServer, "/ws/default"); //defaultWebSocketServer, 어떤 주소에 연결하겠습니다를 작성(절대경로)

registry.addHandler(timeWebSocketServer, "/ws/time");

registry.addHandler(groupWebSocketServer, "/ws/group");

 

//아래와 같이 등록하면 HttpSession의 정보를 WebSocketSession으로 옮겨준다.

registry.addHandler(memberWebSocketServer, "/ws/member")

.addInterceptors(new HttpSessionHandshakeInterceptor());

 

}

 

}

Handshake는 연결을 의미한다. 그림에 있는 지점들의 정보를 옮겨주는 역할을 한다.

home.jsp에 로그인 부분 구문 추가

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<h1>웹소켓 실습 예제</h1>

 

<h2><a href="default">기본 웹소켓 예제</a></h2>

<h2><a href="time">타임 웹소켓 예제</a></h2>

<h2><a href="group">그룹 웹소켓 예제</a></h2>

 

<!-- 로그인 화면 -->

 

<form action = "login" meghod="post">

ID<input type="text" name="memberId">

<br><br>

PW<input type="password" name="memberPw">

<br><br>

<button type="submit">로그인</button>

</form>

dto 패키지 생성 후 MemberDto 클래스 생성

@Data @AllArgsConstructor @NoArgsConstructor @Builder

public class MemberDto {

private String memberId, memberPw, memberNickname, memberEmail, memberContact;

private String memberBirth, memberPost, memberAddr1, memberAddr2, memberLevel;

private int memberPoint;

private Date memberJoin, memberLogin, memberChange;

}

mybatis 폴더 생성 후 member-mapper.xml 파일 생성

<?xml version="1.0" encoding="UTF-8"?>

 

<!DOCTYPE mapper

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"https://mybatis.org/dtd/mybatis-3-mapper.dtd">

 

<mapper namespace="member">

<select id="find" resultType="MemberDto">

select * from member where member_id = #{memberId}

</select>

</mapper>

WebSocketViewController에 구문 추가

//임시 로그인 처리

@Autowired

private SqlSession sqlSession;

 

@PostMapping("/login")

public String login(@ModelAttribute MemberDto memberDto, HttpSession session) {

MemberDto findDto = sqlSession.selectOne("member.find", memberDto);

if(findDto != null) {

boolean pwMatch = findDto.getMemberPw().equals(memberDto.getMemberPw()); //암호화가 들어간다면 equals가 아닌 matches로

if(pwMatch) {

session.setAttribute("name", memberDto.getMemberId()); //아이디

session.setAttribute("level", findDto.getMemberLevel()); //등급

}

}

return "redirect:/";

}

 

@RequestMapping("/logout")

public String logout(HttpSession session) {

session.removeAttribute("name");

session.removeAttribute("level");

return "redirect:/";

}

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>

 

<h2><a href="default">기본 웹소켓 예제</a></h2>

<h2><a href="time">타임 웹소켓 예제</a></h2>

<h2><a href="group">그룹 웹소켓 예제</a></h2>

 

<c:choose>

<c:when test = "${sessionScope.name == null}">

<!-- 로그인 화면 -->

<form action = "login" method="post">

ID <input type="text" name="memberId">

<br><br>

PW <input type="password" name="memberPw">

<br><br>

<button type="submit">로그인</button>

</form>

</c:when>

<c:otherwise>

<a href = "logout">로그아웃</a>

</c:otherwise>

</c:choose>

 

<h2><a href="member">회원 전용 웹소켓 예제</a></h2>

WebSocketViewController에 구문 추가

@RequestMapping("/member")

public String member() {

// return "/WEB-INF/views/member.jsp";

return "member";

}

member.jsp에 구문 추가

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>

 

<h1>회원 전용 웹소켓 예제</h1>

 

<input type="text" class="message-input">

<button type="button" class="send-btn">전송</button>

 

<div class="message-list"></div>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

 

<script>

 

// $(function(){}); // 없어도 되는 이유는 레이지로딩을 했기 때문이다.

window.socket = new WebSocket("ws://localhost:8080/ws/member");

socket.onmessage = function(e){

//console.log(e.data);

$("<div>").text(e.data).appendTo(".message-list");

 

Toastify({

text: e.data,

duration: 3000,

//destination: "https://github.com/apvarun/toastify-js", //누르면 가는 곳

newWindow: true,

close: true,

gravity: "bottom", // `top` or `bottom`

position: "right", // `left`, `center` or `right`

stopOnFocus: true, // Prevents dismissing of toast on hover

style: {

background: "linear-gradient(to right, #00b09b, #96c93d)",

},

onClick: function(){} // Callback after click

}).showToast();

};

 

//전송 버튼을 클릭하면 입력한 메세지를 가져와서 서버로 전달

$(".send-btn").click(function(){

//var input document.querySelector(".message-input").value;

var input = $(".message-input").val();

if(input.length == 0) return;

 

window.socket.send(input);

$(".message-input").val("");

});

 

</script>

MemberWebSocketServer 클래스에 구문 추가

@Slf4j

@Service

public class MemberWebSocketServer extends TextWebSocketHandler {

 

private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

clients.add(session);

log.debug("session = {}",session.getAttributes());

 

//session의 추가 정보(attributes)를 조사하여 HttpSession의 정보를 추출하여 사용

Map<String, Object> attr = session.getAttributes();

String memberId = (String)attr.get("name");

String memberLevel = (String)attr.get("level");

log.debug("아이디 = {}, 등급 = {}", memberId, memberLevel);

 

log.debug("사용자 접속 = {}",clients.size());

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

clients.remove(session);

log.debug("사용자 종료 = {}", clients.size());

}

 

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

//기존 HTTP 세션의 정보를 조회

Map<String, Object> attr = session.getAttributes();

String memberId = (String)attr.get("name");

String memberLevel = (String)attr.get("level");

if(memberId == null || memberLevel == null) { //비회원이라면

return;

}

 

for(WebSocketSession client : clients) {

client.sendMessage(message);

}

}

}

비회원이 보내면 보이지 않는다.

MemberWebSocketServer 클래스에 구문 추가

@Slf4j

@Service

public class MemberWebSocketServer extends TextWebSocketHandler {

 

private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

clients.add(session);

log.debug("session = {}",session.getAttributes());

 

//session의 추가 정보(attributes)를 조사하여 HttpSession의 정보를 추출하여 사용

Map<String, Object> attr = session.getAttributes();

String memberId = (String)attr.get("name");

String memberLevel = (String)attr.get("level");

log.debug("아이디 = {}, 등급 = {}", memberId, memberLevel);

 

log.debug("사용자 접속 = {}",clients.size());

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

clients.remove(session);

log.debug("사용자 종료 = {}", clients.size());

}

 

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

//기존 HTTP 세션의 정보를 조회

Map<String, Object> attr = session.getAttributes();

String memberId = (String)attr.get("name");

String memberLevel = (String)attr.get("level");

if(memberId == null || memberLevel == null) { //비회원이라면

return;

}

 

//메세지에 전송하는 송신자의 ID를 추가하여 전송

TextMessage tm = new TextMessage("[" + memberId + "] " + message.getPayload());

 

for(WebSocketSession client : clients) {

client.sendMessage(tm);

}

}

}

회원의 아이디와 등급, 메세지를 텍스트로 보내는 게 아닌 객체로 만들어서 전송한다.

MemberWebSocketServer 클래스에 구문 추가

@Slf4j

@Service

public class JsonWebSocketServer extends TextWebSocketHandler {

 

private Set<WebSocketSession> clients = new CopyOnWriteArraySet<>();

 

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

clients.add(session);

log.debug("session = {}",session.getAttributes());

 

//session의 추가 정보(attributes)를 조사하여 HttpSession의 정보를 추출하여 사용

Map<String, Object> attr = session.getAttributes();

String memberId = (String)attr.get("name");

String memberLevel = (String)attr.get("level");

log.debug("아이디 = {}, 등급 = {}", memberId, memberLevel);

 

log.debug("사용자 접속 = {}",clients.size());

}

 

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

clients.remove(session);

log.debug("사용자 종료 = {}", clients.size());

}

 

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

//기존 HTTP 세션의 정보를 조회

Map<String, Object> attr = session.getAttributes();

String memberId = (String)attr.get("name");

String memberLevel = (String)attr.get("level");

if(memberId == null || memberLevel == null) { //비회원이라면

return;

}

 

//메세지 전송 시 여러 정보를 JSON 문자열 형태로 변환하여 전송

//(ex) {"memberId":"testuser1" , "memberLevel":"VIP", "content":"Hello!"}

 

//자바에서 JSON을 생성하는 방법은 여러 가지가 있다. (Jackson, Gson, ...)

//- 스프링 부트에 기본 탑재된 jackson-databind의 도구를 사용하여 처리 (ObjectMapper)

 

//FE에게 보낼 메세지 객체를 생성

Map<String, Object> map = new HashMap<>(); //클래스가 있으면 클래스를 쓰면 됨, 자바에서 클래스랑 맵은 같은 역할을 함

map.put("memberId", memberId);

map.put("memberLevel", memberLevel);

map.put("content", message.getPayload());

 

//도구를 만들어 JSON으로 변환

ObjectMapper mapper = new ObjectMapper();

String str = mapper.writeValueAsString(map);

 

//메세지를 생성하여 변환된 내용을 담아 모든 사용자에게 전송

TextMessage tm = new TextMessage(str);

for(WebSocketSession client : clients) {

client.sendMessage(tm);

}

}

}

WebSocketConfiguration에 의존성 주입 구문 추가

@Autowired

private JsonWebSocketServer jsonWebSocketServer;

이 부분을

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 등록할 때는 주소와 도구를 연결해야 한다. (필요하다면 추가 옵션 설정)

// (주의) 절대로 화면의 주소와 겹치면 안 된다.

registry.addHandler(defaultWebSocketServer, "/ws/default"); //defaultWebSocketServer, 어떤 주소에 연결하겠습니다를 작성(절대경로)

registry.addHandler(timeWebSocketServer, "/ws/time");

registry.addHandler(groupWebSocketServer, "/ws/group");

 

//아래와 같이 등록하면 HttpSession의 정보를 WebSocketSession으로 옮겨준다.

registry.addHandler(memberWebSocketServer, "/ws/member")

.addInterceptors(new HttpSessionHandshakeInterceptor());

registry.addHandler(jsonWebSocketServer, "/ws/json")

.addInterceptors(new HttpSessionHandshakeInterceptor());

 

}

이렇게 줄여서도 쓸 수 있다.

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 등록할 때는 주소와 도구를 연결해야 한다. (필요하다면 추가 옵션 설정)

// (주의) 절대로 화면의 주소와 겹치면 안 된다.

registry.addHandler(defaultWebSocketServer, "/ws/default") //defaultWebSocketServer, 어떤 주소에 연결하겠습니다를 작성(절대경로)

.addHandler(timeWebSocketServer, "/ws/time")

.addHandler(groupWebSocketServer, "/ws/group");

 

//아래와 같이 등록하면 HttpSession의 정보를 WebSocketSession으로 옮겨준다.

registry.addHandler(memberWebSocketServer, "/ws/member")

.addHandler(jsonWebSocketServer, "/ws/json")

.addInterceptors(new HttpSessionHandshakeInterceptor());

 

}

WebSocketViewController에 구문 추가

@RequestMapping("/json")

public String json() {

return "json";

}

home.jsp 수정

<h2><a href="member">회원 전용 웹소켓 예제</a></h2>

<h2><a href="json">회원 전용 웹소켓 예제(+JSON)</a></h2>

json.jsp 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>

 

<h1>회원(+JSON) 전용 웹소켓 예제</h1>

 

<input type="text" class="message-input">

<button type="button" class="send-btn">전송</button>

 

<div class="message-list"></div>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

 

<script>

 

// $(function(){}); // 없어도 되는 이유는 레이지로딩을 했기 때문이다.

window.socket = new WebSocket("ws://localhost:8080/ws/json");

socket.onmessage = function(e){

//console.log(e.data);

$("<div>").text(e.data).appendTo(".message-list");

 

Toastify({

text: e.data,

duration: 3000,

//destination: "https://github.com/apvarun/toastify-js", //누르면 가는 곳

newWindow: true,

close: true,

gravity: "bottom", // `top` or `bottom`

position: "right", // `left`, `center` or `right`

stopOnFocus: true, // Prevents dismissing of toast on hover

style: {

background: "linear-gradient(to right, #00b09b, #96c93d)",

},

onClick: function(){} // Callback after click

}).showToast();

};

 

//전송 버튼을 클릭하면 입력한 메세지를 가져와서 서버로 전달

$(".send-btn").click(function(){

//var input document.querySelector(".message-input").value;

var input = $(".message-input").val();

if(input.length == 0) return;

 

window.socket.send(input);

$(".message-input").val("");

});

 

</script>

json.jsp 수정

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

 

<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>

 

<h1>회원(+JSON) 전용 웹소켓 예제</h1>

 

<input type="text" class="message-input">

<button type="button" class="send-btn">전송</button>

 

<div class="message-list"></div>

 

<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script>

 

<script>

 

// $(function(){}); // 없어도 되는 이유는 레이지로딩을 했기 때문이다.

window.socket = new WebSocket("ws://localhost:8080/ws/json");

socket.onmessage = function(e){

//console.log(e.data);

var data = JSON.parse(e.data); //JSON 문자열을 자바스크립트 객체로 해석(<--> JSON.stringfy() : 이 명령은 객체가 문자열이 됨)

console.log(data);

 

var memberId = $("<div>").text(data.memberId);

var memberLevel = $("<div>").text(data.memberLevel);

var content = $("<div>").text(data.content);

 

$("<div>").css("display", "flex")

.append(memberId)

.append(memberLevel)

.append(content) //append는 그냥 나에게 추가

.appendTo(".message-list"); //appendTo는 다른거에 추가

 

Toastify({

text: data.content,

duration: 3000,

//destination: "https://github.com/apvarun/toastify-js", //누르면 가는 곳

newWindow: true,

close: true,

gravity: "bottom", // `top` or `bottom`

position: "right", // `left`, `center` or `right`

stopOnFocus: true, // Prevents dismissing of toast on hover

style: {

background: "linear-gradient(to right, #00b09b, #96c93d)",

},

onClick: function(){} // Callback after click

}).showToast();

 

};

 

//전송 버튼을 클릭하면 입력한 메세지를 가져와서 서버로 전달

$(".send-btn").click(function(){

//var input document.querySelector(".message-input").value;

var input = $(".message-input").val();

if(input.length == 0) return;

 

window.socket.send(input);

$(".message-input").val("");

});

 

</script>