참고 :
https://dev-gorany.tistory.com/212
https://dev-gorany.tistory.com/235
공식문서:
https://stomp.github.io/index.html
https://docs.spring.io/spring-framework/reference/web/websocket/stomp/enable.html
implementation ('org.springframework.boot:spring-boot-starter-websocket') implementation 'org.webjars:sockjs-client:1.5.1'
https://docs.spring.io/spring-framework/reference/web/websocket/stomp/enable.html
@EnableWebSocketMessageBroker
@Configuration
@RequiredArgsConstructor
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ChatPreHandler chatPreHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/game")
.setAllowedOrigins("https://www.seop.site")
.withSockJS();
}
/*어플리케이션 내부에서 사용할 path를 지정할 수 있음*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// pub 경로로 수신되는 STOMP메세지는 @Controller 객체의 @MessageMapping 메서드로 라우팅 됨
// SipmleAnnotationMethod, 메시지를 발행하는 요청 url => 클라이언트가 메시지를 보낼 때 (From Client)
registry.setApplicationDestinationPrefixes("/pub");
// SimpleBroker, 클라이언트에게 메시지를 보낼 때 (To Client)
registry.enableSimpleBroker("/sub");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(chatPreHandler);
}
}
우선 STOMP 웹 소켓에 연결할 엔드포인트를 설정해준다.
- 해당 엔드 포인트는 웹소켓 연결을 위해 3-handshake를 수행하기 위한 URL
- reistry.addEndpoint("/stomp/game") : "/stomp/game"이라는 URL로 요청이 가능하다.
- setAllowedOrigins: 서버 호스트도메인 서버를 설정
- withSockJS() : WebSocket을 지원하지 않는 클라이언트에게 WebSocket Emulation을 통해 웹소켓을 지원하기 위한 설정
함수명에서 뜻하듯이 메시지 전달을 위한 브로커 함수이다.
- registry.setApplicationDestinationPrefixes("/컨트롤러에 라우팅할 URI")
- 서버로 수신되는 STOMP 메시지 중에서 @MessageMapping으로 매핑된 컨트롤러에 전달하기 위한 URI Prefix를 설정한다.
- registry.enableSimpleBroker("/브로드캐스트 URI")
- Subscription을 위한 Spring에 내장된 메세지 브로커를 이용하여 라우팅 된 메시지를 목적지에 브로드캐스팅 하기 위한 URI Prefix를 설정한다.
- 웹 소켓 클라이언트는 /sub로 시작하는 URI를 구독함으로 써, STOMP 웹 소켓 서버에서 publishing된 메시지가 요청 될 때 /sub를 구독하고 있는 모든 웹소켓 클라이언트에게 브로드 캐스팅 메시지를 전송한다.
웹 소켓 연결에 관련하여 들어오는 클라이언트의 동작 메시지를 Interceptor를 통해서 식별하기 위한 함수
- 연결 이슈로 인해서 웹소켓 연결 및 종료 시 동작하는 과정을 디버깅하기 위해 작성
@RequiredArgsConstructor @Component public class ChatPreHandler extends ChannelInterceptorAdapter { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { // 클라이언트(외부)에서 받은 메세지를 Stomp 프로토콜 형태의 메세지로 가공하는 작업 StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); StompCommand command = accessor.getCommand(); // 1번 방법: command null일 경우는 무시 if (command != null) { switch (command) { case CONNECT: System.out.println("유저 접속..."); break; case DISCONNECT: System.out.println("유저 퇴장..."); break; case SUBSCRIBE: System.out.println("유저 구독..."); break; case UNSUBSCRIBE: System.out.println("유저 구독 취소..."); return null; default: System.out.println("다른 커맨드... : " + command); break; } } return message; } }
https://docs.spring.io/spring-framework/reference/web/websocket/stomp/client.html
안드로이드 OS에서 JAVA로 STOMP 클라이언트의 동작을 구현하였다.
의존성
implementation 'com.github.NaikSoftware:StompProtocolAndroid:1.6.6'
public void initStomp(int matchIdx) {
sockClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, "wss://www.seop.site" + "/stomp/game/websocket"); // 소켓연결 (엔드포인트)
AtomicBoolean isUnexpectedClosed = new AtomicBoolean(false);
// 웹소켓 생명주기 이벤트 핸들러
sockClient.lifecycle().subscribe(lifecycleEvent -> { // 라이프사이클 동안 일어나는 일들을 정의
switch (lifecycleEvent.getType()) {
case OPENED: // 오픈될때는 무슨일을 하고~~~ 이런거 정의
break;
case ERROR:
if (lifecycleEvent.getException().getMessage().contains("EOF")) {
isUnexpectedClosed.set(true);
}
break;
case CLOSED:
if (isUnexpectedClosed.get()) {
/**
* EOF Error
*/
initStomp(matchIdx);
isUnexpectedClosed.set(false);
}
break;
}
});
// 웹 소켓 연결
sockClient.connect();
// Subscribe Topid ID로 오는 메시지 핸들러 선언
sockClient.topic("/sub/game/room/" + matchIdx).subscribe(topicMessage -> {
JsonParser parser = new JsonParser();
BroadCastDataResponse data = new Gson().fromJson(topicMessage.getPayload(),BroadCastDataResponse.class);
}, System.out::println);
}
}
서버의 웹소켓 엔드포인트에 3-handshake를 통해서 연결을 시작하기 위한 요청
- 웹 서버의 configure 클래스의 registerStompEndPoints()에서 지정한 엔드포인트를 지정하여 연결 요청을 한다.
- http를 통한 연결은
"ws://www.example.com/엔드포인트/websocket"
- https를 통한 연결은
"wss://www.example.com/엔드포인트/websocket"
엔드 포인트 뒤에 /websocket 은 붙여도되고 붙이지 않아도 되는 두가지 경우를 찾아 볼 수 있었다.
STOMP WebSocket의 라이프싸이클에 대한 이벤트 핸들러 함수
- CLOSED 됐을때 즉 websocket이 닫혔을때 다시 재연결을 요청하도록 하였다.
- 웹소켓 종료를 위한 사전 정의한 메시지가 오기전까지 연결을 계속 유지하기 위함.
WebSocket 연결 본격 요청
- 3-handshake를 통해서 본격적으로 서버와 웹소켓 연결을 하기 위함이다.
- 서버에서 http와 같은 stateless 요청보다 더 윗단계인 세션 유지를 할 수 있도록 연결상태를 "upgrade"할 수 있도록 미리 설정해 놓아야 연결이 완성된다.
STOMP 라이브러리에서 Subscribe(구독)의 기본 단위인 Topic ID를 통해서 다중 웹 소켓 연결을 가능하도록 한다.
"/sub"
은 웹 애플리케이션 서버에서 지정한 메시지 브로드캐스팅 브로커의 Prefix- 해당
/sub/추가URI/+topicID
를 통해서 Publishing된 메시지를 수신 가능하다.- topic().subscribe() 함수를 통해서 핸들러 함수를 작성할 수 있다.
WebSocketMessageBrokerConfigurer 에서 설정한 AnotationMessage Handler로 지정한 Publishing 메시지의 요청을 수행할 컨트롤러 클래스
@Controller
@RequiredArgsConstructor
public class StompGameController {
private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달
//"/pub/chat/enter"
@MessageMapping(value = "/game/enter")
public void enter(ChatMessageDTO message){
message.setMessage(message.getWriter() + " 님이 매칭방에 참여하였습니다.");
template.convertAndSend("/sub/game/room/" + message.getMatchIdx(), message);
}
@MessageMapping(value = "/game/message")
public void message(ChatMessageDTO message){ // 점수 DTO로 수정해야함
System.out.println(message.getMatchIdx() + ": " + message.getWriter() + " -> " + message.getMessage());
template.convertAndSend("/sub/game/room/" + message.getMatchIdx(), message);
}
@MessageMapping(value = "/game/start-game")
public void messageToClient(AdminSendScoreDTO message){
System.out.println(message.getMatchIdx() + ": " + message.getWriter() + " -> " + message.getScore() + " score ");
template.convertAndSend("/sub/game/room/" + message.getMatchIdx(), new AdminSendScoreDTO(message.getPlayerNum(), message.getMatchIdx(),message.getWriter(),message.getScore()));
}
}
컨트롤러의 매핑 함수에 들어오는 메시지는 Publishing 메시지와 동일한 형태
- Publishing 메시지를 커스텀 설정하여 프로토콜을 마음대로 정의할 수 있다.
package org.springframework.messaging.simp;
STOMP 메시지를 브로드캐스트 전송하기 위해 브로커에게 전달해주는 Template 클래스
SimpleMessagingTemplate.convertAndSend()
- 메시지를 헤더와 결합하고 Spring에서 지원하는 Message 클래스를 통해 변환한 다음
- 추상 메서드 doSend() 함수를 통해서 Topic ID Subscribe 웹 소켓 클라이언트에게 브로드캐스트 전송을 진행한다.
- 해당 메세지 전송으로 인해 안드로이드에서 작성한 sockClient.topic().subscribe() 에서 핸들러 함수를 통해 메시지에 의한 동작이 수행된다
@Override public void convertAndSend(D destination, Object payload) throws MessagingException { convertAndSend(destination, payload, (Map<String, Object>) null); }
@Override public void convertAndSend(D destination, Object payload, @Nullable Map<String, Object> headers) throws MessagingException { convertAndSend(destination, payload, headers, null); }
@Override public void convertAndSend(D destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException { Message<?> message = doConvert(payload, headers, postProcessor); send(destination, message); }
잘 설명해주신 블로그: https://dev-gorany.tistory.com/330
nginx 공식 문서 : https://www.nginx.com/blog/websocket-nginx/
location /wsapp/ {
proxy_pass http://wsbackend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}