웹 소켓을 사용한 채팅 서버

박준수·2023년 7월 3일
2

[토이프로젝트]

목록 보기
1/5
post-thumbnail

웹 소켓(Web socket)

  • 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜.
  • 일반 Socket통신과 달리 HTTP 80 Port를 사용하므로 방화벽에 제약이 없으며 통상 WebSocket으로 불린다.
  • 접속까지는 HTTP 프로토콜을 이용하고, 그 이후 통신은 자체적인 WebSocket 프로토콜로 통신하게 된다.

HTTP의 특성 - Connectionless

모든 HTTP를 사용한 통신은 클라이언트가 먼저 요청을 보내고, 그 요청에 따라 웹 서버가 응답하는 형태이며 웹 서버는 응답을 보낸 후 웹 브라우저와의 연결을 끊는다.

  • Websocket이 존재하기 전에는 Polling, Long Polling, Streaming 방식으로 해결


  • Polling : 클라이언트가 평범한 HTTP Request를 서버로 계속 요청해 이벤트 내용을 전달 받는 방식

  • Long Polling : 클라이언트 → 서버로 HTTP Request 요청, 서버 → 클라이언트로 전달할 이벤트가 있다면 그 순간 Response 메세지를 전달하며 연결을 종료

  • Streaming : Long Polling과 비슷하게 클라이언트 → 서버로 HTTP. Request 요청, 서버 → 클라이언트로 이벤트를 전달할 때 해당 요청을 해제하지 않고 필요한 메세지만 보내기를 반복하는 방식

Web socket 특징

  • HTTP 통신의 특징인 (연결 -> 연결 해제) 때문에 비효율을 보안하려함
  • 웹소켓은 클라이언트가 접속 요청을 하고 웹 서버가 응답한 후 연결을 끊는 것이 아닌 Connection을 그대로 유지하고 클라이언트의 요청 없이도 데이터를 전송할 수 있는 프로토콜이다.
  • 프로토콜의 요청은 [ws://~]로 시작한다.
  • HTTP Request를 그대로 사용하기 때문에 기존의 80, 443 포트로 접속을 하므로 추가 방화벽을 열지 않고도 양방향 통신이 가능하고, HTTP 규격인 CORS 적용이나 인증 등 과정을 기존과 동일하게 가져갈 수 있는 것이 장점이다.

Web socket 접속 과정

웹소켓 접속 과정은 TCP/IP 접속 → 웹소켓 열기 → HandShake 과정으로 나눌 수 있다. 웹소켓도 TCP/IP위에서 동작하므로, 서버와 클라이언트는 웹소켓을 사용하기 전에 서로 TCP/IP 접속이 되어있어야 한다. TCP/IP 접속이 완료된 후 서버와 클라이언트는 웹소켓 열기 HandShake 과정을 시작한다.

웹소켓 열기 핸드셰이크는 클라이언트가 먼저 핸드셰이크 요청을 보내고 이에 대한 응답을 서버가 클라이언트로 보내는 구조이다. 서버와 클라이언트는 HTTP 1.1 프로토콜을 사용하여 요청과 응답을 보낸다

Socket, Web Socket의 차이점

소켓(Socket) : 네트워크상에서 동작하는 프로그램 간 통신의 종착점. 1대1 통신의 경우 양 측다 소켓이 존재해야 통신이 가능하다. 현재 대부분의 통신은 인터넷 프로토콜(TCP, UDP)에 기반하고 있으므로 대부분의 네트워크 소켓은 인터넷 소켓이다.

웹 소켓(Web Socket) : 웹소켓은 하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜이다. 그리고 HTTP나 HTTPS 위에서 동작하도록 설계되었으며, 따라서 포트는 80번 혹은 443번이다. HTTP 프로토콜과 구별은 되지만 호환이 된다.

차이점

  • 동작 계층 : OSI 7계층 기준으로 소켓은 인터넷 프로토콜에 기반하므로 TCP, UDP가 속한 4계층에 위치하며 웹 소켓은 TCP에 의존하지만 HTTP에 기반하므로 7계층에 위치한다.
  • TCP에 기반한 소켓 통신은 단순히 바이트 스트림을 통한 데이터 전송이므로 바이트로 이루어진 데이터를 다뤄야하지만, 웹소켓 통신은 어플리케이션 계층인 7계층에 기반하기 때문에 메시지 형식의 데이터를 다루게 된다.

사실 이 둘은 서로 상반되는 개념이 아니기 때문에 완전하게 차이점을 비교할 수는 없다. 웹에서도 TCP 소켓 통신으로 실시간 통신을 할 수는 있지만 전송 계층의 원시 바이트 대신 애플리케이션 계층을 통해 메시지를 보내는 것이 개발측면에서 더 적합하기 때문에 TCP 소켓통신에 기반하여 웹 소켓을 발전시킨 것이기 때문이다.

웹 소켓은 TCP 소켓과 구분되는 것이 아니라 TCP 소켓의 추상화된 형태이다.

“웹 소켓과 소켓은 전혀 다르다” 보다는 “소켓 통신에 기반하여 웹 소켓은 웹 어플리케이션에 맞게 발전한 형태로 소켓 통신을 한다” 라고 보면 좋을 것 같다.

웹소켓을 이용해 채팅 서버 구현 핵심 소스코드

  1. 웹 소켓 config 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WepSocketConfig implements WebSocketConfigurer {

    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/junsu/chat")
                .setAllowedOrigins("*");
    }
}
  1. WebsocketHandler 작성
