diff --git a/springChatRoom/README.md b/springChatRoom/README.md index 9ce895bfc..aeee9eb09 100644 --- a/springChatRoom/README.md +++ b/springChatRoom/README.md @@ -53,12 +53,12 @@ java -jar target/springChatRoom-0.0.1-SNAPSHOT.jar ## Todo - Feature + - show user(online/offline) list - private msg (1 to 1) - setup group, group chat - @ "notification" - msg push (?) - "read" 已讀 feature - - show user list - save history msg - broadcast msg - push msg to users (offer API) @@ -67,16 +67,21 @@ java -jar target/springChatRoom-0.0.1-SNAPSHOT.jar ## Knowledge - WebSocket - - Server can send info to client (bi-directon communicartion, TCP protocol) - - 3 ways handshake - - server, client side can send/receive msg, and terminate connection - - long connection between server and client, no need to check with client every time - - more efficient when communicating (header is shorter) + - Server can send info to client (bi-directon communicartion, TCP protocol) + - 3 ways handshake + - server, client side can send/receive msg, and terminate connection + - long connection between server and client, no need to check with client every time + - more efficient when communicating (header is shorter) - Use case of WebSocket - - chat room - - online game - - geo location app - - stock market app - - co-editing system (e.g. google doc ?) - - app needs near real-time update/sync, data transmit is not small \ No newline at end of file + - chat room + - online game + - geo location app + - stock market app + - co-editing system (e.g. google doc ?) + - app needs near real-time update/sync, data transmit is not small + +## Reference + +- redisTemplate API cmd + - https://ost.51cto.com/posts/2333 \ No newline at end of file diff --git a/springChatRoom/doc/ref.md b/springChatRoom/doc/ref.md index 78584b470..190605fce 100644 --- a/springChatRoom/doc/ref.md +++ b/springChatRoom/doc/ref.md @@ -1,21 +1,29 @@ ## Ref - Article - - https://blog.csdn.net/qqxx6661/article/details/98883166 - - https://github.com/yennanliu/springboot-websocket-demo + - https://blog.csdn.net/qqxx6661/article/details/98883166 + - https://github.com/yennanliu/springboot-websocket-demo - - https://ithelp.ithome.com.tw/articles/10197142 - - https://ithelp.ithome.com.tw/articles/10197191 - - https://www.baeldung.com/websockets-spring - - https://blog.csdn.net/wwd0501/article/details/54582912 - - https://www.slideshare.net/wenhsiaoyi/java-api-for-websocket - - https://www.syscom.com.tw/ePaper_New_Content.aspx?id=368&EPID=194&TableName=sgEPArticle - - https://tw511.com/a/01/16646.html + - https://ithelp.ithome.com.tw/articles/10197142 + - https://ithelp.ithome.com.tw/articles/10197191 + - https://www.baeldung.com/websockets-spring + - https://blog.csdn.net/wwd0501/article/details/54582912 + - https://www.slideshare.net/wenhsiaoyi/java-api-for-websocket + - https://www.syscom.com.tw/ePaper_New_Content.aspx?id=368&EPID=194&TableName=sgEPArticle + - https://tw511.com/a/01/16646.html - Code - - https://spring.io/guides/gs/messaging-stomp-websocket/ - - https://morosedog.gitlab.io/springboot-20190416-springboot28/ + - https://spring.io/guides/gs/messaging-stomp-websocket/ + - https://morosedog.gitlab.io/springboot-20190416-springboot28/ - Video - - https://youtu.be/r4fdPmZuzmY?si=qrmRTm09Bo53Ina1 - - https://youtu.be/RbCFeeePJoM?si=DF6mES5XApvSkhXw - - https://youtu.be/es_fTKyfI4w?si=vVhEz6Us-zrcerTR \ No newline at end of file + - https://youtu.be/r4fdPmZuzmY?si=qrmRTm09Bo53Ina1 + - https://youtu.be/RbCFeeePJoM?si=DF6mES5XApvSkhXw + - https://youtu.be/es_fTKyfI4w?si=vVhEz6Us-zrcerTR + +- Advanced + - Chat with specific user + - https://www.baeldung.com/spring-websockets-send-message-to-user + - https://github.com/eugenp/tutorials/tree/master/spring-security-modules/spring-security-web-sockets - code + - https://juejin.cn/post/6844903717636947981 + + - https://www.jb51.net/article/257091.htm \ No newline at end of file diff --git a/springChatRoom/pom.xml b/springChatRoom/pom.xml index 1d351b556..33c78d1c6 100644 --- a/springChatRoom/pom.xml +++ b/springChatRoom/pom.xml @@ -51,7 +51,13 @@ 1.2.79 - + + junit + junit + test + + + diff --git a/springChatRoom/src/main/java/com/yen/springChatRoom/bean/OnlineUser.java b/springChatRoom/src/main/java/com/yen/springChatRoom/bean/OnlineUser.java new file mode 100644 index 000000000..f64dcda35 --- /dev/null +++ b/springChatRoom/src/main/java/com/yen/springChatRoom/bean/OnlineUser.java @@ -0,0 +1,26 @@ +package com.yen.springChatRoom.bean; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class OnlineUser { + + public OnlineUser(){ + } + + public OnlineUser(List users){ + this.users = users; + } + + private List users; + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + +} diff --git a/springChatRoom/src/main/java/com/yen/springChatRoom/controller/ChatController.java b/springChatRoom/src/main/java/com/yen/springChatRoom/controller/ChatController.java index c9a86a13f..27a94ffce 100644 --- a/springChatRoom/src/main/java/com/yen/springChatRoom/controller/ChatController.java +++ b/springChatRoom/src/main/java/com/yen/springChatRoom/controller/ChatController.java @@ -12,6 +12,9 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.Set; @Controller public class ChatController { @@ -25,6 +28,8 @@ public class ChatController { @Value("${redis.channel.userStatus}") private String userStatus; + final String onlineUserKey = "websocket.onlineUsers"; + // TODO : check difference ? RedisTemplate VS RedisTemplate @Autowired private RedisTemplate redisTemplate; @@ -58,7 +63,6 @@ public void sendMessage(@Payload ChatMessage chatMessage){ } } - @MessageMapping("/chat.addUser") public void addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { @@ -67,28 +71,12 @@ public void addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor 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); } } -// @MessageMapping("/chat.addUser") -// @SendTo("/topic/public") -// public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { -// -// LOGGER.info("User added in Chatroom:" + chatMessage.getSender()); -// // add username in web socket session -// headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); -// return chatMessage; -// -// // TODO : update with below -//// try { -//// headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); -////// redisTemplate.opsForSet().add(onlineUsers, chatMessage.getSender()); -////// redisTemplate.convertAndSend(userStatus, JsonUtil.parseObjToJson(chatMessage)); -//// } catch (Exception e) { -//// LOGGER.error(e.getMessage(), e); -//// } -// } - } diff --git a/springChatRoom/src/main/java/com/yen/springChatRoom/controller/UserController.java b/springChatRoom/src/main/java/com/yen/springChatRoom/controller/UserController.java new file mode 100644 index 000000000..dc2fa3864 --- /dev/null +++ b/springChatRoom/src/main/java/com/yen/springChatRoom/controller/UserController.java @@ -0,0 +1,53 @@ +package com.yen.springChatRoom.controller; + +import com.yen.springChatRoom.bean.OnlineUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Value("${redis.channel.msgToAll}") + private String msgToAll; + + @Value("${redis.set.onlineUsers}") + private String onlineUsers; + + @Value("${redis.channel.userStatus}") + private String userStatus; + + final String onlineUserKey = "websocket.onlineUsers"; + + @Autowired + private RedisTemplate redisTemplate; + + private static final Logger LOGGER = LoggerFactory.getLogger(ChatController.class); + + @GetMapping("/online_user") + public List getOnlineUser(){ + + Set resultSet = redisTemplate.opsForSet().members(onlineUserKey); + System.out.println("(getOnlineUser) resultSet = " + resultSet); + // TODO : optimize below + OnlineUser onlineUser = new OnlineUser(); + List users = new ArrayList<>(); + resultSet.forEach(x -> { + users.add(x); + }); + onlineUser.setUsers(users); + return onlineUser.getUsers(); + } + +} diff --git a/springChatRoom/src/main/java/com/yen/springChatRoom/listener/WebSocketEventListener.java b/springChatRoom/src/main/java/com/yen/springChatRoom/listener/WebSocketEventListener.java index 47dfee335..72b714d91 100644 --- a/springChatRoom/src/main/java/com/yen/springChatRoom/listener/WebSocketEventListener.java +++ b/springChatRoom/src/main/java/com/yen/springChatRoom/listener/WebSocketEventListener.java @@ -79,21 +79,4 @@ public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { } } -// @EventListener -// public void handleWebSocketDisConnectListener(SessionDisconnectEvent event){ -// -// StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); -// String username = (String) headerAccessor.getSessionAttributes().get("username"); -// -// if (username != null){ -// -// LOGGER.info("User disconnected : " + username); -// ChatMessage chatMessage = new ChatMessage(); -// chatMessage.setType(ChatMessage.MessageType.LEAVE); -// chatMessage.setSender(username); -// -// messagingTemplate.convertAndSend("/topic/public", chatMessage); -// } -// } - } diff --git a/springChatRoom/src/main/java/com/yen/springChatRoom/redis/RedisListenerHandle.java b/springChatRoom/src/main/java/com/yen/springChatRoom/redis/RedisListenerHandle.java index ec1e6bdda..266f8f5ae 100644 --- a/springChatRoom/src/main/java/com/yen/springChatRoom/redis/RedisListenerHandle.java +++ b/springChatRoom/src/main/java/com/yen/springChatRoom/redis/RedisListenerHandle.java @@ -49,7 +49,6 @@ public void onMessage(Message message, byte[] bytes) { return; } - if (msgToAll.equals(topic)) { LOGGER.info("Send message to all users:" + rawMsg); ChatMessage chatMessage = JsonUtil.parseJsonToObj(rawMsg, ChatMessage.class); diff --git a/springChatRoom/src/main/resources/static/index.html b/springChatRoom/src/main/resources/static/index.html index e2974a07e..74dc8d61e 100644 --- a/springChatRoom/src/main/resources/static/index.html +++ b/springChatRoom/src/main/resources/static/index.html @@ -50,6 +50,12 @@

