2023스마일게이트윈터데브캠프

[스마일게이트 캠프] 실시간 채팅 구현하기 - Web Socket

딤섬뮨 2023. 2. 17. 00:13
728x90

이해 용도로 작성해서 보충 예정입니다

채팅 서버

스마일게이트 윈터 데브 캠프에서 나는 인증 서버와 채팅 서버를 맡았다!

인증 서버를 끝내고 채팅 서버를 구현할 차례인데, 일단 내용 파악을 해야겠다.

Web Socket

  • 서버는 늘 수동적인 입장이였다. 서버도 능동적이게 행동할 수 있게 만든게 웹소켓!

인터넷이 나오고 HTTP를 통해서 서버로부터 데이터를 가져오기 위해서는 오로지 URL을 통한 요청이 유일한 방법이었다.

매번 요청을 해야 서버가 응답할 수 있었다.

2014년 10월 28일의 HTML5 버전이 나올 때 함께 등장한 웹소켓.

웹소켓에서는 서버와 브라우저 사이에 양방향 소통이 가능하다.

브라우저는 서버가 직접 보내는 데이터를 받아들일 수 있고, 사용자가 다른 웹사이트로 이동하지 않아도 최신 데이터가 적용된 웹을 볼 수 있게 해줍니다.

 

  • 웹소켓은 HTTP와 같은 프로토콜이다. 

Transport protocol의 일종으로 서버와 클라이언트 간의 효율적인 양방향 통신을 실현하기 위한 구조.

웹소켓은 단순한 API로 구성되어있으며, 웹소켓을 이용하면 하나의 HTTP 접속으로 양방향 메시지를 자유롭게 주고받을 수 있습니다.

 

작동 원리

(1) 연결 수립

최초 연결 요청 시 클라이언트에서 HTTP를 통해 웹서버에 요청한다. 이를 핸드셰이크 (Handshake) 라고 한다.

핸드셰이크를 위해 클라이언트는 서버에 아래와 같은 요청(HTTP Upgrade)을 보낸다.

  • HTTP Upgrade?

클라이언트가 서버에게 "현재 연결된 프로토콜을 다른 프로토콜로 바꿔줘!" 라고 요청하는 것.

새로 프로토콜을 연결하는 것이 아닌 현재의 프로토콜을 바꿔달라

핸드셰이크가 성공하면, 둘간의 통신 프로토콜은 Websocket 으로 전환된다.

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

연결 성공시, 아래와 같이 응답해준다.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

(2) 전이중 통신연결이 수립되면 클라이언트와 서버 양측간의 데이터 통신 단계가 시작된다.

서로는 메세지를 보내며 통신하는데, 이 메세지는 프레임(Frame) 단위로 이루어진다.

또한, 연결 수립 이후에는 서버와 클라이언트는 언제든 상대방에게 ping 패킷을 보낼 수 있다. Ping 을 수신한 측은 가능한 빨리 pong 패킷을 상대방에게 전송해야한다. 이런 방식으로 서로의 연결이 살아있는지를 주기적으로 확인할 수 있는데, 이를 Heartbeat 라고 한다.

 

(3) 연결 종료

클라이언트 혹은 서버 양측 누구나 연결을 종료할 수 있다. 연결 종료를 원하는 측이 Close Frame 을 상대쪽으로 전송하면 된다.

 

Spring Web Socket 설정

공식 문서는 https://docs.spring.io/spring-framework/docs/4.3.x/spring-framework-reference/html/websocket.html

  • Gradle을 설정해준다
implementation 'org.springframework.boot:spring-boot-starter-websocket'
  • WebScoketConfig
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
    }
}

@EnableWebSocket : websocket 활성화

WebScoketConfigurer인터페이스를 구현하여 WebSocketConfig를 작성한다.

//보충 필요

 

registerWebSocketHandlers를 작성해준다.

WebSocket에 접속하기 위한 앤드 포인트는 => /ws/chat

도메인이 다른 서버에서도 접속 가능하도록 CORS는 *로 설정해준다.

이제 클라이언트가 ws://localhost:8080/ws/chat으로 커넥션을 연결하고 메세지 통신이 가능해진다.

 

  • 채팅 고도화

위에서 만든 websocket 통신은 ws://localshot:8080/ws/chat에 연결된 클라이언트 끼리만 메시지 통신이 가능하다.

따라서 채팅방이 하나 뿐인 채팅 서버이다

 

  • 채팅 메시지 구현

채팅 메시지를 주고받기 위한 DTO를 만든다.

