Signaling - 1편 Stomp (4)

박근수·2024년 2월 18일
0

배경 지식은 이전 포스팅에서 설명을 얼추 한 것 같습니다. 이후 포스팅부터는 실제적으로 어떻게 1:N 스트리밍을 구현했는지 코드와 함께 설명하도록 하겠습니다. 처음 WebRTC를 접하시는 분들이 많이 읽으실 수 있을 것 같아서 다양한 Example 코드를 읽으면서 제가 공부한 내용과 고민했던 부분, 제 나름의 고민의 답을 같이 적어보겠습니다.

  • 주의 : React를 잘 모르는 상태에서 팀이 프론트를 React를 쓰기로 해서 공부하면서 적용하느라 이상할 수 있음.

Signaling with WebSocket

Signaling

Signaling을 다시 말하자면 WebRTC를 위한 P2P(Peer to Peer) 연결 과정입니다.
이를 위해 Signaling Server와 Peer가 통신을 해야합니다. 이것은 클라이언트 - 서버 통신을 생각하시면 됩니다. Signaling을 통해서 P2P 연결이 이루어지고 나면 그 때는 Peer끼리 알아서 통신을 하게됩니다. 그 연결까지는 Peer가 Signaling Server를 통해서 상대 Peer와 SDP와 Ice Candidate를 교환하게 됩니다.

Signaling data 교환

일반적인 웹 서비스에서 클라이언트 - 서버는 HTTP Request, Response로 설계하게 됩니다. 요청이 있어야 응답이 있죠. 그런데 이 방법은 Signaling에 적용하기에는 문제가 있습니다. Peer와 Signaling Server가 순서없이 데이터를 주고 받기 때문입니다. 물론 SDP는 순서가 있지만 Ice Candidate과정에서는 누가 응답을 보내고 요청을 받는 순서가 정해지기 어렵고 잦은 데이터 교환 과정에서 3-handshake,4-handshake가 일종의 비용이 되기 때문입니다. Ice Candidate가 이루어지는 것을 추후 log로 찍어보시면 이해가 되실 겁니다.
그래서 시그널링을 WebSocket을 통해서 구현하게 됩니다. WebSocket으로 데이터를 주고 받고 각 피어는 EventHandler를 통해서 Signaling을 관리합니다.

WebSocket 여기서 웹 소켓이 무엇인지 한 번 살펴보세요!!

WebSocket - STOMP

자 그러면 WebSocket을 이용해서 통신을 구현해보기 전에 우리가 알아야할게 있습니다. WebSocket은 누구나 쓸 수 있지만 목적에 따라 편하게 쓰기 쉬운 라이브러리가 있습니다. 보통 백엔드가 NodeJS일 때는 SocketIO를 사용하고 Spring 환경에서는 STOMP를 사용합니다. 여기서는 STOMP를 사용합니다.
STOMP까지 설명하는 것은 논지에서 벗어난 것 같아서 생략합니다. 사용할 때 겪었던 어려움만 말씀드리고 자세하게는 말씀드리지 않겠습니다. 공식문서를 찾아보세요.

STOMP

Stomp with spring

Java Config

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer  {

   public void configureMessageBroker(MessageBrokerRegistry config) {
       config.enableSimpleBroker(new String[]{"/topic","/busker","/audience"}); // sub
       config.setApplicationDestinationPrefixes(new String[]{"/app"});
   }

   public void registerStompEndpoints(StompEndpointRegistry registry) {
       registry.addEndpoint(new String[]{"/api/chat"}).setAllowedOriginPatterns("*");
       registry.addEndpoint(new String[]{"/api/signal"})
               .setAllowedOriginPatterns("*");
   }
   
}

Java Stomp를 사용할 때 3가지를 유의해야 합니다.

1. config.enableSimpleBroker(new String[]{"/topic","/busker","/audience"}); // sub
2. config.setApplicationDestinationPrefixes(new String[]{"/app"}); // pub
3. registry.addEndpoint(new String[]{"/api/signal"})
              .setAllowedOriginPatterns("*");

1은 STOMP의 메세지를 구독하는 경우에 앞에 붙입니다.
2는 STOMP에 메세지를 보낼 때 사용합니다.
3은 STOMP WebSocket을 연결할 때 목적이나 클라이언트를 식별하기 위해 사용합니다.

