Skip to content
Merged
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
3 changes: 2 additions & 1 deletion springChatRoom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
<description>Demo project for Spring Boot</description>

<properties>
<java.version>11</java.version>
<!-- TODO : check if use JDK 11 or 17 -->
<java.version>17</java.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

// NOTE !!! DO NOT enable below, or will cause websocket connection error
//@ComponentScan(basePackages = "com.yen.springChatRoom.redis.RedisListenerBean") // https://blog.csdn.net/automal/article/details/111859409
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,51 @@
package com.yen.springChatRoom.bean;

public class ChatMessage {
private MessageType type;
private String content;
private String sender;

public MessageType getType() {
return type;
}

public void setType(MessageType type) {
this.type = type;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getSender() {
return sender;
}

public void setSender(String sender) {
this.sender = sender;
}

@Override
public String toString() {
return "ChatMessage{" +
"type=" + type +
", content='" + content + '\'' +
", sender='" + sender + '\'' +
'}';
}

public enum MessageType {
CHAT,
JOIN,
LEAVE
}

private MessageType type;
private String content;
private String sender;

public MessageType getType() {
return type;
}

public void setType(MessageType type) {
this.type = type;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getSender() {
return sender;
}

public void setSender(String sender) {
this.sender = sender;
}

@Override
public String toString() {
return "ChatMessage{"
+ "type="
+ type
+ ", content='"
+ content
+ '\''
+ ", sender='"
+ sender
+ '\''
+ '}';
}

public enum MessageType {
CHAT,
JOIN,
LEAVE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@ToString
public class Message {

private String sender;
private String content;
private String type;
private String sender;
private String content;
private String type;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@

public class OnlineUser {

private List<String> users;
private List<String> users;

public OnlineUser() {
}
public OnlineUser() {}

public OnlineUser(List<String> users) {
this.users = users;
}
public OnlineUser(List<String> users) {
this.users = users;
}

public List<String> getUsers() {
return users;
}

public void setUsers(List<String> users) {
this.users = users;
}
public List<String> getUsers() {
return users;
}

public void setUsers(List<String> users) {
this.users = users;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,41 @@
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

/**
* Stomp : spring implement WebSocket via Stomp
* <p>
* - https://blog.csdn.net/qq_21294185/article/details/130657375
* - https://blog.csdn.net/u013749113/article/details/131455579
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {

//WebSocketMessageBrokerConfigurer.super.registerStompEndpoints(registry);
registry.addEndpoint("ws").withSockJS(); // if browser not support websocket, use SockJS instead
}


@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {

//WebSocketMessageBrokerConfigurer.super.configureMessageBroker(registry);
registry.setApplicationDestinationPrefixes("/app"); // (client -> server)
registry.enableSimpleBroker("/topic", "/private"); // (server -> client)
}

//
// @Bean
// public SimpMessagingTemplate messagingTemplate() {
//
// //return new SimpMessagingTemplate(/* your message broker relay, e.g., "/topic" */);
// return new SimpMessagingTemplate("/private/");
// }
private final String APP_PRIFIX = "/app";
private final String TOPIC_PRIFIX = "/topic";
private final String PRIVATE_PRIFIX = "/private";

private final String WS_PATH = "ws";

/**
* Stomp : spring implement WebSocket via Stomp
*
* <p>- https://blog.csdn.net/qq_21294185/article/details/130657375 -
* https://blog.csdn.net/u013749113/article/details/131455579
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {

// WebSocketMessageBrokerConfigurer.super.registerStompEndpoints(registry);
registry
.addEndpoint(WS_PATH)
.withSockJS(); // if browser not support websocket, use SockJS instead
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {

// WebSocketMessageBrokerConfigurer.super.configureMessageBroker(registry);
registry.setApplicationDestinationPrefixes(APP_PRIFIX); // (client -> server)
registry.enableSimpleBroker(TOPIC_PRIFIX, PRIVATE_PRIFIX); // (server -> client)
}

//
// @Bean
// public SimpMessagingTemplate messagingTemplate() {
//
// //return new SimpMessagingTemplate(/* your message broker relay, e.g., "/topic" */);
// return new SimpMessagingTemplate("/private/");
// }

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.yen.springChatRoom.controller;

import com.yen.springChatRoom.bean.Message;
import com.yen.springChatRoom.bean.ChatMessage;
import com.yen.springChatRoom.bean.Message;
import com.yen.springChatRoom.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -10,97 +10,79 @@
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;

@Slf4j
@Controller
public class ChatController {

@Value("${redis.channel.msgToAll}")
private String msgToAll;

@Value("${redis.set.onlineUsers}")
private String onlineUsers;

@Value("${redis.channel.userStatus}")
private String userStatus;

@Value("${redis.channel.private}")
private String privateChannel;

final String onlineUserKey = "websocket.onlineUsers";

// TODO : check difference ? RedisTemplate VS RedisTemplate<String, String>
@Autowired
private RedisTemplate<String, String> redisTemplate;
//private RedisTemplate redisTemplate;

@Autowired
private SimpMessagingTemplate simpMessagingTemplate;

private static final Logger LOGGER = LoggerFactory.getLogger(ChatController.class);

/**
* single mode : read msg from FE, and send to
* other users (@SendTo("/topic/public")) directly
*/
// @MessageMapping("/chat.sendMessage")
// @SendTo("/topic/public")
// public ChatMessage sendMessage(@Payload ChatMessage chatMessage){
//
// return chatMessage;
// }

/**
* cluster mode : read msg from FE, but NOT send to other users,
* instead, send to Redis channel, so the other service
* on cluster can read/digest the msg
*/
@MessageMapping("/chat.sendMessage")
public void sendMessage(@Payload ChatMessage chatMessage){
try{

// test : save msg to redis
redisTemplate.opsForSet().add(msgToAll, JsonUtil.parseObjToJson(chatMessage));
//redisTemplate.convertAndSend(msgToAll, JsonUtil.parseObjToJson(chatMessage)));
redisTemplate.convertAndSend(msgToAll, JsonUtil.parseObjToJson(chatMessage));
}catch (Exception e){
LOGGER.error("send msg error : " + e.getMessage(), e);
}
}

@MessageMapping("/chat.addUser")
public void addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {

LOGGER.info("User added in Chatroom:" + chatMessage.getSender());
try {
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
redisTemplate.opsForSet().add(onlineUsers, chatMessage.getSender());
redisTemplate.convertAndSend(userStatus, JsonUtil.parseObjToJson(chatMessage));

// show online user

} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
final String onlineUserKey = "websocket.onlineUsers";
private final String USER_NAME = "username";
@Value("${redis.channel.msgToAll}")
private String msgToAll;
@Value("${redis.set.onlineUsers}")
private String onlineUsers;
@Value("${redis.channel.userStatus}")
private String userStatus;
@Value("${redis.channel.private}")
private String privateChannel;
// private RedisTemplate redisTemplate;
// TODO : check difference ? RedisTemplate VS RedisTemplate<String, String>
@Autowired private RedisTemplate<String, String> redisTemplate;
@Autowired private SimpMessagingTemplate simpMessagingTemplate;

/** single mode : read msg from FE, and send to other users (@SendTo("/topic/public")) directly */
// @MessageMapping("/chat.sendMessage")
// @SendTo("/topic/public")
// public ChatMessage sendMessage(@Payload ChatMessage chatMessage){
//
// return chatMessage;
// }

/**
* cluster mode : read msg from FE, but NOT send to other users, instead, send to Redis channel,
* so the other service on cluster can read/digest the msg
*/
@MessageMapping("/chat.sendMessage")
public void sendMessage(@Payload ChatMessage chatMessage) {
try {

// test : save msg to redis
redisTemplate.opsForSet().add(msgToAll, JsonUtil.parseObjToJson(chatMessage));
// redisTemplate.convertAndSend(msgToAll, JsonUtil.parseObjToJson(chatMessage)));
redisTemplate.convertAndSend(msgToAll, JsonUtil.parseObjToJson(chatMessage));
} catch (Exception e) {
log.error("send msg error : " + e.getMessage(), e);
}

// TODO : check @DestinationVariable ?
@RequestMapping("/app/private/{username}")
public void handlePrivateMessage(@DestinationVariable String username, Message message){

log.info("handlePrivateMessage : username = " + username + " message = " + message);
// save to redis

// redisTemplate.convertAndSend(userStatus, JsonUtil.parseObjToJson(chatMessage));
redisTemplate.opsForSet().add(privateChannel + "." + username, JsonUtil.parseObjToJson(message));

simpMessagingTemplate.convertAndSendToUser(username, "/topic/private", message);
}

@MessageMapping("/chat.addUser")
public void addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {

log.info("User added in Chatroom:" + chatMessage.getSender());
try {
headerAccessor.getSessionAttributes().put(USER_NAME, chatMessage.getSender());
redisTemplate.opsForSet().add(onlineUsers, chatMessage.getSender());
redisTemplate.convertAndSend(userStatus, JsonUtil.parseObjToJson(chatMessage));
// TODO : show online user
} catch (Exception e) {
log.error(e.getMessage(), e);
}

}

// TODO : check @DestinationVariable ?
@RequestMapping("/app/private/{username}")
public void handlePrivateMessage(@DestinationVariable String username, Message message) {

log.info("handlePrivateMessage : username = " + username + " message = " + message);
// save to redis
// redisTemplate.convertAndSend(userStatus, JsonUtil.parseObjToJson(chatMessage));
redisTemplate
.opsForSet()
.add(privateChannel + "." + username, JsonUtil.parseObjToJson(message));
simpMessagingTemplate.convertAndSendToUser(username, "/topic/private", message);
}
}
Loading
Loading