Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ dependencies {

//chat
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// 외부 브로커를 사용하기 위해

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-reactor-netty', version: '2.4.6'
//jackson2json에서 LocalDateTime을 handling 하기 위해
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.12.4'

implementation 'org.springframework.boot:spring-boot-starter-amqp'
testImplementation 'org.springframework.amqp:spring-rabbit-test'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/com/cluting/clutingbackend/chat/config/RabbitConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.cluting.clutingbackend.chat.config;

import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;

import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableRabbit
public class RabbitConfig {

@Value("${spring.rabbitmq.host}")
private String rabbitHost;

@Value("${spring.rabbitmq.username}")
private String rabbitUsername;

@Value("${spring.rabbitmq.password}")
private String rabbitPassword;

@Value("${spring.rabbitmq.port}")
private Integer rabbitPort;

private static final String CHAT_QUEUE_NAME = "chat.queue";
private static final String CHAT_EXCHANGE_NAME = "chat.exchange";
private static final String ROUTING_KEY = "*.room.*";

// Queue 등록
@Bean
public Queue queue(){
return new Queue(CHAT_QUEUE_NAME,true);
}

@Bean
public TopicExchange exchange(){
return new TopicExchange(CHAT_EXCHANGE_NAME);
}

@Bean
public Binding binding(Queue queue, TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
}

@Bean
public RabbitTemplate rabbitTemplate(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setMessageConverter(jsonMessageConverter());
rabbitTemplate.setRoutingKey(CHAT_QUEUE_NAME);
return rabbitTemplate;
}

@Bean
public SimpleMessageListenerContainer container(){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
container.setQueueNames(CHAT_QUEUE_NAME);
// container.setMessageListener(null);
container.setMessageListener(message -> {
String body = new String(message.getBody());
System.out.println("Received message: " + body);
// 메시지 처리 로직 추가
});
return container;
}

//Spring에서 자동생성해주는 ConnectionFactory는 SimpleConnectionFactory인가? 그건데
//여기서 사용하는 건 CachingConnectionFacotry라 새로 등록해줌
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost(rabbitHost);
factory.setPort(rabbitPort);
factory.setUsername(rabbitUsername);
factory.setPassword(rabbitPassword);
return factory;
}

@Bean
public Jackson2JsonMessageConverter jsonMessageConverter(){
//LocalDateTime serializable을 위해
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
objectMapper.registerModule(dateTimeModule());

Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(objectMapper);

return converter;
}

@Bean
public Module dateTimeModule(){
return new JavaTimeModule();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.cluting.clutingbackend.chat.config;

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.ApplicationRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Configuration
public class RabbitMQConnectionChecker {

private static final Logger logger = LoggerFactory.getLogger(RabbitMQConnectionChecker.class);

public ApplicationRunner checkConnection(ConnectionFactory connectionFactory) {
return args -> {
try {
connectionFactory.createConnection().close();
logger.info("RabbitMQ connection successful.");
} catch (Exception e) {
logger.error("RabbitMQ connection failed: {}", e.getMessage());
throw new IllegalStateException("Failed to connect to RabbitMQ", e);
}
};
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.cluting.clutingbackend.chat.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Value("${spring.rabbitmq.host}")
private String rabbitHost;

@Value("${spring.rabbitmq.username}")
private String rabbitUsername;

@Value("${spring.rabbitmq.password}")
private String rabbitPassword;

@Value("${spring.rabbitmq.port}")
private Integer rabbitPort;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
// stomp 접속 주소 url -> ws://AWS EC2 ip주소/ws
registry.addEndpoint("/stomp/chat")
.setAllowedOrigins("http://localhost:8081")
.withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry){
// 메시지를 수신하는 요청 엔드포인트
// registry.enableSimpleBroker("/sub");
// 메시지를 송신하는 엔드포인트
// registry.setApplicationDestinationPrefixes("/pub");

registry.setPathMatcher(new AntPathMatcher(".")); // url을 chat/room/3 -> chat.room.3으로 참조하기 위한 설정
registry.setApplicationDestinationPrefixes("/pub");

registry.enableStompBrokerRelay("/queue","/topic","/exchange","amq/queue")
.setRelayHost(rabbitHost) // RabbitMQ의 IP 주소
.setRelayPort(61613) // STOMP 포트
.setClientLogin(rabbitUsername) // RabbitMQ 사용자 이름
.setClientPasscode(rabbitPassword) // RabbitMQ 비밀번호
.setSystemLogin(rabbitUsername)
.setSystemPasscode(rabbitPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//package com.cluting.clutingbackend.chat.controller;
//
//import com.cluting.clutingbackend.chat.dto.ChatMessageDto;
//import lombok.RequiredArgsConstructor;
//import org.springframework.messaging.handler.annotation.MessageMapping;
//import org.springframework.messaging.simp.SimpMessagingTemplate;
//import org.springframework.stereotype.Controller;
//
//@Controller
//@RequiredArgsConstructor
//public class ChatController {
//
// private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달
//
// //Client가 SEND할 수 있는 경로
// //stompConfig에서 설정한 applicationDestinationPrefixes와 @MessageMapping 경로가 병합됨
// //"/pub/chat/enter"
// @MessageMapping(value = "/chat/enter")
// public void enter(ChatMessageDto message){
// message.setMessage(message.getWriter() + "님이 채팅방에 참여하였습니다.");
// template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
// }
//
// @MessageMapping(value = "/chat/message")
// public void message(ChatMessageDto message){
// template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.cluting.clutingbackend.chat.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping(value = "/chat")
public class ChatRoomController {

@GetMapping("/rooms")
public String getRooms(){
return "chat/rooms";
}

@GetMapping(value = "/room")
public String getRoom(Long chatRoomId, String nickname, Model model){

model.addAttribute("chatRoomId", chatRoomId);
model.addAttribute("nickname", nickname);

return "chat/room";
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.cluting.clutingbackend.chat.controller;

import com.cluting.clutingbackend.chat.dto.ChatDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import java.time.LocalDateTime;

@Controller
@RequiredArgsConstructor
@Log4j2
public class StompRabbitController {

private final RabbitTemplate template;

private final static String CHAT_EXCHANGE_NAME = "chat.exchange";
private final static String CHAT_QUEUE_NAME = "chat.queue";

@MessageMapping("chat.enter.{chatRoomId}")
public void enter(ChatDto chat, @DestinationVariable String chatRoomId){

chat.setMessage("입장하셨습니다.");
chat.setRegDate(LocalDateTime.now());

template.convertAndSend(CHAT_EXCHANGE_NAME, "room." + chatRoomId, chat); // exchange
//template.convertAndSend("room." + chatRoomId, chat); //queue
//template.convertAndSend("amq.topic", "room." + chatRoomId, chat); //topic
}

@GetMapping("/send")
public String sendMessage() {
ChatDto dto = new ChatDto(1L,1L,1L,"HI TEST","SEOUL",LocalDateTime.now());
template.convertAndSend(CHAT_EXCHANGE_NAME, "room." + 1, dto);
return "Message sent!";
}

@MessageMapping("chat.message.{chatRoomId}")
public void send(ChatDto chat, @DestinationVariable String chatRoomId){

chat.setRegDate(LocalDateTime.now());

template.convertAndSend(CHAT_EXCHANGE_NAME, "room." + chatRoomId, chat);
//template.convertAndSend( "room." + chatRoomId, chat);
//template.convertAndSend("amq.topic", "room." + chatRoomId, chat);
}

//receive()는 단순히 큐에 들어온 메세지를 소비만 한다. (현재는 디버그용도)
@RabbitListener(queues = CHAT_QUEUE_NAME)
public void receive(ChatDto chat){

System.out.println("received : " + chat.getMessage());
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/cluting/clutingbackend/chat/dto/ChatDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.cluting.clutingbackend.chat.dto;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.*;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class ChatDto {
private Long id;
private Long chatRoomId;
private Long memberId;

private String message;
private String region;

@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime regDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.cluting.clutingbackend.chat.dto;

import lombok.Data;

@Data
public class ChatMessageDto {

private String roomId;
private String writer;
private String message;
}
25 changes: 25 additions & 0 deletions src/main/java/com/cluting/clutingbackend/chat/dto/ChatRoomDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.cluting.clutingbackend.chat.dto;

import lombok.Data;
import org.springframework.web.socket.WebSocketSession;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Data
public class ChatRoomDto {

private String roomId;
private String name;
private Set<WebSocketSession> sessions = new HashSet<>();
//WebSocketSession은 Spring에서 Websocket Connection이 맺어진 세션

public static ChatRoomDto create(String name){
ChatRoomDto room = new ChatRoomDto();

room.roomId = UUID.randomUUID().toString();
room.name = name;
return room;
}
}
Loading