[기술정리]웹 소켓

송송승현's avatar
Dec 20, 2024
[기술정리]웹 소켓
💡
HTTP 기반의 프로토콜
서버와 클라이언트 간에 실시간 양방향 통신을 지원하는 기술
지속적 연결을 유지
양측이 데이터 송수신을 자유롭게 가능

웹 소켓의 특징

  • 양방향 통신 : 서버와 클라이언트가 자유롭게 데이터를 주고받음
  • 지속적 연결 : 초기 연결 후 클라이언트와 서버 간 연결이 유지
  • 저지연성: 헤더 오버헤드가 줄어들어 빠른 데이터 교환

동작 원리

  • http 핸드셰이크
    • 웹소켓 연결은 http 요청으로 시작
    • 클라이언트가 서버로 업그레이드 요청을 보내고, 서버가 수락하면 웹 소켓 프로토콜로 전환
  • 지속적 연결
    • 연결이 유지된 상태에서 서버와 클라이언트가 데이터 송수신’
  • 종료
    • 라이언트 또는 서버가 명시적으로 연결 종료를 요청, 네트워크 문제가 발생하면 연결이 종료
 

http 요청, 응답 헤더

// 요청 GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 // 응답 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
 

Stomp를 활용한 예제

SocketConfig

import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; // SRP: 메세지 브로커 세팅 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 구독 발행 엔드포인트 설정 @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/sub"); // sub로 시작하는 모든 주소 registry.setApplicationDestinationPrefixes("/pub"); // pub로 시작하는 모든 주소 } // 웹소켓 연결 엔드포인트 설정 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/connect").setAllowedOrigins("*"); } }

Entity

import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "chat_tb") @Entity public class Chat { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String msg; @Builder public Chat(Integer id, String msg) { this.id = id; this.msg = msg; } }

Controller

import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @RequiredArgsConstructor @Controller public class ChatController { private final ChatService chatService; private final SimpMessageSendingOperations sms; @GetMapping("/save-form") public String saveForm() { return "save-form"; } @GetMapping("/") public String index(Model model) { model.addAttribute("models",chatService.findAll()); return "index"; } // @MessageMapping("/pub") @PostMapping("/chat") public String save(String msg){ Chat chat = chatService.save(msg); sms.convertAndSend("/sub/chat",chat); return "redirect:/"; } }

Service

import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.util.List; @Service @RequiredArgsConstructor public class ChatService { private final ChatRepository chatRepository; @Transactional public Chat save(String msg){ Chat chat = Chat.builder().msg(msg).build(); return chatRepository.save(chat); } public List<Chat> findAll(){ Sort desc = Sort.by(Sort.Direction.DESC, "id"); return chatRepository.findAll(desc); } }

index.mustache

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Document</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> </head> <body> <div> <ul> <li><a href="/">채팅목록</a></li> <li><a href="/save-form">채팅 메시지쓰기</a></li> </ul> </div> <h1>채팅 목록</h1> <hr> <ul id="chat-box"> {{#models}} <li>{{msg}}</li> {{/models}} </ul> <script> // 1. 웹소켓 연결 세팅 및 연결 완료 let socket = new WebSocket('ws://192.168.0.99:8080/connect'); let stompClient = Stomp.over(socket); stompClient.connect({}, (frame)=>{ console.log("1", "Connected"); stompClient.subscribe("/sub/chat", (response)=>{ console.log("2", response); let body = JSON.parse(response.body); console.log("3", body); attack(body.msg); }); }); function attack(msg){ // 1. body 초기화 document.querySelector("body").innerHTML = ""; // 2. body 스타일 설정 document.querySelector("body").style.cssText = ` margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; background-color: black; `; // 3. 글자 요소 생성 const textElement = document.createElement("div"); textElement.textContent = msg; // 텍스트 내용 textElement.style.cssText = ` color: white; font-size: 50px; font-family: Arial, sans-serif; `; // 4. body에 추가 document.querySelector("body").appendChild(textElement); } </script> </body> </html>

chat.html

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Document</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> </head> <body> <button onclick="send1()">메시지전송1(pub)</button> <hr> <script> // 1. 웹소켓 연결 세팅 및 연결 완료 let socket = new WebSocket('ws://192.168.0.99:8080/connect'); let stompClient = Stomp.over(socket); stompClient.connect({}, (frame)=>{ console.log("1. Connected"); // 2. 구독하기 /sub -> /pub/room 이런식으로 해야 발동함 stompClient.subscribe("/sub/2", (response)=>{ console.log("2. Sub/2"); console.log(response); }); }); function send1(){ stompClient.send("/pub/room", {}, "2"); } </script> </body> </html>
 
Share article

송승현의 블로그