코딩마을방범대
웹 소켓을 이용한 메시지 전송(백엔드 - SpringBoot) 본문
WebSocket
- 웹버전의 TCP 또는 Socket
- 서버와 클라이언트 간에 Socket Connection을 유지해서 언제든 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술
- Real-time web application 구현을 위해 널리 사용되어지고 있음
(SNS 애플리케이션, LoL 같은 멀티플레이어 게임, 구글 Doc, 증권거래, 화상채팅 등)
인터넷의 채팅 기능들은 websocket을 통해 이루어짐
WebSocket Sevrer를 운용할 때의 유의사항
WebSocket은 하나의 URL을 통해 Connection이 맺어지고, 후에는 해당 Connection으로만 통신
- 서버와의 정기적인 HTTP 연결을 설정 한 다음 Upgrade헤더를 전송하여 양방향 웹 소켓 연결로 업그레이드
- HandShake가 완료되고 Connection을 유지
핸드셰이크 ( HandShake )
비대칭 암호화를 사용하여 서버와의 연결을 보호하는 프로세스
HTTP와 WebSocket의 차이점
HTTP에서 동작하나, 그 방식이 HTTP와는 많이 상이함
HTTP
요청-응답이 완료되면 Connection을 close
(이론상 하나의 Server가 Port 수의 한계(n <65535)를n<65535 넘는 client의 요청을 처리할 수 있음)
WebSocket
Connection을 유지하고 있으므로, 가용 Port 수만큼의 Client와 통신할 수 있음
웹소켓 프로토콜
soket.io
인터넷 익스플로러 구버전의 사용자는 webcoket으로 작성된 웹페이지를 볼 수 없습니다.
이를 해결하기위해 soket.io는 웹페이지가 열리는 브라우저가 websoket을 지원하면 일반 websoket방식으로 동작하고, 지원하지 않는 브라우저라면 http를 이용해 websoket을 흉내내는 방식으로 통신을 지원합니다.
soket.io는 nodeJS에 종속적 입니다.
soket.js
스프링에서 위와 같은 브라우저 문제를 해결하기 위한 방법으로 soketJS를 솔루션으로 제공합니다.
서버 개발시 일반 websoket으로 통신할지 SoketJS 호환으로 통신할지 결정할 수 있습니다.
그리고 클라이언트는 SoketJS client를 통해 서버랑 통신합니다.
stomp
stomp는 단순 (또는 스트리밍) 텍스트 지향 메시징 프로토콜입니다.
spring에 종속적이며, 구독방식으로 사용하고 있습니다.
가벼워서 보통 많이들 사용합니다.
Node.js 를 이용할땐 soket.io 를 주로 사용하고,
Spring을 사용할땐 soket.js, stomp 를 주로 사용합니다.
사용 방법
※ 모든 소스는 본인의 프로젝트에 맞게 수정이 필요하며, 간단한 예제만을 넣은 것이기 때문에 그냥 참고용임!
Build.gradle
implementation 'org.springframework.boot:spring-boot-starter-websocket'
Handler
웹소켓 연결 전후로 데이터를 변형시킬 수 있다.
@Component
public class WebsocketHandler implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
HttpServletRequest servletRequest = (HttpServletRequest) request;
// request 를 통해 값 가져오기
String userKey = servletRequest.get("userKey");
// attribuets에 세팅해놓으면 웹소켓 연결 후 가져올 수 있음
attributes.put("userKey", userKey);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
@EventListener
public void connectedWebsocketHandler(SessionConnectedEvent sessionConnectedEvent) {
MessageHeaderAccessor messageHeaderAccessor = NativeMessageHeaderAccessor.getAccessor(sessionConnectedEvent.getMessage(), SimpMessageHeaderAccessor.class);
GenericMessage generic = (GenericMessage) messageHeaderAccessor.getHeader("simpConnectMessage");
Map<String, String> simpSessionAttributes = (Map<String, String>) generic.getHeaders().get("simpSessionAttributes");
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(sessionConnectedEvent.getMessage());
String userKey = simpSessionAttributes.get("userKey");
authEventService.subscribe(userKey, headerAccessor.getSessionId());
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
authEventService.unsubscribe(null, headerAccessor.getSessionId());
}
}
1. beforeHandshake()
소켓 연결 시 Handshake 전 함수,
※ 인자인 attributes에 핸들러로 넘겨줄 데이터를 저장할 수 있음
2. afterHandshake()
Handshake 완료 후 함수
3. connectedWebsocketHandler()
소켓 연결 이후 이벤트 함수
@EventListener
어노테이션이 붙은 메소드의 인자로 받은 이벤트가 발생되었을 경우 실행된다.
4. handleWebSocketDisconnectListener()
소켓 연결이 끊겼을 때의 이벤트 함수
SessionDisconnectEvent
WebSocket 하위 프로토콜(예: STOMP)이 닫혔을 때 실행
Configuration
프론트에서 구독할 경로 및 적용시킬 인터셉터, 브로커 등을 설정함
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker//@EnableWebSocketMessageBroker is used to enable our WebSocket server
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final WebsocketHandler sessionHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/db","/test")
.addInterceptors(sessionHandler)
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 메시지를 보낼 때 사용됨
registry.enableSimpleBroker("/topic", "/queue");
// 메시지를 받을 때 사용됨
registry.setApplicationDestinationPrefixes("/app");
}
}
@EnableWebSocketMessageBroker
WebSocket 서버를 활성화하는 데 사용
implements WebSocketMessageBrokerConfigurer
웹 소켓 연결을 구성하기 위한 메서드를 구현하고 제공
1. registerStompEndpoints()
클라이언트가 웹 소켓 서버에 연결하는 데 사용할 웹 소켓 엔드 포인트를 등록
- addEndpoint(path): 주어진 매핑 경로에서 STOMP를 등록
- addInterceptors(인터셉터): 적용할 인터셉터 설정
- setAllowedOrigins("*"): 전체 메소드 허용
- 엔드 포인트 구성에 withSockJS ()를 사용
결과적으로 응답이 websocket:true 이면 웹소켓 연결된다.
SockJS는 웹 소켓을 지원하지 않는 브라우저에 폴백 옵션을 활성화하는 데 사용
Fallback
어떤 기능이 약해지거나 제대로 동작하지 않을 때, 이에 대처하는 기능 또는 동작
2. configureMessageBroker()
한 클라이언트에서 다른 클라이언트로 메시지를 라우팅 하는 데 사용될 메시지 브로커를 구성
라우팅 ( Routing )
어떤 네트워크 안에서 통신 데이터를 보낼 때 최적의 경로를 선택하는 과정
registry.setApplicationDestinationPrefixes("/app")
- 매칭한 구독 접두사와 연결하여 메시지를 구독하고 있는 클라이언트에게 전달
( /dbkim/sub 라는 토픽에 대해 구독했을 때 실제 경로는 /app/dbkim/sub 가 되는 것)
registry.enableSimpleBroker("/topic")
- 메시지 브로커를 등록시켜줌
- "/topic"과 "/queue"를 사용하며, topic은 해당 토픽을 구독하고 있는 다수에게 메시지를 모두 전송,
queue는 메시지를 발행한 한 명에게 다시 정보를 보내는 경우에 사용
메시지 브로커는 특정 주제를 구독한 연결된 모든 클라이언트에게 메시지를 브로드캐스팅함
브로드캐스팅 ( Broadcasting)
송신 호스트가 전송한 데이터가 네트워크에 연결된 모든 호스트에 전송되는 방식
Service
브로커를 통한 전달(1:N)은 컨트롤러에서 @SendToUser을 사용하여 쉽게 전달할 수 있으나
직접 전달(1:1) 시에는 SimpMessagingTemplate으로 전달할 수 있음
( 브로커를 통한 전달은 아래 Controller 참고! )
@Component
@RequiredArgsConstructor
public class AuthEventService {
private final SimpMessagingTemplate simpMessagingTemplate;
private Map<String, Set<String>> sessionSetMap = new ConcurrentHashMap<>();
private Map<String, String> sessionKeyMap = new ConcurrentHashMap<>();
public boolean subscribe(String userKey, Object value) {
String sessionId = (String) value;
Set<String> sessionIdSet = sessionSetMap.get(userKey);
if (sessionIdSet == null) {
sessionIdSet = ConcurrentHashMap.newKeySet();
sessionSetMap.put(userKey, sessionIdSet);
}
sessionIdSet.add(sessionId);
sessionKeyMap.put(sessionId, userKey);
return true;
}
public boolean unsubscribe(String userKey, Object value) {
String sessionId = (String) value;
// 핸들러에서 설정한 바와 같이 userKey값이 null로 오기 때문에 KeyMap에서 유저키 가져오기
userKey = sessionKeyMap.get(sessionId);
// ...
sessionSetMap.get(userKey).remove(sessionId);
sessionSetMap.remove(userKey);
return false;
}
public void sendData(String userKey, Map<String, String> data) throws KnChainException {
Set<String> sessions = sessionSetMap.get(userKey);
if (sessions != null && !sessions.isEmpty()) {
Map<String, Object> msgMap = new HashMap<>();
msgMap.put("type", "data");
msgMap.putAll(data);
sessions.parallelStream()
.forEach(sessionId -> {
try {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue", msgMap, headerAccessor.getMessageHeaders());
} catch (Exception e) {
log.error("sendStompMessage exception : ", e);
}
});
}
}
}
SimpMessageHeaderAccessor.create(메시지타입)
변수의 SimpMessageType을 통해 SimpMessageHeaderAccessor 인스턴스 생성
headerAccessor.setSessionId(세션아이디)
해당 세션아이디로 메시지를 전달 할 수 있음
headerAccessor.setLeaveMutable(boolean)
true 일 경우 변경 가능, false 일 경우 변경 불가능
messagingTemplate.convertAndSendToUser
@SendToUser와 다르게 헤더정보가 없으므로 직접 입력해주어야 함
클라이언트가 브로커를 통해서가 아닌
직접 메세지를 받기 위해서는 구독 시 주소 앞에 /user이 붙여야 함// 브로커를 통한 메세지 수신 @SendToUser session.subscribe(“/queue”, brokerMessageHandler); // 직접 메세지 수신 convertAndSendToUser session.subscribe(“/user/queue”, directMessageHandler);
Controller
클라이언트가 메시지를 보낸 경우에 받을 컨트롤러가 필요함
( 기존 컨트롤러에 추가해도 무방 )
위에 Configuration 설정 때 'setApplicationDestinationPrefixes' 를 /app으로 해주었기 때문에,
프론트엔드에선 /app/messages 를 호출해야 하나, 백에서는 configuration에서 자동으로 app을 붙여준다.
※ build.gradle에 json 의존성 추가 필요!
@MessageMapping("/messages") // 클라이언트가 메시지를 보내는 경로
@SendTo("/topic/user") // topic user를 구독한 모든 클라이언트에게 메시지 전송
public void chat(@RequestBody String str) {
JSONObject jsonObject = new JSONObject(str);
String name = jsonObject.getString("name")
}
참고사이트
[Spring]Springboot + websocket 채팅[1]
[Spring] WebSocket 특정 유저로 메세지 보내기
WebSoket (stompJS+React) 채팅 - 1
'💡 백엔드 > Java' 카테고리의 다른 글
실행 가능한 war 파일 만들기 (0) | 2023.06.08 |
---|---|
디버깅 실행 시 application이 정상적으로 실행 되지 않을 경우 (0) | 2023.06.01 |
실무에서 쓰이는 어노테이션 및 메소드 (0) | 2023.05.31 |
@Valid와 @Validated (0) | 2023.05.31 |
GenerationType의 IDENTITY와 SEQUENCE (0) | 2023.05.29 |