학습

[WebSocket] WebSocket을 활용한 실시간 요청 및 응답 구현

힝뿌 2023. 11. 16. 00:28
반응형

프로젝트를 진행하던 중, 사용자에게 실시간으로 진행상황을 보여주고 싶었다.

'어떻게 구현을 할 수 있을까?' 생각을 하다가 실시간 채팅 구현에 많이 사용한다는 WebSocket을 이용해 보기로 했다.

 

 

현재 개발 프로세스는 크게 다음과 같다.

  • 모델 서버에서 AI 모델이 돌아가고 있다.
  • 사용자의 요청을 받은 AI 모델이 무엇인가를 만들어낼 때마다 '나 이만큼 만들었어!' 하고 web 서버한테 현재 진행상황에 대한 내용을 보낸다.
  • web 서버는 그 내용을 받아 사용자에게 return 해준다.

 

 

우선 모델 서버에 직접 개발하기 전, WebSocket에 대해 처음 인지했기 때문에 다른 환경에서 한 번의 연결로 계속 통신이 되는지를 테스트해보고자 한다.

 

 

 

그전에, WebSocket에 대해 잠깐 알아가는 시간을 가져보자.

 

 

 

WebSocket은 양방향 통신을 지원하는 프로토콜로, 실시간 채팅, 주식 시세 업데이트, 온라인 게임 등에서 주로 사용된다.

 

HTTP와 WebSocket의 차이

  • HTTP
    • 클라이언트가 서버에 요청을 보내면, 서버가 응답을 보내고 연결이 끊기는 단방향 통신 방식이다.
    • 즉, 매번 연결을 요청해 맺고 끊는 작업이 발생해 실시간으로 변경되는 정보에 대해서는 비효율적이다.
  • WebSocket
    • 클라이언트와 서버 간의 양방향 통신이 가능하다. 
    • 한 번 연결이 수립되면 계속해서 데이터를 주고받을 수 있어, 연결이 유지되는 동안 실시간으로 데이터를 전송할 수 있다.
    • 즉, HTTP와는 달리 연결을 새로 맺을 필요가 없는 것이다.

 

 

WebSocket 원리

클라이언트가 서버에게 WebSocket 연결을 요청하면, 서버와 클라이언트 간에 핸드쉐이크(Handshake)가 이루어진다.

Handshake는 HTTP 프로토콜을 기반으로 하며, 연결이 성공하면 WebSocket 연결이 설정된다.

연결이 설정되면, 클라이언트나 서버는 자유롭게 데이터를 전송할 수 있고, 상대방이 해당 메시지를 받으면 된다.

WebSocket이 연결이 되면 그때는 http://~ 가 아닌 ws://~로 요청을 보내야 한다!

(HTTP Upgrade라고 표현하는 것 같다.)

WebSocket을 종료시키기 위해서도 Closing handshake가 필요하다.

Handshake 이후 WebSocket 연결이 종료된다.

 

 

이해를 돕기 위한 사진으로, 원래는 client ▶️ server의 형태인데,

현재 나의 상황에서는 모델 서버가 web 서버에게 요청을 하는 입장이므로 모델 서버 = client, web 서버 = server 역할인 셈이다.

 

 

해당 이미지는 https://www.dotcom-monitor.com/blog/websocket-application-monitoring/ 의 이미지를 참고해서 구성했습니다.

 

 

여기서 모델 서버는 FastApi를, web 서버는 Springboot를 이용했다.

 

 

 

FastApi 코드

5초의 간격을 두고 1분간 12번 전송해 보는 테스트 코드다.

더 정확하게 보기 위해 시간도 함께 출력을 해서 보기로 했다.

from fastapi import FastAPI, WebSocket
import asyncio
import websockets
import datetime
import uvicorn

app = FastAPI()

@app.get("/websocket/test")
async def send_data_to_spring_boot():
    uri = "ws://{web_server_ip}:{web_server_port}/{web_server_api}" 

    async with websockets.connect(uri) as websocket:

        for i in range(12): 
            try:
                message = f"{i} : Hello, GM!"  
                await websocket.send(message)
                current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                print(f"Sent: {message} : {current_time}")

                await asyncio.sleep(5)

            except websockets.ConnectionClosed:
                print("Connection to the WebSocket server closed. Reconnecting...")
                await asyncio.sleep(5)

 

 

Springboot 코드 - WebSocketHandler

여기서도 정확하게 확인해 보기 위해 시간을 출력했다.

@Component
public class WebSocketHandler extends TextWebSocketHandler {

    private static final ConcurrentHashMap<String, WebSocketSession> CLIENTS = new ConcurrentHashMap<>();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("received = " + message.getPayload() + " : " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")));
        CLIENTS.get(session.getId()).sendMessage(message);
    }

    /**
     * <afterConnectionEstablished>
     * WebSocket 연결이 성공적으로 수립된 후에 호출되는 콜백 메소드
     * 클라이언트가 서버에 연결된 직후에 수행해야 하는 작업을 정의하는데 사용
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println(session.getId() + " 연결 됨");
        CLIENTS.put(session.getId(), session);
    }

    /**
     * <afterConnectionClosed>
     * WebSocket 연결이 닫힌 후에 호출되는 콜백 메소드
     * 클라이언트와 서버 사이의 연결이 종료된 후 수행해야 하는 작업을 정의하는데 사용
     * @param session : 연결된 WebSocket을 나타내는 객체
     * @param status : WebSocket 연결이 종료된 이유를 나타내는 객체
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println(session.getId() + " 연결 끊김");
        CLIENTS.remove(session.getId());
    }
}

 

 

Springboot 코드 - WebSocketConfig

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {

    private final WebSocketHandler webSocketHandler;

    /**
     * setAllowedOrigins("*")" : 웹 소켓 cors 정책으로 인해 허용 도메인을 지정해줘야 함. 테스트이기 때문에 와일드카드(*)로 모든 도메인을 열어줌.
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/webSocketHandler").setAllowedOrigins("*");
    }

}

 

 

 

테스트 결과

FastApi 출력 결과

 

Springboot 출력 결과

 

FastApi에서 총 12번의 request를 보냈고, Springboot에서는 12번의 메시지를 받았음을 확인할 수 있다.

또한, 요청을 보낼 때와 받을 때의 시간을 보면 동일한 시간임을 알 수 있고, 다음 요청을 받기까지 5초의 시간이 걸렸음을 알 수 있다.

(로컬 환경에서 진행한 부분이라 시간이 딱딱딱 정확하게 나온 것일까..? 실제 모델이 돌아가는 서버에서도 동일하게 나오는지 궁금하다.)

그리고 한 번의 연결과 한 번의 연결 끊김이 발생한 부분도 확인할 수 있다.

 

 

이제 모델이 있는 모델 서버에 실제로 적용해 보는 일만 남았다.

또 어떤 수많은 오류들이 나를 기다릴까~

참... 절겁다....

 

나중에는 이것을 이용해 실시간 채팅을 해볼 수 있는 기능도 구현해 봐야겠다. (메모)

 

이상 오늘의 정리  -The End-

반응형