implementation 'org.springframework.boot:spring-boot-starter-freemarker'
implementation 'org.springframework.boot:spring-boot-devtools'
// 채팅 웹 화면 구현 js
implementation 'org.webjars.bower:bootstrap:4.3.1'
implementation 'org.webjars.bower:vue:2.5.16'
implementation 'org.webjars.bower:axios:0.17.1'
// sockjs
implementation 'org.webjars:sockjs-client:1.5.1'
// stomp
implementation 'org.webjars:stomp-websocket:2.3.4'
// gson
implementation 'com.google.code.gson:gson:2.9.0'
spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=false
spring.freemarker.cache=false
Stomp를 사용
**@EnableWebSocketMessageBroker
** 선언**WebSocketMessageBrokerConfigurer
** 상속pub/ sub 구현
Stomp websocket 의 endpoint → /ws-stomp
- 접속 주소 : ws://localhost:8080/ws-stomp
@Slf4j(topic = "WebSocketChatHandler")
@Component
public class WebSocketChatHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload {}", payload);
TextMessage textMessage = new TextMessage("입장하셨습니다.");
session.sendMessage(textMessage);
}
}
ChatService → ChatRoomRepository
@Repository
public class ChatRoomRepository {
private Map<String, ChatRoom> chatRoomMap;
@PostConstruct
private void init() {
chatRoomMap = new LinkedHashMap<>();
}
public List<ChatRoom> findAllRoom() {
// 채팅방 생성 순서 최신 순으로 반환
List<ChatRoom> chatRooms = new ArrayList<>(chatRoomMap.values());
Collections.reverse(chatRooms);
return chatRooms;
}
public ChatRoom findRoomById(String id) {
return chatRoomMap.get(id);
}
public ChatRoom creaetChatRoom(String name) {
ChatRoom chatRoom = ChatRoom.create(name);
chatRoomMap.put(chatRoom.getRoomId(), chatRoom);
return chatRoom;
}
}
**@MessageMapping
** 을 통해 Websocket으로 들어오는 메세지 발행 처리WebSocketChatHandler → ChatController
/*
publisher 구현
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
@Slf4j(topic = "채팅방 생성/ 조회")
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate;
/*
@MessageMapping -> websocket 으로 들어오는 메세지 발행 처리
*/
@MessageMapping("/chat/message")
public void message(ChatMessage message) {
// 1. 클라이언트 - prefix 붙여 "/pub/chat/message"로 발행 요청
// 입장 메세지일 경우
if(ChatMessage.MessageType.ENTER.equals(message.getType())){
message.setMessage(message.getSender() + "님이 입장하셨습니다.");
}
// 2. "/sub/chat/room/{roomId}"로 메세지 발송
// 클라이언트 : "/sub/chat/room/{roomId}" 를 구독하고 있다가 메세지가 전달되면 화면에 출력
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
@RequiredArgsConstructor
@Controller
@RequestMapping("/chat")
public class ChatRoomController {
private final ChatRoomRepository chatRoomRepository;
// 채팅 리스트 화면
@GetMapping("/room")
public String rooms(Model model) {
return "room";
}
// 모든 채팅방 목록 반환
@GetMapping("/rooms")
@ResponseBody
public List<ChatRoom> room() {
return chatRoomRepository.findAllRoom();
}
// 채팅방 생성
@PostMapping("/room")
@ResponseBody
public ChatRoom createRoom(@RequestParam String name) {
return chatRoomRepository.createChatRoom(name);
}
// 채팅방 입장 화면
@GetMapping("/room/enter/{roomId}")
public String roomDetail(Model model, @PathVariable String roomId) {
model.addAttribute("roomId", roomId);
return "roomdetail";
}
// 특정 채팅방 조회
@GetMapping("/room/{roomId}")
@ResponseBody
public ChatRoom roomInfo(@PathVariable String roomId) {
return chatRoomRepository.findRoomById(roomId);
}
}
<!doctype html>
<html lang="en">
<head>
<title>Websocket Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div class="row">
<div class="col-md-12">
<h3>채팅방 리스트</h3>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">방제목</label>
</div>
<input type="text" class="form-control" v-model="room_name" v-on:keyup.enter="createRoom">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="createRoom">채팅방 개설</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item list-group-item-action" v-for="item in chatrooms" v-bind:key="item.roomId" v-on:click="enterRoom(item.roomId)">
{{item.name}}
</li>
</ul>
</div>
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
room_name : '',
chatrooms: [
]
},
created() {
this.findAllRoom();
},
methods: {
findAllRoom: function() {
axios.get('/chat/rooms').then(response => { this.chatrooms = response.data; });
},
createRoom: function() {
if("" === this.room_name) {
alert("방 제목을 입력해 주십시요.");
return;
} else {
var params = new URLSearchParams();
params.append("name",this.room_name);
axios.post('/chat/room', params)
.then(
response => {
alert(response.data.name+"방 개설에 성공하였습니다.")
this.room_name = '';
this.findAllRoom();
}
)
.catch( response => { alert("채팅방 개설에 실패하였습니다."); } );
}
},
enterRoom: function(roomId) {
var sender = prompt('대화명을 입력해 주세요.');
if(sender != "") {
localStorage.setItem('wschat.sender',sender);
localStorage.setItem('wschat.roomId',roomId);
location.href="/chat/room/enter/"+roomId;
}
}
}
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<title>Websocket ChatRoom</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script src="/webjars/sockjs-client/1.5.1/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.4/stomp.min.js"></script>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div>
<h2>{{room.name}}</h2>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">내용</label>
</div>
<input type="text" class="form-control" v-model="message" v-on:keypress.enter="sendMessage">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="sendMessage">보내기</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item" v-for="message in messages">
{{message.sender}} - {{message.message}}</a>
</li>
</ul>
<div></div>
</div>
<script>
//alert(document.title);
// websocket & stomp initialize
var sock = new SockJS("/ws-stomp");
var ws = Stomp.over(sock);
var reconnect = 0;
// vue.js
var vm = new Vue({
el: '#app',
data: {
roomId: '',
room: {},
sender: '',
message: '',
messages: []
},
created() {
this.roomId = localStorage.getItem('wschat.roomId');
this.sender = localStorage.getItem('wschat.sender');
this.findRoom();
},
methods: {
findRoom: function() {
axios.get('/chat/room/'+this.roomId).then(response => { this.room = response.data; });
},
sendMessage: function() {
ws.send("/pub/chat/message", {}, JSON.stringify({type:'TALK', roomId:this.roomId, sender:this.sender, message:this.message}));
this.message = '';
},
recvMessage: function(recv) {
this.messages.unshift({"type":recv.type,"sender":recv.type=='ENTER'?'[알림]':recv.sender,"message":recv.message})
}
}
});
function connect() {
// pub/sub event
ws.connect({}, function(frame) {
ws.subscribe("/sub/chat/room/"+vm.$data.roomId, function(message) {
var recv = JSON.parse(message.body);
vm.recvMessage(recv);
});
ws.send("/pub/chat/message", {}, JSON.stringify({type:'ENTER', roomId:vm.$data.roomId, sender:vm.$data.sender}));
}, function(error) {
if(reconnect++ <= 5) {
setTimeout(function() {
console.log("connection reconnect");
sock = new SockJS("/ws-stomp");
ws = Stomp.over(sock);
connect();
},10*1000);
}
});
}
connect();
</script>
</body>
</html>
(참고 : https://www.daddyprogrammer.org/post/4691/spring-websocket-chatting-server-stomp-server/)