ㅁ
- 채팅 웹소켓
- 핸들러 패키지의 chatendchcoghandler 복사해온다.
package com.br.boot.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.boot.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));
}
}
}
- import해도 빨간줄 뜬다.
그말은 얘네들을 제공하는 라이브러리들이 연결되어있지 않다는 거다.
- 기존에 웹소켓 할때도 라이브리를 추가했었다.
- 그리고 에코핸들러 클래스를 만들고나서 웹소켓 관련 등록구문을 작성했었다.
ㅁ
- 패키지 익스플로러에서 프로젝트 우클릭 - 스프링 - add starters
- websocket 검색해서 하나 나온거 체크하고 next하고 pom.xml체크하고 finish.
ㅁ pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.br</groupId>
<artifactId>boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>boot</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- jsp 사용을 위한 jasper 라이브러리 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<!--<version>11.0.0</version>-->
</dependency>
<!-- jstl 사용을 위한 라이브러리들 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<!--<version>6.1.0</version>-->
<!--<scope>provided</scope>-->
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<!--<version>3.0.2</version>-->
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<!--<version>3.0.1</version>-->
</dependency>
<!-- BCryptPasswordEncoder 사용을 위한 라이브러리 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<!--<version>5.7.5</version>-->
</dependency>
<!-- MyBatis, Oralce 관련 라이브러리-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<scope>runtime</scope>
</dependency>
<!-- log4jdbc 라이브러리 -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
<version>1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<!-- 웹소켓 관련 starter 라이브러리 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 웹소켓 관련한 dependency가 하나 추가된걸 볼수 있다
starter가 붙은건 집약형태다.
ㅁ (스프링) servlet.contxet.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<resources mapping="/upload/**" location="file:///upload/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.br.spring" />
<!-- webSocket 관련 등록 구문 -->
<beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />
<websocket:handlers>
<websocket:mapping handler="chatEchoHandler" path="/chat" />
<websocket:handshake-interceptors>
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
</websocket:handshake-interceptors>
<websocket:sockjs />
</websocket:handlers>
<!-- interceptor 관련 등록 구문 -->
<interceptors>
<interceptor>
<mapping path="/member/myinfo.do" />
<mapping path="/board/regist.do" />
<beans:bean class="com.br.spring.interceptor.LoginCheckInterceptor" id="LoginCheckInterceptor"/>
</interceptor>
<!--
<interceptor>
</interceptor>
-->
</interceptors>
<!-- Scheduler -->
<task:annotation-driven />
</beans:beans>
- 스프링을 쓸 때는 servlet-context.xml에
- <beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />
ChatEchoHandler가 빈으로 등록되어있어야 하고,
- <websocket:mapping handler="chatEchoHandler" path="/chat" />
어떤 url요청시 어떤 핸들러를 구동시킬건지 작성되어있어야 하고
-<websocket:handshake-interceptors>
handshake ineterceptors를 등록했고
- <websocket:sockjs />
sockjs 사용을 위한 구문을 작성했었다.
ㅁ ChatEchoHandler
package com.br.boot.handler;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
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.boot.dto.MemberDto;
import lombok.extern.slf4j.Slf4j;
@Component // serlvet-context.xml에 빈등록 구문 작성 대체.
@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));
}
}
}
- 클래스 위에 @Component 어노테이션을 붙여서 serlvet-context.xml에 빈등록 구문 작성을 대체한다.
- 스프링 부트에는 xml 파일을 따로 안둬서 빈 등록하는 3방법 중 xml 파일로 빈을 등록하는 방법은 못 쓴다.
자바 방식으로 빈을 등록한다.
ㅁ com.br.boot.config에 WebSocketConfig (일반) 클래스 생성
package com.br.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import com.br.boot.handler.ChatEchoHandler;
import lombok.RequiredArgsConstructor;
@EnableWebSocket
@RequiredArgsConstructor
@Configuration
public class WebSocketConfig implements WebSocketConfigurer{
private final ChatEchoHandler chatEchoHandler;
/*
<websocket:handlers>
<websocket:mapping handler="chatEchoHandler" path="/chat" />
<websocket:handshake-interceptors>
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
</websocket:handshake-interceptors>
<websocket:sockjs />
</websocket:handlers>
*/
@Bean
HttpSessionHandshakeInterceptor HttpSessionHandshakeInterceptor() {
return new HttpSessionHandshakeInterceptor();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatEchoHandler, "/chat")
.addInterceptors(HttpSessionHandshakeInterceptor()) // 메소드 호출구문을 쓰면 저 객체가 생성되어서 온다. 이 객체를 인터셉터로 등록한다.
.withSockJS(); // <websocket:sockjs /> 대체
}
}
- (1) implements WebSocketConfigurer한다.
- (2) @Configuration 어노테이션을 붙인다.
- (3) 빨간줄 뜬다. 오버라이딩 하라고 뜬다. 오버라이딩 한다.
- (4) private final ChatEchoHandler chatEchoHandler; 선언
- (5) @RequiredArgsConstructor 어노테이션을 붙여서 주입한다.
- 오버라이딩한 비어있는 메소드에 내용 작성.
- <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
근데 이걸 빈으로 등록했었어야 했다.
근데 이건 우리가 만든 클래스가 아니라서 xml방식이나 자바 방식만 가능하다.
- 스프링 부트에 xml 파일은 별도로 없기 때문에 자바 방식으로 얘를 빈 등록 한다.
웹소켓 관련 빈이기 때문에 WebSocketConfig 클래스에 @Bean 어노테이션을 붙여서 HttpSessionHandshakeInterceptor를 빈으로 등록한다.
- sockjs
- @EnableWebSocket
ㅁ 서버 start
- 브라우저 2개로 로그인하고 채팅이 잘 되나 확인한다. 잘 된다.