Spring WebSocket Chat Demo

+ +
+

Online Users

+
    +
    + diff --git a/springChatRoom/src/main/resources/static/js/main.js b/springChatRoom/src/main/resources/static/js/main.js index a6234e8aa..f995f4201 100644 --- a/springChatRoom/src/main/resources/static/js/main.js +++ b/springChatRoom/src/main/resources/static/js/main.js @@ -119,5 +119,34 @@ function getAvatarColor(messageSender) { return colors[index]; } +// online user +function fetchUserList() { + fetch('/user/online_user') // Replace with the actual endpoint URL + .then(response => response.json()) + .then(data => { + updateOnlineUsers(data); + }) + .catch(error => { + console.error('Error fetching user list: ', error); + }); +} + +function updateOnlineUsers(users) { + const userList = document.getElementById('userList'); + userList.innerHTML = ''; // Clear the list first + + users.forEach(user => { + const listItem = document.createElement('li'); + listItem.textContent = user; + userList.appendChild(listItem); + }); +} + +// Call the fetchUserList function to initially populate the user list +fetchUserList(); + +// You can also periodically update the user list using a timer or other events +setInterval(fetchUserList, 5000); // Update every 5 seconds (adjust as needed) + usernameForm.addEventListener('submit', connect, true) messageForm.addEventListener('submit', sendMessage, true) \ No newline at end of file diff --git a/springChatRoom/src/test/java/com/yen/springChatRoom/controller/ChatControllerTest.java b/springChatRoom/src/test/java/com/yen/springChatRoom/controller/ChatControllerTest.java new file mode 100644 index 000000000..cf8120efa --- /dev/null +++ b/springChatRoom/src/test/java/com/yen/springChatRoom/controller/ChatControllerTest.java @@ -0,0 +1,32 @@ +package com.yen.springChatRoom.controller; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +class ChatControllerTest { + + @Autowired + private RedisTemplate redisTemplate; + + @Test + public void testQueryRedis(){ + + final String onlineUserKey = "websocket.onlineUsers"; + // https://ost.51cto.com/posts/2333 + Set resultSet = redisTemplate.opsForSet().members(onlineUserKey); + System.out.println("resultSet:" + resultSet); + + } + +} \ No newline at end of file