Spring Boot Web Socket

송지윤·2024년 5월 22일

Spring Framework

목록 보기
62/65

HTTP 프로토콜 사용 (Hypertext Transfer Protocol)
통신 규약
웹페이지 (HTML, image 등등) 전송하기 위해서 사용하는 것

-> 웹 브라우저 통해서 서버가 전송한 HTML 파일 확인 가능
(단순 문서 전달)

문서 전달 뿐 아니라 그 이상의 것을 원함

실시간 통신
-> 채팅, 알림, 주식 가격 실시간 전달...

HTTP 통신의 한계
요청을 보내면 응답을 가지고 되돌아옴 (클라이언트 <-> 서버)
요청이 있어야만 응답을 줄 수 있음 -> 실시간 통신의 한계점
(하나의 요청을 하고 응답을 받으면 통신 끝)

서버 변동 시 알아서 클라이언트에게 줘야함 (실시간 통신)

Websocket

클라이언트가 서버에게 맨 처음에 요청을 보냄
HTTP 통신이지만 Websocket 통신으로 바꾸고 싶다고 HandShake 요청을 보냄
ws:// 로 바뀜
이 때부터는 클라이언트가 요청 보내지 않아도 서버에 변동이 있으면 알려줌 (서버에서 실시간으로 보내줄 수 있음)

Websocket 라이프 사이클

양방향 통신을 실시간으로 가능하게 해주는 프로토콜

websocket test

Handshake 가장 먼저 필요함 -> interceptor 동작 방식으로 흘러감
클래스 생성

websocket.interceptor 패키지에 SessionHandshakeInterceptor 클래스 생성

HandshakeInterceptor interface 상속 받아서 사용

처음에 HTTP 통신으로 요청 보냄 그걸 웹소켓 연결 설정하기 위해 필요한 interface

ctrl + c afterHandshake, beforeHandshake 두개 상속 받아서 사용

beforeHandshake 메서드 작성

매개변수

  • ServerHttpRequest : HttpServletRequest 의 부모 인터페이스
  • ServerHttpResponse : HttpServletResponse 의 부모 인터페이스
  • attributes : 해당 맵에 세팅된 속성(데이터)은 다음에 동작할 Handler 객체에게 전달됨
    (HandshakeInterceptor -> Handler 데이터 전달하는 역할)

ServletServerHttpRequest -> ServerHttpRequest 의 자식 => 여기서 session 을 뽑아와야함
다운캐스팅 중 예외 발생할 수 있음
request 가 참조하는 객체가
ServletServerHttpRequest 로 다운캐스팅이 가능한가를 처리해주는 if문 작성

@Component // Bean 으로 등록해야함 웹소켓 통신할 때 interceptor 가 필요한 곳에 주입돼서 사용되려면 꼭 등록돼있어야함
public class SessionHandshakeInterceptor implements HandshakeInterceptor {

	// 핸들러 동작 전에 수행되는 메서드
	@Override
	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> attributes) throws Exception {

		if(request instanceof ServletServerHttpRequest) {
			
			// 다운 캐스팅
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
			
			// 웹 소켓 동작을 요청한 클라이언트의 세션을 얻어올 것
			HttpSession session = servletRequest.getServletRequest().getSession();
			
			// 가로챈 세션을 Handler 에 전달할 수 있게 값을 세팅
			attributes.put("session", session);
		}
		
		// 가로채기 할지 말지 여부를 작성하는 곳
		// true 로 작성해야 세션을 가로채서 Handler 에게 전달 가능 (기본값이 false)
		return true;
	}
	
	@Override
	public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Exception exception) {
		// TODO Auto-generated method stub
		
	}
	
}

interceptor 이후 Handler 에게 넘겨줄 값 세팅 후 Handler 클래스 생성

websocket.handler 패키지 안에 TestWebsocketHandler 클래스 생성

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 인터페이스를 상속받아 구현한
	이진 데이터 메시지를 처리하는 데 사용.
	주로 바이너리 데이터(예: 이미지, 파일)를 주고받을 때 사용.

TextWebSocketHandler 상속 받아서 사용

웹소켓 통신할 때 여러 명 클라이언트가 들어올 수 있음 모든 애들의 session 가져와서 서버에서 수집하기 위해 만듦
요청을 보냈는데 동기로 받으면 session 꼬이는 순간이 발생함
-> 여러 클라이언트가 동시에 연결되고 동시에 종료되게 (순서 꼬이지않고) 데이터 일관성있게 하기 위해
여러 가지 스레드가 동작하는 환경에서 하나의 컬렉션에 여러 스레드가 접근하여 의도치 않은 문제가 발생되지 않게
하기 위해서 동기화를 진행하여 스레드가 순서대로 한 컬렉션에 접근할 수 있도록 변경. (보호하는 역할)

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

TestWebsocketHandler

