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