728x90
전체흐름
Server
dependencies
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
implementation 'org.webjars:sockjs-client:1.0.2'
implementation 'org.webjars:stomp-websocket:2.3.3'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Entity: HelloMessage
채팅 입력하는 하는 곳
package com.example.animalwarchatting.entity;
import ...
// ## 1
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HelloMessage {
private String name;
private String content;
}
Entity: Greeting
입력한 값 받아오는 곳
package com.example.animalwarchatting.entity;
import ...
// ## 2
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Greeting {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String message;
}
WebsocketConfiguration
메시지 처리
package com.example.animalwarchatting.config;
import ...
// ## 3
@Configuration
@EnableWebSocketMessageBroker // 웹소켓 STOMP 통신 기능 활성화.. 메시지 브로커를 사용하여 WebSocket 메시지 처리를 활성화하는 데 사용
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) { // WebSocket 엔드포인트를 등록, 클라이언트가 해당 엔드포인트를 통해 연결할 수 있도록 설정
WebSocketMessageBrokerConfigurer.super.registerStompEndpoints(registry);
// #1 socket을 front와 연결
// 클라이언트에서 웹소켓 STOMP 연결을 위해 지정할 엔드포인트 URL을 등록..
registry.addEndpoint("/stomp-endpoint") // 'stomp-endpoint'라는 엔드포인트를 등록, 클라이언트는 이 엔드포인트를 통해 WebSocket 연결을 시도
.withSockJS(); // SockJS를 사용하여 연결, SockJS는 WebSocket이 지원되지 않는 일부 브라우저에서도 WebSocket 연결을 에뮬레이트하게 도와줍니다.
}
// 1.메시지 중개기 URL을 설정합니다, 2.메시지 핸들러 메서드 URL을 설정합니다
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) { // 메시지 브로커를 구성하는 역할
WebSocketMessageBrokerConfigurer.super.configureMessageBroker(registry);
// #2 url에서 "/topic"을 접두사로 가지고 있는 건 구독하는 하는 것이다..
//도착지가 /topic으로 시작하는 메시지는 중개기로 보냅니다
registry.enableSimpleBroker("/topic"); // "/topic"으로 시작하는 대상 주제를 구독하는 클라이언트에게 메시지를 브로드캐스트하는 데 사용, 이것은 메시지를 구독하는 클라이언트들에게 메시지를 보내는 메시지 브로커를 활성화
// #4 url에서 "/app"을 접두사로 가지고 있는 게 있으면 메시지 보내는 것이다..
// /app으로 시작하는 메시지는 @MassageMapping을 붙인 핸들러 메서드로 보냅니다
registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 메시지를 송신할 때 사용하는 목적지 접두어(prefix)를 설정합니다. 즉, 클라이언트가 "/app"으로 시작하는 메시지 목적지에 메시지를 송신할 수 있습니다.
}
}
주석없는 버전
package com.example.animalwarchatting.config;
import ...
// ## 3
@Configuration
@EnableWebSocketMessageBroke
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
WebSocketMessageBrokerConfigurer.super.registerStompEndpoints(registry);
registry.addEndpoint("/stomp-endpoint")
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
WebSocketMessageBrokerConfigurer.super.configureMessageBroker(registry);
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
GreetingController
받아온(@MessageMapping("hello") 메세지를 다시 화면에 보내준다(SendTo("/topic/greetings")).
package com.example.animalwarchatting.controller;
import ...
// ## 4
@RestController
@RequiredArgsConstructor
public class GreetingController {
private final GreetingService greetingService;
private final JwtService jwtService;
@MessageMapping("/hello") // #4 "/app" 붙어 있는 건.. 여기서 메시지 받기
@SendTo("/topic/greetings") // #2 "/topic/greetings"를 구독하고 여기에 메세지를 보내겠다..
// 메시지를 수신하고 처리한 후, 처리된 결과를 특정 대상 주제(topic)로 다시 보내는 데 사용
// 받은 메시지를 다시 "/topic/greetings"로 보낸다.(SendTo)
public Greeting greet(HelloMessage message) {
return greetingService.save(message); // save는 db에 저장하기 위함..
}
}
여기까지만 하면 채팅서비스의 서버는 구현이 됐다.
추가적으로 MySQL에 채팅내용 저장하는 것을 구현..
DB 저장
GreetingService
db저장 로직
package com.example.animalwarchatting.service;
import ..
@Service
@RequiredArgsConstructor
public class GreetingService {
private final GreetingRepository greetingRepository;
public Greeting save(HelloMessage message) {
Greeting greeting = Greeting.builder()
.name(message.getName())
.message(message.getContent())
.build();
greetingRepository.save(greeting);
return greeting;
}
}
GreetingRepository
package com.example.animalwarchatting.repository;
import ...
@Repository
public interface GreetingRepository extends JpaRepository<Greeting, Long> {
Greeting save(Greeting greeting);
}
MySQL
채팅을 30개까지만 저장하고, 30개 이후부터는 1부터 삭제
-- MySQL에 넣으면 됨..
create database animalwarChatting;
use animalwarChatting;
-- ID가 31보다 큰 데이터를 삭제
-- DELETE FROM greeting
-- WHERE id <= (SELECT MAX(id) - 30 FROM greeting);
DELIMITER
CREATE EVENT delete_old_records_event_limit_30
ON SCHEDULE EVERY 1 SECOND
DO
BEGIN
DECLARE old_id INT;
SET old_id = (SELECT MAX(id) - 30 FROM greeting);
IF old_id > 0 THEN
DELETE FROM greeting WHERE id <= old_id;
END IF;
END;
DELIMITER ;
-- 이벤트 활성화
ALTER EVENT delete_old_records_event_limit_30 ON COMPLETION NOT PRESERVE ENABLE;
-- 이벤트 삭제
--DROP EVENT delete_old_records_event_limit_30;
-- 테이블 확인
select * from greeting;
-- 행 수 확인
SELECT COUNT(*) AS row_count
FROM greeting;
Web
resources-static에 만들면 됨..서버와 분리하고 싶으면 프로젝트를 새로하면 됨..
app.js
var stompClient = null;
let id = null;
let nickName = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#greetings").html("");
}
function connect() {
var socket = new SockJS('/stomp-endpoint'); // #1 socket열기(server와 연결)
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) { // #2'/topic/greetings'을 구독
showGreeting(JSON.parse(greeting.body)); // #2-2 '/topic/greetings'에서 받은 메세지를 분해해서 보여주기
});
});
}
// # 2-2
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message.name + " : " + message.message + "</td></tr>");
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// #4 메시지 보내기
function sendName() {
const obj = {
'name' : nickName != null ? nickName : "GUEST",
'content' : $("#content").val()
}
stompClient.send("/app/hello", {}, JSON.stringify(obj));
}
function parseToken() {
const url = 'http://localhost:8080/api/v1/user/me';
const token = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJuaWNrTmFtZSI6Imdvb2Rib3kiLCJpZCI6Imp1bmtpIiwic3ViIjoianVua2kiLCJleHAiOjE2OTc3ODk1MzR9.xbClRoOdXXEeI8k2AfbpUE2jsaLX_zT6JUdJ1WD-e3k';
fetch(url, {
method: 'GET',
headers: {
'Authorization': token,
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
console.log('서버 응답:', data);
id = data.id;
nickName = data.nickName;
})
.catch(error => {
console.error('에러 발생:', error);
});
}
$(function () {
parseToken(); //
connect(); // 바로 connect.. disconnect없앴음..
$("form").on('submit', function (e) {
e.preventDefault();
});
// $( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
});
index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello WebSocket</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">-->
<link href="/main.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- <script src="/webjars/jquery/jquery.min.js"></script>-->
<!-- <script src="/webjars/sockjs-client/sockjs.min.js"></script>-->
<!-- <script src="/webjars/stomp-websocket/stomp.min.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<!-- <label for="connect">WebSocket connection:</label>-->
<!-- <button id="connect" class="btn btn-default" type="submit">Connect</button>-->
<button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<input type="text" id="content" class="form-control" placeholder="Your content here...">
</div>
<button id="send" class="btn btn-default" type="submit">Send</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>Greetings</th>
</tr>
</thead>
<tbody id="greetings">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
main.css
body {
background-color: #f5f5f5;
}
#main-content {
max-width: 940px;
padding: 2em 3em;
margin: 0 auto 20px;
background-color: #fff;
border: 1px solid #e5e5e5;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
Notes
위 코드는 현재 진행중인 프로젝트와 섞여 있기 때문에 정말 기본적이 Socket을 하고자 한다면 공유하는 링크에서 참고하는 것이 좋다.
채팅 확인을 위한 테스트용 화면이다.
추후 조원들과 함께 화면이 만들어지고 프로젝트 화면에 구현할 예정이다.
채팅을 MongoDB로 구현해보고 싶었지만 혼자 하는 프로젝트가 아니다 보니 멘토님의 의견을 우선순위로 삼았다.
먼저 SQL에 익숙해지라는 말씀을 참고하여 MySQL로 구현하였다.
프로젝트를 하며 더 공부하여 익숙해진 다음에는 다른 DB로 개인프로젝트에서 적용해보도록 하겠다.
참고 Git링크
https://github.com/dailycodebuffer/Spring-MVC-Tutorials/tree/master/spring-websocket
프로젝트 Git링크
'Project > Collabo Project' 카테고리의 다른 글
[Villion] 마이크로 서비스 간의 통신을 위해 Feign Client vs. Kafka 중 어떤 것을 사용할까? (0) | 2024.05.02 |
---|---|
[동물전쟁] 아이템 합성 서비스(Kafka 사용) (1) | 2023.11.09 |
[동물전쟁] Socket에 Token받아오기 (0) | 2023.10.22 |
직무별 채용 사이트 추천 서비스 (1) | 2023.09.06 |
신발 쇼핑몰 프로젝트 (0) | 2023.08.04 |