@Slf4j
@Component // Bean 등록 필요한 곳에 주입되어 사용돼야함 (의존성 주입 받아)
public class TestWebsocketHandler extends TextWebSocketHandler {
	
	// Collections.synchronizedSet
	private Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>());
	
	// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행하는 메서드
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {

		// session 은 Interceptor 에서 가로채 온 session 이 매개변수로 들어온 거
		// -> 가로채온 session 을 set 에 추가할 거
		// 연결된 클라이언트의 WebSocketSession 정보를 Set 에 추가
		// -> 웹소켓에 연결된 클라이언트 정보를 모아둠
		sessions.add(session);
	}
	
	// 클라이언트와 연결이 종료되면 실행하는 메서드 (채팅방 나간 경우 sessions 에서 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);
		}
		
	}
}

Set 중복 안됨 순서 유지 X (-> session 이 중복돼서 들어오지 않음)

WebSocketSession -> 웹소켓 연결을 나타내는 객체 클라이언트와 서버간 개별적 연결을 나타내는 객체
클라이언트와 서버 간에 전이중 통신을 담당하는 객체 (양방향 왔다갔다할 수 있는 통신)
이전에 만들어둔 SessionHandshakeInterceptor 가 가로챈 연결한 클라이언트의 HttpSession 값을 가지고 있음
-> attributes 에 추가한 값

동기화된 Set 생성
동기와 비동기의 차이
비동기는 하나의 작업이 끝나기 전에 작업을 시작하는 것
동기는 하나의 작업이 끝나면 작업을 시작하는 것

어디서 어떻게 이용될지 설정용 클래스 하나 생성해줘야함 websocket.config 패키지 안에 WebSocketConfig 클래스 생성

WebSocketConfig 클래스

@Configuration // 서버 실행 시 작성된 메서드를 모두 수행할 수 있게끔 해주는 어노테이션
@EnableWebSocket // 웹소켓 활성화 설정 어노테이션
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer{

	// Bean 으로 등록된 SessionHandshakeInterceptor 가 주입됨 (HandshakeInterceptor 자식)
	private final HandshakeInterceptor handshakeInterceptor;
	
	// 웹소켓 처리 동작이 작성된 객체 의존성 주입
	private final TestWebsocketHandler testWebsocketHandler;
	
	// 웹소켓 핸들러를 등록하는 메서드 (핸들러를 등록해줘야 이용할 수 있음)
	// 어떤 interceptor 이용해서 가로채왔는지도 알려줘야함
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		// addHandler(웹소켓 핸들러, 웹소켓 요청 주소)
		// 자바스크립트에서 써줄 요청주소를 똑같이 써주면 됨
		
		registry.addHandler(testWebsocketHandler, "/testSock")
		// ws:// 웹소켓 프로토콜
		// ws://localhost/testSock 으로 클라이언트가 요청을 하면
		// testWebsocketHandler 가 처리하도록 등록하는 과정
		.addInterceptors(handshakeInterceptor)
		// 클라이언트 연결 시 HttpSession 을 가로채 핸들러에게 전달
		.setAllowedOriginPatterns("http://localhost/",
								"http://127.0.0.1/",
								"http://192.168.50.206/")
		// 웹 소켓 요청이 허용되는 ip/도메인 지정 (루프백 ip) 내 컴퓨터로만 접속 가능
		// 남들이 들어올 때 사용하려면 내 ip 주소도 적어줘야함
		.withSockJS();
		// javascript 에서 요청할 때 SockJS 연결
	}
}

클라이언트 쪽에서 websocket 설정 처리

common.html

sockjs를 이용한 WebSocket 구현을 위해 라이브러리 추가
https://github.com/sockjs/sockjs-client

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>

websocket_test.js

/* 웹소켓 테스트 */

// 1. SockJS 라이브러리 추가
// -> common.html 에 작성

// 2. SockJS 객체를 생성
const testSock = new SockJS("/testSock");
// - 객체 생성 시 자동으로
// ws://localhost(또는 ip)/testSock 으로 연결 요청을 보냄

// 3. 생성된 SockJS 객체를 이용해서 메세지 전달
const sendMessageFn = (name, str) => {

    // JSON 을 이용해서 데이터를 TEXT 형태로 전달
    const obj = {
        "name" : name,
        "str" : str
    };

    // 연결된 웹소켓 핸들러로 JSON 전달
    testSock.send(JSON.stringify(obj));
}

// 4. 서버로부터 현재 클라이언트에게 웹소켓을 이용한 메세지가 전달된 경우
testSock.addEventListener("message", e => {

    // e.data : 서버로부터 전달받은 message
    const msg = JSON.parse(e.data); // JSON 형태로 온 java 객체를 JS Object로 변환
    console.log(`${msg.name} : ${msg.str}`);
    // 홍길동 : Hi
});

새 시크릿 창 열기 각각 다른 클라이언트가 됨
F12

0개의 댓글