HTTP의 특성 - Connectionless
모든 HTTP를 사용한 통신은 클라이언트가 먼저 요청을 보내고, 그 요청에 따라 웹 서버가 응답하는 형태이며 웹 서버는 응답을 보낸 후 웹 브라우저와의 연결을 끊는다.
Websocket이 존재하기 전에는 Polling, Long Polling, Streaming 방식으로 해결
Polling : 클라이언트가 평범한 HTTP Request를 서버로 계속 요청해 이벤트 내용을 전달 받는 방식
Long Polling : 클라이언트 → 서버로 HTTP Request 요청, 서버 → 클라이언트로 전달할 이벤트가 있다면 그 순간 Response 메세지를 전달하며 연결을 종료
Streaming : Long Polling과 비슷하게 클라이언트 → 서버로 HTTP. Request 요청, 서버 → 클라이언트로 이벤트를 전달할 때 해당 요청을 해제하지 않고 필요한 메세지만 보내기를 반복하는 방식
웹소켓 접속 과정은 TCP/IP 접속 → 웹소켓 열기 → HandShake 과정으로 나눌 수 있다. 웹소켓도 TCP/IP위에서 동작하므로, 서버와 클라이언트는 웹소켓을 사용하기 전에 서로 TCP/IP 접속이 되어있어야 한다. TCP/IP 접속이 완료된 후 서버와 클라이언트는 웹소켓 열기 HandShake 과정을 시작한다.
웹소켓 열기 핸드셰이크는 클라이언트가 먼저 핸드셰이크 요청을 보내고 이에 대한 응답을 서버가 클라이언트로 보내는 구조이다. 서버와 클라이언트는 HTTP 1.1 프로토콜을 사용하여 요청과 응답을 보낸다
소켓(Socket) : 네트워크상에서 동작하는 프로그램 간 통신의 종착점. 1대1 통신의 경우 양 측다 소켓이 존재해야 통신이 가능하다. 현재 대부분의 통신은 인터넷 프로토콜(TCP, UDP)에 기반하고 있으므로 대부분의 네트워크 소켓은 인터넷 소켓이다.
웹 소켓(Web Socket) : 웹소켓은 하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜이다. 그리고 HTTP나 HTTPS 위에서 동작하도록 설계되었으며, 따라서 포트는 80번 혹은 443번이다. HTTP 프로토콜과 구별은 되지만 호환이 된다.
사실 이 둘은 서로 상반되는 개념이 아니기 때문에 완전하게 차이점을 비교할 수는 없다. 웹에서도 TCP 소켓 통신으로 실시간 통신을 할 수는 있지만 전송 계층의 원시 바이트 대신 애플리케이션 계층을 통해 메시지를 보내는 것이 개발측면에서 더 적합하기 때문에 TCP 소켓통신에 기반하여 웹 소켓을 발전시킨 것이기 때문이다.
웹 소켓은 TCP 소켓과 구분되는 것이 아니라 TCP 소켓의 추상화된 형태이다.
“웹 소켓과 소켓은 전혀 다르다” 보다는 “소켓 통신에 기반하여 웹 소켓은 웹 어플리케이션에 맞게 발전한 형태로 소켓 통신을 한다” 라고 보면 좋을 것 같다.
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WepSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/junsu/chat")
.setAllowedOrigins("*");
}
}
@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);
}
}
@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));
}
}
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);
}
}
즉, 웹소켓을 이용하여 각 채팅방 식별자에 해당하는 세션들에게 메시지를 전달하는 방식으로 채팅 서비스를 구현해 보았다.