코딩 노트
실시간 채팅(WebSocket) 본문
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>
<%@ 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>
'Spring' 카테고리의 다른 글
DM을 어떻게 보내는가 (0) | 2023.10.23 |
---|---|
웹소켓 - 실시간 채팅, 반응형 디자인 (0) | 2023.10.23 |
HTTP 쿠키(Cookie) (0) | 2023.10.18 |
myBatis04 - 암호화 로그인 + view resolver + 회원가입 축하 이메일 (0) | 2023.10.17 |
myBatis03 - 멤버 복합검색 + 암호화 (0) | 2023.10.16 |