CS/Network

WebSocket 통신 개념, 프론트, 백엔드 연결

prden 2023. 8. 1. 20:25

1. WebSocket 의미

메시지 처리 방식 : Polling, Long Polling, WebSocket

 1) Polling : 클라이언트(ex) 웹 브라우저)가 정해진 간격으로 서버에 요청을 보내 새로운 메시지나 데이터가 있는지 확인. 그러나 일정 주기로 메시지를 클라이언트에서 가져가는 방식이라서 다른 사용자가 작성한 메시지가 전달되는데까지 일정시간 지연 발생 및 클라이언트 별로 Polling 되는 시점에 따라 서로 보고있는 메시지가 다르기 때문에 실시간으로 보이는 대화 내용이 불일치 할 수 있음.

 

 2) Long Polling : 클라이언트가 서버에 요청을 보내면, 서버는 새로운 메시지가 도착할 때까지 응답을 지연시킨다. 새로운 메시지가 도착하면 그 즉시 응답을 보내고 클라이언트는 즉시 다시 요청을 보낸다.

 

https://velog.io/@codingbotpark/Web-Socket-%EC%9D%B4%EB%9E%80

 

Web Socket 이란?

유튜브 [10분 테코톡] 🧲코일의 Web Socket을 정리한 웹소켓 과 웹소켓의 특징, 동작방법, 특이점 을 포함한 내용입니다

velog.io

2. 스프링 부트 설정

WebSocket - STOMP 프로토콜

- 프론트 입장(React.js) 예시 : 

function App() {
  const stompClient = useRef(null);
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState("");

  const handleInputChange = (event) => {
    setInputValue(event.target.value);
  };

  const disconnectApi = async (sendId, rcvId, chatRoomId) => {
    try {
      const config = {
        headers: {
          Authorization:
            "Bearer aaaa",
        },
      };
      const payload = {
        sendId: 8,
        rcvId: 3,
        chatRoomId: 2,
      };

      const response = await axios.post(
        "/api/test/v1/chat-room/disconnect",
        payload,
        config
      );

      if (response.status === 200) {
        console.log("Chat room disconnected successfully:", response.data);
      }
    } catch (error) {
      // Handle errors here
      console.error("Error disconnecting chat room:", error);
    }
  };

  const connect = () => {
    stompClient.current = new Client({
      brokerURL: "ws://localhost:80/ws", // WS 서버 아이피
      connectHeaders: {
        Authorization:
            "Bearer aaaa",
        chatRoomId: 2, // Passing the chatRoomId in connect headers
        sendId: 8,
        rcvId: 3,
      },
      // connection 맺기
      onConnect: () => {
        console.log("Connected to WebSocket");
      // connection 맺은 후 subscribe();  
        subscribe();
      },
      debug: (str) => {
        console.log(new Date(), str);
      },
      reconnectDelay: 5000, // Auto reconnect after 5 seconds
    });

    stompClient.current.activate();
  };

  const subscribe = () => {
    stompClient.current.subscribe("/sub/chat/room/2", (message) => {
      const newMessage = JSON.parse(message.body);
      setMessages((prevMessages) => [...prevMessages, newMessage]);
    });
  };

  const disconnect = (chatRoomNo, authToken) => {
    console.log("채팅이 종료되었습니다.");
    disconnectApi();
    stompClient.current.deactivate();
  };

  useEffect(() => {
    connect();
    return () => disconnect();
  }, []);

// pub 영역
  const sendMessage = () => {
    if (stompClient.current && inputValue) {
      const body = {
        chatRoomId: 2,
        sendId: 8,
        rcvId: 3,
        message: inputValue,
      };

      stompClient.current.publish({
        destination: `/pub/send-message`,
        body: JSON.stringify(body),
      });

      // setInputValue("");
    }
  };

  return (
    <div className="chat-container">
      <div className="send-message">
        <h3>사용자 채팅창</h3>
        <input
          type="text"
          value={inputValue}
          onChange={handleInputChange}
          placeholder="Type your message here"
          onKeyPress={(e) => {
            if (e.key === "Enter") {
              sendMessage();
            }
          }}
        />
        <button onClick={sendMessage}>Send</button>
      </div>

      <div className="received-messages">
        <h3>Received Messages</h3>
        <ul>
          {messages.map((item, index) => (
            <li key={index} className="list-item">
              <strong>{item.sendId}: </strong>
              {item.message}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export default App;

 

 

- 스프링 부트(WS 서버 입장)

@Slf4j
@Component
@RequiredArgsConstructor
public class StompHandler implements ChannelInterceptor {
    private final TokenProvider tokenProvider;
    private final ChatHeadService chatHeadService;

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        log.debug("message: " + message);
        log.debug("헤더: " + message.getHeaders());
        log.debug("토큰: " + accessor.getNativeHeader("Authorization"));

        String token = getAuthorizationToken(accessor);

        switch (Objects.requireNonNull(accessor.getCommand())) {
            case CONNECT:
                log.debug("STOMP Connect command");
                try {
                    tokenProvider.validateToken(token);
                    Long chatRoomId = getChatRoomId(accessor);
                    Long senderId = getSendId(accessor);
                    Long rcvId = getRcvId(accessor);

                    chatHeadService.connectToChatRoom(chatRoomId, senderId, rcvId);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                break;
            case DISCONNECT:
                log.debug("STOMP Disconnect command");
//                DISCONNECT는 api로 Header값 꺼낼 수 없음
//                Long chatRoomId = getChatRoomId(accessor);
//                Long senderId = getSendId(accessor);
//                Long rcvId = getRcvId(accessor);
//                chatHeadService.disConnectToChatRoom(chatRoomId, senderId, rcvId);
                break;
            case SEND:
            	// Header에서 authorization 꺼낼 수 없음.
                log.debug("STOMP Send command");
                // Handle send case logic here
                break;
            case SUBSCRIBE:
                log.debug("STOMP Subscribe command", message);
                // Handle subscribe case logic here
                break;
            default:
                log.warn("Unknown STOMP command");
                break;
        }
        return message;
    }
}

 

STOMP = Simple Text Oriented Messaging Protocol는 웹 소켓 위에서 작동하는 메시징 프로토콜로, 클라이언트와 서버간의 메시지 기반 통신을 간소화하교 표준화한다. 

 

세션을 서버에서 따로 관리하는 MAP 자료구조 방식 비교해서 알아두기 

차례 : 외부 메시지 브로커 필요한 이유 

https://brunch.co.kr/@springboot/695

 

Spring Websocket & STOMP

오랫만에 작성하는 기술블로그 포스팅입니다. 이 글에서는 스프링 부트 기반의 웹소켓 및 STOMP에 대해서 설명합니다. 이 글을 읽기 위해서는 기본적인 HTTP 지식이 있어야 하며, 스프링 프레임워

brunch.co.kr

3.  RabbitMQ or Kafka 연결

 

4. 배민 사례

https://techblog.woowahan.com/2547/

 

실시간 서비스 경험기(배달운영시스템) | 우아한형제들 기술블로그

{{item.name}} 경험기 들어가기 앞서 이 글은 신기술 사용기 또는 소개가 아닌 실시간 서비스 즉 배민라이더스 BROS 1.0 을 개발 하면서 겪어왔던 다소 특별한 개발 및 운영 경험기 입니다. BROS 2.0이 나

techblog.woowahan.com