상황에 따라 채팅방입장,메세지 보내기, 두가지 상황이 있으므로 ENTER ,TALK을 enum으로 선언한다.

나머지 멤버 필드는 채팅방 구별 id , 메시지를 보낸 사람, 메세지로 구성한다

 

@Getter
public class ChatMessage {
    // 메시지 타입 : 입장, 채팅
    public enum MessageType {
        ENTER, TALK
    }
    private MessageType type; // 메시지 타입
    private String roomId; // 방번호
    private String sender; // 메시지 보낸사람
    private String message; // 메시지
}

 

  • 채팅방 구현하기

채팅방을 구현하기 위한 DTO를 만든다.

채팅방은 입장한 클라이언트의 정보를 가지고 있어야하므로 WebSocketSession 정보 리스트를 필드로 가진다.

나머지는 채팅방id,채팅방 이름을 추가합니다.

채팅방에서는 입장,대화하기의 기능이 있으므로 handleAction을 통해 분기 처리합니다.

입장시에는 채팅룸의 session정보에 클라이언트의 리스트를 추가했다가 채팅룸에 메세지 도착시 , 모든 session에 메세지를 전송하면 채팅이 완성

 

@Getter
public class ChatRoom {
    private String roomId;
    private String name;
    private Set<WebSocketSession> sessions = new HashSet<>();

    @Builder
    public ChatRoom(String roomId, String name) {
        this.roomId = roomId;
        this.name = name;
    }

    public void handleActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
        if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
            sessions.add(session);
            chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
        }
        sendMessage(chatMessage, chatService);
    }

    public <T> void sendMessage(T message, ChatService chatService) {
        sessions.parallelStream().forEach(session -> chatService.sendMessage(session, message));
    }
}

 

  • 채팅 서비스 구현

채팅방을 생성,조회하고 하나의 세션에 메시지 발송을 하는 서비스를 구현.

 

채팅방 Map은 서버에 생성된 모든 채팅방의 정보를 모아둔 구조체

채팅룸의 정보저장은 빠른 구현을 위해 외부 저장소가 아닌 Hashmap

 

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {

    private final ObjectMapper objectMapper;
    private Map<String, ChatRoom> chatRooms;

    @PostConstruct
    private void init() {
        chatRooms = new LinkedHashMap<>();
    }
    
    //모든 채팅방 조회 Map에 담긴 정보를 조회
    public List<ChatRoom> findAllRoom() {
        return new ArrayList<>(chatRooms.values());
    }

	//특정 채팅방 조회
    public ChatRoom findRoomById(String roomId) {
        return chatRooms.get(roomId);
    }
    
	//Random UUID로 구별 ID를 가진 채팅방 객체를 생성하고 채팅방 MAP추가
    public ChatRoom createRoom(String name) {
        String randomId = UUID.randomUUID().toString();
        ChatRoom chatRoom = ChatRoom.builder()
            .roomId(randomId)
            .name(name)
            .build();
        chatRooms.put(randomId, chatRoom);
        return chatRoom;
    }
    
//지정한 WebSocket 세션에 메세지를 발송
    public <T> void sendMessage(WebSocketSession session, T message) {
        try {
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }
}
  • 컨트롤러

생성 및 조회는 restapi를 통해

@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatService chatService;

    @PostMapping
    public ChatRoom createRoom(@RequestParam String name) {
        return chatService.createRoom(name);
    }

    @GetMapping
    public List<ChatRoom> findAllRoom() {
        return chatService.findAllRoom();
    }
}

Handler 수정.

WebSocket 클라이언트로부터 채팅 메시지를 받아 , 채팅 메시지 객체로 변환

전달받은 메시지에 담긴 채팅방 id로 채팅방 정보조회

해당 채팅방에 있는 모든 클라이언트에게 메세지 전송.

@Slf4j
@RequiredArgsConstructor
@Component
public class WebSockChatHandler extends TextWebSocketHandler {
    private final ObjectMapper objectMapper;
    private final ChatService chatService;

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("payload {}", payload);
// 삭제        TextMessage textMessage = new TextMessage("Welcome chatting sever~^^ ");
// 삭제       session.sendMessage(textMessage);
        ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
        ChatRoom room = chatService.findRoomById(chatMessage.getRoomId());
        room.handleActions(session, chatMessage, chatService);
    }
}

1. RestAPI로 채팅방 생성

2. 채팅방이 생성되면 roomId가 올것임

3. 이 roomId를 가지고 , websocket 발송.

728x90