pub / sub
๊ตฌ์กฐ๋ก ๋์ด์์ด ๋ฉ์ธ์ง๋ฅผ ์ ์กํ๊ณ ๋ฉ์ธ์ง๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๋ ๋ถ๋ถ์ด ํ์คํจpub / sub (๋ฐํ ๋ฐ ๊ตฌ๋
)
: ๋ฉ์ธ์ง๋ฅผ ๋ฐํํ๋ ์ฃผ์ฒด์ ์๋นํ๋ ์ฃผ์ฒด๋ฅผ ๋ถ๋ฆฌํ์ฌ ์ ๊ณตํ๋ ๋ฉ์ธ์ง ๋ฐฉ๋ฒWebsocket ์์์ ๋์ํ๋ ํ๋กํ ์ฝ๋ก์จ ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ ์กํ ๋ฉ์ธ์ง์ ์ ํ, ํ์, ๋ด์ฉ์ ์ ์ํ๋ ๋ฉ์ปค๋์ฆ.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:sockjs-client:1.1.2'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// websocket์ ์ ์ํ๊ธฐ ์ํ endpoint ์ค์ , ๋๋ฉ์ธ์ด ๋ค๋ฅธ ์๋ฒ์์๋ ์ ์ ๊ฐ๋ฅํ๋๋ก CORS ์ค์
registry.addEndpoint("/ws-stomp") // WebSocket ๋๋ SockJS Client๊ฐ ์น์์ผ ํธ๋์
ฐ์ดํฌ ์ปค๋ฅ์
์ ์์ฑํ ๊ฒฝ๋ก
.setAllowedOriginPatterns("*")
.withSockJS(); // sockJS ๋ฑ๋ก
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub"); //client์์ SEND ์์ฒญ ์ฒ๋ฆฌ (์์ )
registry.enableSimpleBroker("/sub"); //ํด๋น ๊ฒฝ๋ก๋ก SimpleBroker ๋ฑ๋ก, SimpleBroker๋ ํด๋น ๊ฒฝ๋ก๋ฅผ ๊ตฌ๋
ํ๋ client์๊ฒ ๋ฉ์์ง๋ฅผ ์ ๋ฌ (๋ฐ์ )
}
.
.
.
}
@Entity
@ApiModel(value = "๊ฐ์ธ ์ฑํ
๋ฃธ ์ ๋ณด", description = "๊ฐ์ธ ์ฑํ
๋ฃธ ์ ๋ณด๋ฅผ ๋ํ๋ธ๋ค.")
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "CHAT_ROOM_NO")
@ApiModelProperty(value = "๊ฐ์ธ ์ฑํ
๋ฐฉ ๋ฒํธ")
private Integer no;
@ManyToOne(fetch = LAZY)
@JoinColumn(name="PUBLISHER_NO")
@ApiModelProperty(value = "๋ณด๋ด๋ ์ฌ๋")
private User publisher;
@ManyToOne(fetch = LAZY)
@JoinColumn(name="SUBSCRIBER_NO")
@ApiModelProperty(value = "๋ฐ๋ ์ฌ๋")
private User subscriber;
}
@Entity
@ApiModel(value = "์ฑํ
๋ฉ์ธ์ง ์ ๋ณด", description = "์ฑํ
๋ฉ์ธ์ง ์ ๋ณด๋ฅผ ๋ํ๋ธ๋ค.")
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "CHAT_MESSAGE_NO")
@ApiModelProperty(value = "์ฑํ
๋ฉ์ธ์ง ๋ฒํธ")
private Integer no;
@ManyToOne(fetch = LAZY)
@JoinColumn(name="PROJECT_ROOM_NO")
@ApiModelProperty(value = "์ฑํ
๋ฐฉ ๋ฒํธ")
private ChatRoom chatRoom;
@ManyToOne(fetch = LAZY)
@JoinColumn(name="USER_NO")
@ApiModelProperty(value = "์ ์ ๋ฒํธ")
private User user;
@ApiModelProperty(value = "์ฑํ
๋ด์ฉ")
private String content;
@ApiModelProperty(value = "์ ์ก ์๊ฐ")
private LocalDateTime time;
}
package com.ssafy.proma.model.entity.user;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Getter;
import javax.persistence.*;
@Getter
@Entity
@Builder
@NoArgsConstructor
@ApiModel(value = "ํ์์ ๋ณด", description = "ํ์์ ์์ธ ์ ๋ณด๋ฅผ ๋ํ๋ธ๋ค.")
public class User {
@Id
@Column(name = "USER_NO", nullable = false, length = 15)
@ApiModelProperty(value = "ํ์ ๋ฒํธ")
private String no;
@Column(length = 15)
@ApiModelProperty(value = "ํ์ ๋๋ค์")
private String nickname;
.
.
.
}
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatRoomController {
private final ChatService chatService;
@ApiOperation(value = "๊ฐ์ธ ์ฑํ
์์ฑ ๋ฐ ์กฐํ", notes = "ํด๋น ์ ์ ์ ๊ฐ์ธ ์ฑํ
๋ฐฉ ์์ฑ ๋ฐ ์กฐํ")
@GetMapping("/room/user/{subNo}")
public ResponseEntity<Map<String, Object>> getChatRoom(@PathVariable String subNo
, @RequestParam(required = false) Integer lastMsgNo) {
Map<String, Object> result = new HashMap<>();
HttpStatus status = HttpStatus.ACCEPTED;
try{
result = chatService.getChatRoom(subNo, lastMsgNo);
if(result.get("message").equals(CHATROOM_SUCCESS_MESSAGE)) {
status = HttpStatus.OK;
}
} catch (Exception e) {
result.put("message", CHATROOM_ERROR_MESSAGE);
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
return new ResponseEntity<>(result, status);
}
}
@Controller
@RequiredArgsConstructor
public class ChatMessageController {
private final SimpMessageSendingOperations messagingTemplate;
private final ChatService chatService;
@ApiOperation(value = "์ฑํ
๋ฉ์ธ์ง ์ ์ก", notes = "ํด๋น ์ฑํ
๋ฐฉ์ผ๋ก ๋ฉ์ธ์ง ์ ์ก")
@MessageMapping("/chat/msg")
public void privateMessage(@RequestBody ChatMessageReq message) {
ChatMessageRes response = chatService.savePrivateMessage(message);
messagingTemplate.convertAndSend("/sub/chat/room/user/" + response.getRoomNo(), response);
}
}
@Service
@RequiredArgsConstructor
public class ChatService {
private final UserRepository userRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
public ChatMessageRes savePrivateMessage(ChatMessageReq request) {
chatRoom chatRoom = findChatRoom(request.getRoomNo());
User user = findUser(request.getPubNo());
String content = request.getContent();
LocalDateTime time = LocalDateTime.now();
ChatMessageRes response = new ChatMessageRes(chatRoom.getNo(), user.getNo(),
user.getNickname(), content, time);
ChatMessage chatMessage = ChatMessageDto.toPrivateMsgEntity(chatRoom, user, content, time);
chatMessageRepository.save(chatMessage);
System.out.println("์ฑํ
์ ์ฅ์๋ฃ.");
return response;
}
}
<div class="container" id="app" v-cloak>
<div>
<h2>์ฑํ
๋ฐฉ</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="content" 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.nickname}} - {{message.content}} {{message.time}}</a>
</li>
</ul>
<div></div>
</div>
<script>
var sock = new SockJS("http://localhost:8080/ws-stomp");
var ws = Stomp.over(sock);
var reconnect = 0;
// vue.js
var vm = new Vue({
el: '#app',
data: {
roomNo: '',
pubNo: '',
content: '',
messages: []
},
created() {
this.roomNo = "${roomNo}";
this.pubNo = "${userNo}";
},
methods: {
sendMessage: function() {
ws.send("/pub/chat/msg", {"JWT":""}, JSON.stringify({roomNo:this.roomNo, pubNo:this.pubNo, content:this.content}));
this.message = '';
},
recvMessage: function(recv) {
this.messages.unshift({"nickname":recv.nickname,"content":recv.content, "time":recv.time})
}
}
});
function connect() {
// pub/sub event
ws.connect({"JWT":""}, function(frame) {
ws.subscribe("/sub/chat/room/user/"+vm.$data.roomNo, function(message) {
var recv = JSON.parse(message.body);
vm.recvMessage(recv);
});
}, function(error) {
if(reconnect++ <= 5) {
setTimeout(function() {
console.log("connection reconnect");
sock = new SockJS("http://localhost:8080/ws-stomp");
ws = Stomp.over(sock);
connect();
},10*1000);
}
});
}
connect();
</script>