STOMP 웹 소켓 + Android

김성인·2023년 10월 17일
1

🍃 SpringBoot

목록 보기
14/18

참고 :
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

Dependency

implementation ('org.springframework.boot:spring-boot-starter-websocket')
implementation 'org.webjars:sockjs-client:1.5.1'

Enable STOMP

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);
    }
}

registerStompEndPoints()

우선 STOMP 웹 소켓에 연결할 엔드포인트를 설정해준다.

  • 해당 엔드 포인트는 웹소켓 연결을 위해 3-handshake를 수행하기 위한 URL
  • reistry.addEndpoint("/stomp/game") : "/stomp/game"이라는 URL로 요청이 가능하다.
  • setAllowedOrigins: 서버 호스트도메인 서버를 설정
  • withSockJS() : WebSocket을 지원하지 않는 클라이언트에게 WebSocket Emulation을 통해 웹소켓을 지원하기 위한 설정

configureMessageBroker()

함수명에서 뜻하듯이 메시지 전달을 위한 브로커 함수이다.

  • registry.setApplicationDestinationPrefixes("/컨트롤러에 라우팅할 URI")
    • 서버로 수신되는 STOMP 메시지 중에서 @MessageMapping으로 매핑된 컨트롤러에 전달하기 위한 URI Prefix를 설정한다.
  • registry.enableSimpleBroker("/브로드캐스트 URI")
    • Subscription을 위한 Spring에 내장된 메세지 브로커를 이용하여 라우팅 된 메시지를 목적지에 브로드캐스팅 하기 위한 URI Prefix를 설정한다.
  • 웹 소켓 클라이언트는 /sub로 시작하는 URI를 구독함으로 써, STOMP 웹 소켓 서버에서 publishing된 메시지가 요청 될 때 /sub를 구독하고 있는 모든 웹소켓 클라이언트에게 브로드 캐스팅 메시지를 전송한다.

configureClientInboundChannel()

웹 소켓 연결에 관련하여 들어오는 클라이언트의 동작 메시지를 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;
    }
}

클라이언트 STOMP 웹 소켓 연결

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);
    }
}

Stomp.over()

서버의 웹소켓 엔드포인트에 3-handshake를 통해서 연결을 시작하기 위한 요청

  • 웹 서버의 configure 클래스의 registerStompEndPoints()에서 지정한 엔드포인트를 지정하여 연결 요청을 한다.
  • http를 통한 연결은 "ws://www.example.com/엔드포인트/websocket"
  • https를 통한 연결은 "wss://www.example.com/엔드포인트/websocket"
    엔드 포인트 뒤에 /websocket 은 붙여도되고 붙이지 않아도 되는 두가지 경우를 찾아 볼 수 있었다.

sockClient.lifecycle().subscribe()

STOMP WebSocket의 라이프싸이클에 대한 이벤트 핸들러 함수

  • CLOSED 됐을때 즉 websocket이 닫혔을때 다시 재연결을 요청하도록 하였다.
  • 웹소켓 종료를 위한 사전 정의한 메시지가 오기전까지 연결을 계속 유지하기 위함.

sockClient.connect()

WebSocket 연결 본격 요청

  • 3-handshake를 통해서 본격적으로 서버와 웹소켓 연결을 하기 위함이다.
  • 서버에서 http와 같은 stateless 요청보다 더 윗단계인 세션 유지를 할 수 있도록 연결상태를 "upgrade"할 수 있도록 미리 설정해 놓아야 연결이 완성된다.

sockClient.topic().subscribe()

STOMP 라이브러리에서 Subscribe(구독)의 기본 단위인 Topic ID를 통해서 다중 웹 소켓 연결을 가능하도록 한다.

  • "/sub" 은 웹 애플리케이션 서버에서 지정한 메시지 브로드캐스팅 브로커의 Prefix
  • 해당 /sub/추가URI/+topicID 를 통해서 Publishing된 메시지를 수신 가능하다.
  • topic().subscribe() 함수를 통해서 핸들러 함수를 작성할 수 있다.

STOMP CONTROLLER

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()));
    }
}

Argument - 매개변수

컨트롤러의 매핑 함수에 들어오는 메시지는 Publishing 메시지와 동일한 형태

  • Publishing 메시지를 커스텀 설정하여 프로토콜을 마음대로 정의할 수 있다.

SimpleMessagingTemplate

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);
	}

HTTP Connection Upgrade

잘 설명해주신 블로그: 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;
}

0개의 댓글