@Service
@RequiredArgsConstructor
public class WebSocketHandler extends TextWebSocketHandler {
    private final RoomService roomService;
    private final MessageRepository messageRepository;

    private final Sessions sessions;

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(payload);
        String type = jsonNode.get("type").asText();
        Long roomId = jsonNode.get("roomId").asLong();
        String sender = jsonNode.get("sender").asText();
        String comment = jsonNode.get("message").asText();
        Room room = roomService.findOneRoom(roomId);

        Message chatMessage = Message.builder()
                .type(Type.valueOf(type))
                .message(comment)
                .sender(sender)
                .room(room).build();
        messageRepository.save(chatMessage);

        handleActions(session, chatMessage);

    }

    public void handleActions(WebSocketSession session, Message chatMessage) throws Exception {

        if (chatMessage.getType().equals(Type.ENTER)){
            chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
            messageRepository.save(chatMessage);
            Set<WebSocketSession> roomSessions = sessions.getSessionsByRoomId(chatMessage.getRoom().getId());
            roomSessions.add(session);
            sendMessage(chatMessage);
        } else if (chatMessage.getType().equals(Type.TALK)){
            sendMessage(chatMessage);
        } else if (chatMessage.getType().equals(Type.EXIT)){
            chatMessage.setMessage(chatMessage.getSender() + "님이 퇴장했습니다.");
            messageRepository.save(chatMessage);
            sendMessage(chatMessage);
            sessions.removeSession(session);
        }

    }
  • 메시지 형식을 chatMessage 엔티티 타입에 맞게 저장하고 Message.Type에 맞게 입장, 대화, 퇴장을 할 수 있도록 했다.
  1. 채팅방 식별자와 세션을 Map 자료구조로 정의하였다.
@Component
public class Sessions {
    private final Map<Long, Set<WebSocketSession>> sessionsByRoomId = new ConcurrentHashMap<>();

    public Set<WebSocketSession> getSessionsByRoomId(Long roomId) {
        return sessionsByRoomId.computeIfAbsent(roomId, k -> new HashSet<>());
    }

    public void removeSession(WebSocketSession session) {
        sessionsByRoomId.values().forEach(sessions -> sessions.remove(session));
    }
}
  1. 해당 채팅방의 각 세션들에 해당 메시지를 보네준다
public void sendMessage(Message chatMessage) {
        Set<WebSocketSession> roomSessions = sessions.getSessionsByRoomId(chatMessage.getRoom().getId());
        roomSessions.parallelStream().forEach(session -> {
            roomService.sendMessage(session, chatMessage);
        });
    }
public <T> void sendMessage(WebSocketSession session, T message) {
        try{
            MessageInfo messageInfo = messageMapper.mapEntityToInfo((Message) message);
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(messageInfo)));
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

즉, 웹소켓을 이용하여 각 채팅방 식별자에 해당하는 세션들에게 메시지를 전달하는 방식으로 채팅 서비스를 구현해 보았다.

참고 :
웹소켓과 소켓은 어떻게 다른가
[Spring Boot] WebSocket과 채팅 (1)

profile
방구석개발자

0개의 댓글