일반적으로 HTTP 프로토콜은 클라이언트가 서버에 요청을 보내고, 서버는 요청에 대한 응답을 보내는 단방향 통신 방식이다. 이는 주로 웹 페이지를 요청하고 정적인 콘텐츠를 전달하는 데에 사용된다. 그러나 HTTP는 요청-응답 사이클이 완료되면 연결을 닫게 되며, 실시간 업데이트가 필요한 애플리케이션에는 적합하지 않다.
WebSocket은 이러한 제한을 극복하기 위해 설계되었다. 클라이언트와 서버 간의 연결을 유지하면서, 양방향으로 데이터를 전송할 수 있다. 이를 통해 실시간 채팅, 주식 시장 업데이트, 게임 등과 같은 실시간 데이터 통신이 가능해진다.
클라이언트가 서버에 WebSocket 연결 요청을 보낸다.
이 요청은 HTTP 프로토콜을 사용하여 보내지며, 요청 헤더에 "Upgrade"와 "Connection" 필드가 포함된다. 이를 통해 클라이언트가 WebSocket 연결을 요청하고 있다는 것을 알려준다.
서버는 클라이언트의 요청을 받으면, 이를 확인하고 WebSocket 연결을 수락한다.
이를 위해 서버는 HTTP 응답을 반환하며, 응답 헤더에 "Upgrade"와 "Connection" 필드를 포함시킨다. 또한, "Sec-WebSocket-Accept" 필드를 포함하여 요청의 유효성을 검증한다.
클라이언트는 서버의 응답을 받으면, WebSocket 연결이 수락되었다는 것을 확인하고 연결을 확립한다. 이후부터는 클라이언트와 서버 간의 연결이 지속적으로 유지된다.
연결이 확립된 후, 클라이언트와 서버는 양방향으로 데이터를 주고받을 수 있다.
이를 위해 클라이언트와 서버는 WebSocket 프레임이라는 단위로 데이터를 교환한다.
클라이언트나 서버는 원하는 시점에 데이터를 보낼 수 있으며, 상대방은 해당 데이터를 수신하여 처리한다.
클라이언트 또는 서버는 연결을 종료하기 위해 명시적으로 연결을 닫을 수 있다.
이를 위해 클라이언트나 서버는 WebSocket 프레임에 종료 코드를 포함시켜 상대방에게 연결 종료를 알린다. 상대방은 이를 수신하고 연결을 닫는다.
@Slf4j
@Component
public class SimpleChatHandler extends TextWebSocketHandler {
// 현재 연결되어 있는 클라이언트를 관리하기 위한 리스트
private final List<WebSocketSession> sessions = new ArrayList<>();
// 사용자 이름으로 세션을 구분하려면?
// private Map<String, WebSocketSession> sessionByUsername;
public void broadcast(String message) throws IOException {
for (WebSocketSession connected: sessions) {
connected.sendMessage(new TextMessage(message));
}
}
@Override
// WebSocket 최초 연결시 실행
public void afterConnectionEstablished(
// 연결된 클라이언트를 나타내는 객체
WebSocketSession session
) throws Exception {
// 방금 참여한 사용자를 저장
sessions.add(session);
log.info("connected with session id: {}, total sessions: {}", session.getId(), sessions.size());
}
@Override
// WebSocket 메세지를 받으면 실행
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("received: {}", payload);
for (WebSocketSession connected: sessions) {
connected.sendMessage(message);
}
}
@Override
// WebSocket 연결이 종료 되었을 때
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("connection with {} closed", session.getId());
// 더이상 세션 객체를 보유하지 않도록
sessions.remove(session);
}
}
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final SimpleChatHandler simpleChatHandler;
public WebSocketConfig(
SimpleChatHandler simpleChatHandler
) {
this.simpleChatHandler = simpleChatHandler;
}
@Override
// WebSocketHandler 객체를 등록하기 위한 메소드
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(simpleChatHandler, "ws/chat")
.setAllowedOrigins("*");
}
}
WebSocket 구성을 정의하고, WebSocketHandler 및 관련 구성 요소를 등록하기 위해 사용한다.
WebSocketConfigurer 인터페이스를 구현한 클래스가 WebSocket 구성을 정의하기 위해서는 클래스에 @Configuration
어노테이션과 @EnableWebSocket
어노테이션을 적용하여 WebSocket 지원을 활성화해야 한다.
chat.html
에서 form 태그를 제출하는 이벤트가 발생할 때마다 “http://localhost:8080/ws/chat”
의 경로로 요청하도록 코드를 작성해 놓았기 때문에 WebSocketHandler 객체를 “ws/chat”
경로에 등록한다.
모든 클라이언트가 접근할 수 있도록 .setAllowedOrigins(”*”)
로 설정해 놓았다.
@Controller
@RequestMapping("chat")
@RequiredArgsConstructor
public class ChatController {
private final SimpleChatHandler simpleChatHandler;
private final Gson gson;
@GetMapping("test")
public @ResponseBody String test() throws IOException {
simpleChatHandler.broadcast(gson.toJson(new ChatMessage("admin", "10분 후 서버가 종료됩니다.")));
return "done";
}
@GetMapping("rooms")
public String rooms() {
return "rooms";
}
@GetMapping("enter")
public String enter(@RequestParam("username") String username) {
return "chat";
}
}
WebSocket이 양방향 통신 채널을 제공했다면, STOMP(Streaming Text Oriented Messaging Protocol)는 주고 받는 메시지를 구조화하고 전송하는 프로토콜로 사용된다.
Topic(주제)
Topic은 관련된 메시지를 그룹화하고 전달할 수 있는 주제나 주제 영역을 의미한다.
특정 주제에 대해 관심(구독)이 있는 사용자들은 Topic을 이용해 메시지를 공유하고 전달할 수 있다.
Subscribe(구독)
Subscribe 명령어를 사용하면 웹 애플리케이션이 메시지를 구독할 수 있다.
구독하려는 대상 주제(Topic)를 지정하고, 해당 주제로 전송되는 모든 메시지를 수신하게 된다.
Send(전송)
Send 명령어를 사용하여 메시지를 전송한다.
메시지를 보내려면 대상 주제를 지정하고, 메시지 내용(Body)을 포함하여 메시지를 서버에 전송한다.
Message(메시지)
메시징 서버에서 웹 애플리케이션으로 전송되는 메시지를 나타낸다.
메시징 서버는 구독 Subscribe 명령어로 구독한 주제의 메시지를 Message 명령어를 통해 웹 애플리케이션에 전송한다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
@Override
// STOMP 엔드포인트 설정용 메소드
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chatting");
}
@Override
// MessageBroker를 활용하는 방법 설정
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
WebSocket 및 STOMP 프로토콜을 구성하는 데 사용한다.
registerStompEndpoints()
: 연결 엔드포인트 등록configureMessageBroker()
: 메시지 브로커 구성@Slf4j
@Controller
@RequiredArgsConstructor
public class WebSocketMapping {
// STOMP over WebSocket
private final SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/chat")
public void sendChat(
ChatMessage chatMessage,
@Headers Map<String, Object> headers,
@Header("nativeHeaders") Map<String, String> nativeHeaders
) {
log.info(chatMessage.toString());
log.info(headers.toString());
log.info(nativeHeaders.toString());
String time = new SimpleDateFormat("HH:mm").format(new Date());
chatMessage.setTime(time);
simpMessagingTemplate.convertAndSend(
String.format("/topic/%s", chatMessage.getRoomId()),
chatMessage
);
}
}
메시지를 발행하는 측과 해당 메시지를 수신하고자 하는 구독자 간의 통신을 기반으로 한다.
패턴의 기본 아이디어는 발행자(Publisher)가 메시지를 특정 주제(Topic)로 발행하고, 이 주제에 관심을 가지는 구독자(Subscriber)가 해당 메시지를 수신하는 것이다.
발행자는 메시지를 주제로 식별할 수 있도록 발행할 때 해당 주제를 지정하며, 구독자는 관심 있는 주제를 구독하여 해당 주제로 발행된 메시지를 수신한다.
Spring에서 제공하는 메시징 템플릿으로 실시간 메시징을 구현할 때 유용하다.
메시지 전송, 변환, 주제 기반 브로드캐스트, 개별 클라이언트에게 메시지 전송 등 다양한 기능을 제공하여 실시간 메시징을 쉽게 구현할 수 있도록 도와준다.
여기서는 converAndSend()
메서드에 매개변수로,
1) 목적지인 특정 주제 2) 전송할 메시지 내용을 담아 주제를 구독하고 있는 모든 클라이언트에게 전달한다.
@Slf4j
@Controller
@RequiredArgsConstructor
public class WebSocketMapping {
// STOMP over WebSocket
private final SimpMessagingTemplate simpMessagingTemplate;
// 누군가가 구독할 때 실행하는 메소드
@SubscribeMapping("/topic/{roomId}")
public ChatMessage sendGreet(
@DestinationVariable("roomId")
Long roomId
) {
log.info("new subscription to {}", roomId);
ChatMessage chatMessage = new ChatMessage();
chatMessage.setRoomId(roomId);
chatMessage.setSender("admin");
chatMessage.setMessage("hello!");
String time = new SimpleDateFormat("HH:mm").format(new Date());
chatMessage.setTime(time);
return chatMessage;
}
}
출처 : 멋사 5기 백엔드 위키 9팀 9글