Websocket 이란?
양방향으로 실시간 데이터 를 교환할수 있는 프로토콜 이다. 이를 통해 실시간 정보 업데이트, 실시간 채팅 등을 구현할수 있고 좀 더 나아가 화상채팅도 구현할수 있다. 또한 HTTP 프로트콜에서 WS 프로트콜에 변환해서 연결을 한다.
Polling
클라이언트가 n초 간격으로 request를 서버로 계속 날려서 response를 전달받는 방식이다. 단, 이렇게 request 를 날리게 되면 서버에 부담이 된다. 그렇다고 n초를 길게 잡으면 실시간 데이터 통신이 아니니 이상하다. 이를 해결하기 위해 WebSocket이 나온것 같다.
Polling의 종류는 대표적으로 Short Polling, Long Polling 있다.
Websocket 구현
간단하게 구현을 해보았다. 간단하게 설명을 하자면, ChatMessage DTO 를 이용하여 ChatController 를 통해 데이터를 양반향으로 여러명이 채팅 할수 있는 구현이다.
WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("http://localhost:8080/") //어디 주소에서 접근 허용
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app"); //클라이언트가 /app 접두어를 사용하여 메세지 전송.
registry.enableSimpleBroker("/topic"); //서버가 해당 메세지를 /topic 구독한 클라이언트에게 전다.
}
}
/ws 에 SockJS 을 보냈다 단, localhost:8080 에서만 접근 한 파일이다.
클라이언트는 서버에서 SockJS 를 받아 메세지를 보내거나 받을수 있다.
ChatController.java
@Controller
@RequiredArgsConstructor
public class ChatController {
private final SimpMessageSendingOperations template;
@MessageMapping("/chat.sendMessage")
public void sendMessage(@Payload ChatMessage chatMessage) {
System.out.println("메세지 : "+chatMessage.content());
template.convertAndSend("/topic/public", new ChatMessage(MessageType.CHAT, chatMessage.content(), chatMessage.sender()));
}
@MessageMapping("/chat.addUser")
public void addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor){
headerAccessor.getSessionAttributes().put(chatMessage.sender()+"", chatMessage.sender());
template.convertAndSend("/topic/public", new ChatMessage(MessageType.JOIN, chatMessage.sender()+"님이 처음 입장 하셨습니다.",chatMessage.sender()));
}
}
SimpMessageSendingOperations 인터페이스를 통해 내가 원하는 주소에 데이터를 보낼수 있다. 이를 통해 romm 을 통한 채팅도 만들수 있을것 같다. @MessageMapping 은 클라이언트가 어떠한 주소에 데이터를 보낼시 받는 어노테이션이다.
@Payload 는 WebSocket 핸들러 메서드에서 메시지 페이로드를 사용할 때 적용되며, 해당 페이로드를 메서드의 매개변수에 바인딩한다. 또한 SimpMessageHeaderAccessor 클래스는 Spring WebSocket 에서 사용하는 클래스이다. 꼭 써야 하는점은 아니지만 회사마다 다르기 때문에 잘 고려해서 써야 된다. 그리고 headerAccessor.getSessionAttributes() 메서드는 현재 WebSocket 세션의 속성을 가져옵니다. 이 속성은 WebSocket 세션에 대한 추가 정보를 저장하고 공유할 수 있는 방법입니다. 예를 들어, 사용자 ID, 사용자 인증 정보, 세션 상태 등을 속성으로 저장할 수 있다.
MainController
@Controller
public class MainController {
@RequestMapping("/")
public String main(){
return "index";
}
}
ChatMessage
public record ChatMessage(
MessageType messageType,
String content,
String sender
) {
public ChatMessage(MessageType messageType, String content, String sender) {
this.messageType = messageType;
this.content = content;
this.sender = sender;
}
}
MessageType
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>테스트</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<style>
.button-bar{
width: 100px;
height: 50px;
float:right;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="username-page">
<div class="username-page-container">
<div class="button-bar">
<form id="usernameForm" name="usernameForm">
<input type="hidden" id="name" value="로그인할시닉네임" class="form-control" />
<button type="submit" class="accent username-submit"><h3>채팅 열기</h3></button>
</form>
</div>
</div>
</div>
<div id="chat-page" class="hidden">
<div class="chat-container">
<div class="chat-header">
<h2>상담원과 채팅</h2>
</div>
<div class="connecting">
연결중...
</div>
<ul id="messageArea">
</ul>
<form id="messageForm" name="messageForm">
<div class="form-group">
<div class="input-group clearfix">
<input type="text" id="message" placeholder="Type a message..." autocomplete="off" class="form-control"/>
<button type="submit" class="primary">보내기</button>
</div>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/dist/umd/popper.min.js" integrity="sha384-zYPOMqeu1DAVkHiLqWBUTcbYfZ8osu1Nd6Z89ify25QV9guujx43ITvfi12/QExE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.min.js" integrity="sha384-Y4oOpwW3duJdCWv5ly8SCFYWqFDsfob/3GkgExXKV4idmbt98QcxXYs9UoXAB7BZ" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>
main.js
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var colors = [
'#2196F3', '#32c787', '#00BCD4', '#ff5652',
'#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];
function connect(event) {
username = document.querySelector('#name').value.trim();
if(username) {
usernamePage.classList.add('hidden');
chatPage.classList.remove('hidden');
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
}
event.preventDefault();
}
function onConnected() {
// Subscribe to the Public Topic
stompClient.subscribe('/topic/public', onMessageReceived);
// Tell your username to the server
stompClient.send("/app/chat.addUser",
{},
JSON.stringify({sender: username, type: 'JOIN'})
)
connectingElement.classList.add('hidden');
}
function onError(error) {
connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
connectingElement.style.color = 'red';
}
function sendMessage(event) {
var messageContent = messageInput.value.trim();
if(messageContent && stompClient) {
var chatMessage = {
sender: username,
content: messageInput.value,
type: 'CHAT'
};
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
messageInput.value = '';
}
event.preventDefault();
}
function onMessageReceived(payload) {
var message = JSON.parse(payload.body);
var messageElement = document.createElement('li');
if(message.type === 'JOIN') {
messageElement.classList.add('event-message');
message.content = message.sender + ' joined!';
} else if (message.type === 'LEAVE') {
messageElement.classList.add('event-message');
message.content = message.sender + ' left!';
} else {
messageElement.classList.add('chat-message');
var avatarElement = document.createElement('i');
var avatarText = document.createTextNode(message.sender[0]);
avatarElement.appendChild(avatarText);
avatarElement.style['background-color'] = getAvatarColor(message.sender);
messageElement.appendChild(avatarElement);
var usernameElement = document.createElement('span');
var usernameText = document.createTextNode(message.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
var textElement = document.createElement('p');
var messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
messageArea.appendChild(messageElement);
messageArea.scrollTop = messageArea.scrollHeight;
}
function getAvatarColor(messageSender) {
var hash = 0;
for (var i = 0; i < messageSender.length; i++) {
hash = 31 * hash + messageSender.charCodeAt(i);
}
var index = Math.abs(hash % colors.length);
return colors[index];
}
usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)
'Spring' 카테고리의 다른 글
WebSocket(2) (0) | 2023.05.25 |
---|---|
Spring Security (1) (0) | 2023.05.24 |
thymeleaf (0) | 2023.05.20 |
Querydsl (2) | 2023.05.19 |
Rest Repositories (0) | 2023.05.19 |