
⇒ 문서 전달
⇒ HTTP는 클라이언트에게 요청이 와야만 응답을 할 수 있다.
⇒ 이 한계를 극복하기 위해서는 실시간 양방향 통신이 필요

⇒ 해당 과정을 Handshake라고 함
⇒ 클라이언트와 서버가 웹소켓 통신을 하기 위한 일련의 과정을 Handshake라고 부름
⇒ 이때부터 양방향 통신이 이뤄짐 ⇒ 클라이언트 ↔ 서버
⇒ 각각 먼저 보낼 수 있음
⇒ close 또한 양쪽에서 진행할 수 있음
<!---- sockJS 를 이용한 WebSocket 구현을 위해 라이브러리 추가 ------->
<!-- https://github.com/sockjs/sockjs-client -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
implementation 'org.springframework.boot:spring-boot-starter-websocket'
⇒ 해당 모듈이 존재해야함
package edu.kh.project.websocket.config;
import edu.kh.project.websocket.handler.TestWebSocketHandler;
import lombok.RequiredArgsConstructor;
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.HandshakeInterceptor;
@Configuration // 서버 실행 시 작성된 메서드를 모두 수행 + Bean 등록
@EnableWebSocket // 웹소켓 활성화 설정
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final TestWebSocketHandler testWebSocketHandler;
private final HandshakeInterceptor handshakeInterceptor;
// 의존성 주입 : 같은 타입 or 상속관계의 타입,
// 즉 Bean으로 등록된 SessionHandshakeInterceptor가 의존성 주입됌
// WebSocketConfigurer :
// Spring에서 웹소켓 통신을 어디에서, 어떤 방식으로 할 것인지
// 규칙을 정의하는 설정용 인터페이스
// 1. 핸들러 등록
// 2. 접속 주소 매핑
// 3. 부가 기능 설정
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// addHandler(웹소켓핸들러, "웹소켓 요청 주소")
registry.addHandler(testWebSocketHandler, "/testSock")
// 클라이언트가 http://localhost/testSock으로 클라이언트 요청을 보내면
// Websocket 통신으로 변환 후 testWebsocketHandler가 처리하도록 등록
.addInterceptors(handshakeInterceptor)
// 클라이언트 연결 시 session을 가로채 핸들러에게 전달하는 handshakeInterceptor 등록
.setAllowedOriginPatterns("http://localhost/", "http://127.0.0.1/", "http://192.168.132.20")
// 웹소켓 요청이 허용되는 ip/도메인 지정
.withSockJS();
}
}
package edu.kh.project.websocket.interceptor;
import java.util.Map;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import jakarta.servlet.http.HttpSession;
// SessionHandshakeInterceptor 클래스
// - WebSocketHandler가 동작하기 전/후에
// 연결된 클라이언트의 세션을 가로채는 동작을 작성할 클래스
// Handshake : 클라이언트와 서버가 Websocket 연결을 수립하기 위해 HTTP 프로토콜을 통해
// 수행하는 초기 단계
// -> 즉, 기존 HTTP 연결을 WebSocket 연결로 변경
// 클라이언트마다 고유한 세션을 가짐
// 클라이언트의 세션을 가져와서 해당 클라이언트에게 메세지를 보낸다는 것을 해주기 위해
// 클라이언트의 세션을 가로채는 동작이 필요
// 핸드쉐이크 코드는 js에서 진행
@Component // 일반적인 Bean으로 등록할 때 사용하는 어노테이션
public class SessionHandshakeInterceptor implements HandshakeInterceptor {
// 핸들러 동작 전에 수행되는 메서드
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
// ServerHttpRequest : HttpServletRequest의 부모 인터페이스
// ServerHttpResponse : HttpServletResponse의 부모 인터페이스
// attributes : 해당 맵에 세팅된 속성(데이터)은
// 다음에 동작할 Handler 객체에게 전달됨
// HandshakeInterceptor -> Handler 데이터 전달하는 역할
// request가 참조하는 객체가
// SetvletServerHttpRequest로 다운캐스팅이 가능한가?
if (request instanceof ServletServerHttpRequest) {
// instanceof : 부모 자식관계인지 확인하는 키워드
// 다운캐스팅
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
// ServerHttpRequest(부모) -> ServletServerHttpRequest(자식)
// 웹소켓 동작을 요청한 클라이언트의 세션을 얻어옴
HttpSession session = servletRequest.getServletRequest().getSession();
// servletRequest.getServletRequest() = 여기까지가 request
// request.getSession() 가져오기
// 가로챈 클라이언트의 세션을 Handler에 전달할 수 있게 세팅
attributes.put("session", session);
}
// true or false에 따라 인터셉터를 진행할지 말지를 결정
// 즉, 가로채기 진행 여부 : true로 작성해야 세션을 가로채서 핸들러에게 전달하는게 가능해짐
// 현재까지는 클라이언트에서 서버에게 요청을 보내는 (session전달) 역할을 하는 코드를 작성한 것
return true;
}
// 핸들러 동작 후에 수행되는 메서드
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
}
package edu.kh.project.websocket.handler;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import lombok.extern.slf4j.Slf4j;
// OOOWebSocketHandler 클래스
// - 웹소켓 동작 시 수행할 구문을 작성하는 클래스
// 즉, 어떤 동작을 하고싶은 것인지를 의미
@Component
@Slf4j
public class TestWebSocketHandler extends TextWebSocketHandler {
// 동기화된 Set 생성
private Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>());
// -> 여러 스레드가 동작하는 환경에서 하나의 컬렉션에 여러 스레드 접근하여 의도치 않은 문제가
// 발생되지 않게 하기 위해 동기화를 진행하여 스레드가 순서대로 동작할 수 있도록 함.
// WebSocketSession :
// 클라이언트 - 서버 간 양방향 통신을 담당하는 객체
// SessionHandshakeInterceptor가 가로챈
// 연결된 클라이언트의 HttpSession을 가지고 있음
// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행하는 메서드
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 연결된 클라이언트의 WebSocketSession 정보를 Set에 추가
// -> 웹소켓에 연결된 클라이언트 정보를 모아두기 위함
sessions.add(session);
}
// 클라이언트와 연결이 종료되면 실행
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 웹소켓 연결이 끊긴 클라이언트 정보를 Set에서 제거
sessions.remove(session);
}
// 클라이언트로부터 텍스트 메시지를 받았을 때 실행
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// TextMessage : 웹소켓으로 연결된 클라이언트가 전달한
// 텍스트(내용)가 담겨있는 객체
// message.getPayload() : 통신 시 탑재된 데이터 자체
log.info("전달받은 메시지 : {}", message.getPayload());
// 전달 받은 메시지를
// 현재 해당 웹소켓에 연결된 모든 클라이언트에게 보내기
for (WebSocketSession s : sessions) {
s.sendMessage(message);
}
}
}
/*
* WebSocketHandler 인터페이스 : 웹소켓을 위한 메소드를 지원하는 인터페이스 -> WebSocketHandler 인터페이스를
* 상속받은 클래스를 이용해 웹소켓 기능을 구현 WebSocketHandler 주요 메소드
*
* void handlerMessage(WebSocketSession session, WebSocketMessage message) -
* 클라이언트로부터 메세지가 도착하면 실행
*
* void afterConnectionEstablished(WebSocketSession session) - 클라이언트와 연결이 완료되고,
* 통신할 준비가 되면 실행 void afterConnectionClosed(WebSocketSession session,
* CloseStatus closeStatus) - 클라이언트와 연결이 종료되면 실행 void
* handleTransportError(WebSocketSession session, Throwable exception) - 메세지 전송중
* 에러가 발생하면 실행 ----------------------------------------------------------------
* TextWebSocketHandler : WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메세지 전용 웹소켓 핸들러
* 클래스 handlerTextMessage(WebSocketSession session, TextMessage message) -
* 클라이언트로부터 텍스트 메세지를 받았을때 실행
*
* BinaryWebSocketHandler: WebSocketHandler 인터페이스를 상속받아 구현한 이진 데이터 메시지를 처리하는 데
* 사용. 주로 바이너리 데이터(예: 이미지, 파일)를 주고받을 때 사용.
*/
// websocket_test.js
// 웹소켓 테스트
// 1. SockJS 라이브러리 추가
// -> common.html 추가해둠
// 2. SockJS 객체 생성
const testSock = new SockJS("/testSock");
// - WebSocket 통신을 할 수 있게끔 해주는
// 객체를 생성함과 동시에 자동으로
// (localhost)/test으로 연결 요청을 보냄
// => 서버에게 보내는 요청 -> http://localhost/testSock으로 연결 요청을 보냄
// => 추후 http://는 ws://로 바뀜
// 3. 생성된 SockJS 객체를 이용해서 서버에 메시지 전달
const sendMessageFn = (name, str) => {
// JSON을 이용해서 데이터를 TEXT 형태로 전달
const obj = {
name: name,
str: str,
};
testSock.send(JSON.stringify(obj));
};
// sendMessageFn("홍길동", "안녕하세요~");
// 4. 서버로부터 클라이언트에게
// 웹소켓을 이용한 메시지가 전달된 경우
testSock.addEventListener("message", (e) => {
// e.data : 서버로부터 전달받은 message
const msg = JSON.parse(e.data); // 서버로부터 JSON 형태로 받아온 것을 JS 객체 형태로 변환
console.log(`${msg.name}의 메세지 : ${msg.str}`);
});