JS connection

const Streaming = ({ isStreaming }) => {
    const pcRef = useRef(new RTCPeerConnection(PCConfig));
    const clientRef = useRef(
        new StompJS.Client({
            brokerURL: `wss://이건 비밀이에요./api/signal`,
        })
    );
    const pc = pcRef.current;
    const client = clientRef.current; //useRef를 사용합니다.

brokerURL에서 /api/signal을 붙였는데 EndPoint에 대응하는 Connection을 사용하시면 됩니다. 이 소켓은 Signaling을 위한 소켓입니다.

Subcribe

java

    public void buskerSendIceCandidate(String userId, HashMap<String, Object> iceCandidate){
        simpMessagingTemplate.convertAndSend(
                "/busker/" + userId + "/iceCandidate", iceCandidate
        );
    }

JS

client.subscribe(`/busker/${userId}/iceCandidate`, (res) => {
                const iceResponse = JSON.parse(res.body);
                if (iceResponse.id === "iceCandidate") {
                    console.log(koreaTime + " server send ice \n" + iceResponse.candidate.candidate)
                    const icecandidate = new RTCIceCandidate(iceResponse.candidate)
                    pc.addIceCandidate(icecandidate)
                        .then()
                }

1의 prefix busker로 구독 신청해놨습니다. 자바에서 보낼 때나 JS에서 받을 때 subcribe의 prefix를 유의해야합니다. prefix의 뒤로 특정 대상을 식별하지만 prefix가 없다면 애시당초 메세지를 읽을 수 없을겁니다.

JAVA Publish

@RestController
@RequiredArgsConstructor
public class SignalController {
    private final Logger log = LoggerFactory.getLogger(SignalController.class);
    private final SimpMessagingTemplate simpMessagingTemplate;
    private final BuskingManagingService buskingManagingService;

    @MessageMapping("/api/busker")
    public void listenTestStomp(@Payload String message) {
        HashMap<String, String> map = new HashMap<>();
        map.put("test", "test");

        simpMessagingTemplate.convertAndSend("/busker", map);
        return;
    }

JS publish

client.publish({
  destination: `/app/api/busker/${userId}/offer`,
  body: JSON.stringify({
       userId,
       offer,
       })
  })

JAVA에서 Controller가 JS가 보낸 메세지의 destination을 통해 매핑합니다.

주의

  1. 동기들이 stomp를 적용할 때 블로그에 있는 많은 코드들을 참고했다가 어려움을 겪었습니다. latest 버전으로 업데이트 된 사용법과 블로그에 있는 내용이 다르거나 ChatGPT가 엉뚱하게 알려준 코드를 참고했을 문제를 겪었습니다. JAVA는 괜찮은데 Stomp-js를 쓰시는 경우는 유의하실 필요가 있는 것 같습니다. STOMP-js는 공식 문서를 참고하세요.
  2. Sock-js를 적용하려다가 스프링에서 말썽을 부렸습니다. Sock-js는 웹소켓을 지원하지 않는 브라우저가 사용할 수 있도록 하기 위한 방법인데 IE8,9 정도의 예전 브라우저를 위한 방법입니다. 전 그냥 Sock-js 빼버렸습니다. Sock-js는 Pulling을 사용하기도 하고 IE8,9를 사용하시는 분이면 이 서비스를 거의 사용하지 않으실 것 같아서 그랬습니다. 문제 해결이 어렵더라고요.
  3. wss는 HTTPS처럼 SSL이 추가된 소켓입니다. 서버에서 HTTPS를 적용하셨다면 딱히 하실 것은 없습니다.
  4. 소켓 통신을 하신다면 ws를 http의 자리에 넣어주세요.

성공

WebSocket 연결이 끝났다면 축하합니다. WebRTC의 10분 능선 중 1분 능선을 건너셨습니다. 나머지 9분 능선 중 8이 이 Peer Connection입니다. (1은 종료와 마무리)

다음은 Peer Connection입니다.

내용이 너무 많아서 다음 포스팅으로 넘기겠습니다.

profile
개성이 확실한편

0개의 댓글