[Chatting Server] Basic WebSocket Server

원알렉스·2020년 9월 8일
0

Chatting Server

목록 보기
1/2
post-thumbnail

🎯 WebSocket 으로 채팅서버 구현

일반적인 HTTP 통신을 하는 서버들과 달리 채팅 서버는 Socket 통신을 하는 서버가 필요합니다. 통상적으로 HTTP 통신은 Client 의 요청이 있을 때만 서버가 응답하고 연결을 종료하는 단방향 통신 입니다. 따라서 Client 가 Server 에 접속해 데이터를 요청하고 결과를 받아오는 구조의 서비스에서 많이 사용됩니다.

이와 반대로 Socket 통신은 Server 와 Client 가 지속적으로 연결을 유지하고 양방향 통신 을 하는 방식입니다. 주로 채팅 같은 실시간성을 요구하는 서비스에서 많이 사용됩니다.

✓ WebSocket

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

✓ Spring Boot 프로젝트 구성

의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-websocket'

WebSocket Handler 구현

Socket 통신은 Server 와 Client 가 1:N 의 관계 를 맺게 됩니다. 따라서 한 Server 에 여러 Client 가 접속할 수 있으며, Server 에는 여러 Client 가 전송한 메시지를 받아 처리해줄 Handler 의 구현이 필요합니다.

@Component
public class WebSocketHandler extends TextWebSocketHandler {

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        TextMessage textMessage = new TextMessage(payload);
        session.sendMessage(textMessage);
    }
}

WebSocket Config

  • 위에서 구현한 Handler 를 이용하여 WebSocket 을 활성화하기 위한 Config 파일을 작성해야 합니다.

  • @EnableWebSocket 을 통해서 WebSocket 을 활성화 시켜줍니다.

  • WebSocket 연결에 접속하기 위한 EndPoint 는 /chat으로 설정하고 도메인이 다른 서버에서도 접속 가능하도록 우선 setAllowedOrigins("*")을 통해 모두 활성화 시켜줍니다.

  • 이제 Client 가 ws://localhost:8080/chat 으로 커넥션을 연결하고 메시지 통신을 할 수 있는 준비가 끝났습니다.

@EnableWebSocket
@Configuration
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private WebSocketHandler webSocketHandler;

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

🚀 확장하기

위에서 구현한 WebSocket 통신은 ws://localhost:8080/chat 에 연결된 Client 끼리만 메시지 통신이 가능합니다. 즉, 채팅방이 하나 뿐인 Server 입니다. 여러개의 채팅방을 만들어서 해당 채팅방에 입장한 Client 들간에 메시지를 교환하려면 고도화가 필요합니다.

Client들은 Server에 접속하면 개별의 WebSocket Session을 가지게 됩니다. 따라서 채팅방에 입장시 Client들의 WebSocket Session 정보를 채팅방에 매핑시켜서 보관하고 있으면 서버에 전달된 메시지를 특정 방의 WebSocket Session으로 보낼 수 있으므로 개별의 채팅방을 구현할 수 있습니다.

✓ 채팅 메시지 구현

public class ChatMessage {

    private String id;
    private String chatRoomId;
    private String sender;
    private String message;
    private MessageType messageType;
}

✓ 채팅방 구현

채팅방을 구현하기 위해 DTO를 하나 생성해줍니다. 채팅방은 입장한 Client들의 정보를 가지고 있어야 하므로 WebSocket Session 정보를 리스트 형태의 멤버 필드로 갖습니다. 채팅방에서는 입장과 대화하기의 기능이 있으므로 handleAction 메소드를 통해 분기처리 해줍니다. 입장 시에는 채팅방의 session 리스트에 입장한 Client의 session을 추가해줍니다. 그리고 해당 채팅방에 메시지가 도착할 경우 해당 채팅방의 모든 session으로 메시지를 전송하면 됩니다.

public class ChatRoom {

    private String id;
    private String name;
    private Set<WebSocketSession> sessions = new HashSet<>();
    
    public void handleAction(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
        if (chatMessage.getMessageType().equals(MessageType.ENTER)) {
            sessions.add(session);
            chatMessage.setMessage(chatMessage.getSender() + " 님이 입장했습니다.");
        }
        sendMessage(chatMessage, chatService);
    }

    private void sendMessage(ChatMessage chatMessage, ChatService chatService) {
        sessions
                .parallelStream()
                .forEach(session -> chatService.sendMessage(session, chatMessage));
    }
    
    ...
}

✓ 채팅 서비스 구현

채팅방을 생성, 조회하고 하나의 세션에 메시지 전송을 담당하는 서비스 클래스를 구현해줍니다. sendMessage() 메소드는 전달 받은 session으로 메시지를 보냅니다.

@Service
public class ChatService {

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

    @Autowired
    public ChatService(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        this.chatRooms = new LinkedHashMap<>();
    }

    public List<ChatRoom> findAllChatRoom() {
        return new ArrayList<>(chatRooms.values());
    }

    public ChatRoom findChatRoomById(String id) {
        return chatRooms.get(id);
    }

    public ChatRoom createChatRoom(String name) {
        String id = UUID.randomUUID().toString();
        ChatRoom newChatRoom = ChatRoom.of(id, name);
        chatRooms.put(id, newChatRoom);
        return newChatRoom;
    }

    public void sendMessage(WebSocketSession session, ChatMessage chatMessage) {
        try {
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessage)));

        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}

✓ 채팅 컨트롤러 구현

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

    @Autowired
    private ChatService chatService;

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

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

✓ WebSocket Handler 수정

  • WebSocket Client로부터 메시지를 전달받아 채팅 메시지 객체로 변환
  • 전달 받은 메시지에 담긴 채팅방 ID로 전송해야할 채팅방 조회
  • 해당 채팅방에 입장해 있는 모든 Client(WebSocket session)들에게 메시지 전송
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    String payload = message.getPayload();

    ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
    ChatRoom chatRoom = chatService.findChatRoomById(chatMessage.getChatRoomId());
    chatRoom.handleAction(session, chatMessage, chatService);
}
profile
Alex's Develog 🤔

0개의 댓글