From 3c6119369d7e086970100166cf5ef01b50275f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C3=B3=C5=82grabia?= Date: Sun, 10 Sep 2017 12:22:15 +0200 Subject: [PATCH] Ping pong mechanism works. --- build.gradle | 3 + .../configs/WebsocketConfig.java | 4 ++ .../chat/websocketschat/dto/GlobalCtx.java | 12 ++++ .../chat/websocketschat/dto/UserCtx.java | 20 +++++++ .../handlers/ChatWebsocketHandler.java | 56 ++++++++++++++++--- .../handlers/NickCommandHandler.java | 10 +++- .../handlers/PingMessageHandler.java | 14 +++++ .../handlers/PongMessageHandler.java | 43 ++++++++++++++ .../handlers/UserCommandHandler.java | 14 ++++- src/main/resources/logback.xml | 18 ++++++ src/main/resources/static/IrcClient.js | 52 +++++++++++++++++ src/main/resources/static/index.html | 6 ++ src/main/resources/static/index.js | 39 ++++++++----- 13 files changed, 264 insertions(+), 27 deletions(-) create mode 100644 src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PingMessageHandler.java create mode 100644 src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PongMessageHandler.java create mode 100644 src/main/resources/logback.xml create mode 100644 src/main/resources/static/IrcClient.js diff --git a/build.gradle b/build.gradle index 73f5c56..85cf61e 100644 --- a/build.gradle +++ b/build.gradle @@ -27,5 +27,8 @@ dependencies { compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.boot:spring-boot-starter-websocket') compile('org.webjars:sockjs-client:0.3.4') + compile('org.webjars:bootstrap:3.3.7-1') + compile('org.webjars:jquery:3.2.1') + compile('org.webjars:bootbox:4.4.0') testCompile('org.springframework.boot:spring-boot-starter-test') } diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/configs/WebsocketConfig.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/configs/WebsocketConfig.java index 1d0be4e..23b960b 100644 --- a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/configs/WebsocketConfig.java +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/configs/WebsocketConfig.java @@ -1,6 +1,8 @@ package pl.polgrabiat.websockets.chat.websocketschat.configs; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @@ -10,6 +12,8 @@ import javax.inject.Inject; @Configuration @EnableWebSocket +@EnableScheduling +@EnableAsync public class WebsocketConfig implements WebSocketConfigurer { @Inject diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/GlobalCtx.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/GlobalCtx.java index 9474309..06d8636 100644 --- a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/GlobalCtx.java +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/GlobalCtx.java @@ -53,4 +53,16 @@ public class GlobalCtx { public void setUserNameSessions(Map userNameSessions) { this.userNameSessions = userNameSessions; } + + synchronized public void removeSession(WebSocketSession session) { + sessions.remove(session); + UserCtx userCtx = userSessions.get(session); + userSessions.remove(userSessions); + userNameSessions.remove(userCtx.getNick()); + } + + synchronized public void addSession(WebSocketSession session) { + sessions.add(session); + userSessions.put(session, new UserCtx(session)); + } } diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/UserCtx.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/UserCtx.java index 76346a1..e7d8873 100644 --- a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/UserCtx.java +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/dto/UserCtx.java @@ -2,11 +2,15 @@ package pl.polgrabiat.websockets.chat.websocketschat.dto; import org.springframework.web.socket.WebSocketSession; +import java.time.LocalDateTime; + public class UserCtx { private final WebSocketSession session; private String nick; private String userName; private String realName; + private int lastPongNumber; + private LocalDateTime lastPongTime; public UserCtx(WebSocketSession session) { this.session = session; @@ -39,4 +43,20 @@ public class UserCtx { public WebSocketSession getSession() { return session; } + + public int getLastPongNumber() { + return lastPongNumber; + } + + public void setLastPongNumber(int lastPongNumber) { + this.lastPongNumber = lastPongNumber; + } + + public LocalDateTime getLastPongTime() { + return lastPongTime; + } + + public void setLastPongTime(LocalDateTime lastPongTime) { + this.lastPongTime = lastPongTime; + } } diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/ChatWebsocketHandler.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/ChatWebsocketHandler.java index 0bcdadd..594600e 100644 --- a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/ChatWebsocketHandler.java +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/ChatWebsocketHandler.java @@ -2,27 +2,30 @@ package pl.polgrabiat.websockets.chat.websocketschat.handlers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.PongMessage; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import pl.polgrabiat.websockets.chat.websocketschat.dto.GlobalCtx; import pl.polgrabiat.websockets.chat.websocketschat.dto.UserCtx; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; @Component public class ChatWebsocketHandler extends TextWebSocketHandler { private static final Logger lg = LoggerFactory.getLogger(ChatWebsocketHandler.class); + private static final long PING_PONG_TIME = 59; - private Set sessions = new HashSet<>(); private Map commandHandlers = new HashMap<>(); private GlobalCtx globalCtx = new GlobalCtx(); + private Random rnd = new Random(); public ChatWebsocketHandler() { commandHandlers.put("USER", new UserCommandHandler()); @@ -30,20 +33,21 @@ public class ChatWebsocketHandler extends TextWebSocketHandler { commandHandlers.put("JOIN", new JoinCommandHandler()); commandHandlers.put("LEAVE", new LeaveCommandHandler()); commandHandlers.put("PRIVMSG", new PrivMessageHandler()); + commandHandlers.put("PONG", new PongMessageHandler()); + commandHandlers.put("PING", new PingMessageHandler()); } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); - sessions.add(session); - globalCtx.getUserSessions().put(session, new UserCtx(session)); + + globalCtx.addSession(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { super.afterConnectionClosed(session, status); - sessions.remove(session); - globalCtx.getUserSessions().remove(session); + globalCtx.removeSession(session); } @Override @@ -68,4 +72,38 @@ public class ChatWebsocketHandler extends TextWebSocketHandler { session.sendMessage(new TextMessage("Invalid command " + payload)); } + + + @Scheduled(fixedDelay=60000) + public void checkSessionActiveness() { + LocalDateTime time = LocalDateTime.now(); + lg.debug("Checking sessions with set local date time {} ...", time); + for (WebSocketSession session: globalCtx.getSessions()) { + try { + UserCtx userCtx = globalCtx.getUserSessions().get(session); + LocalDateTime lastPongTime = userCtx.getLastPongTime(); + if (lastPongTime != null + && lastPongTime.until(LocalDateTime.now(), ChronoUnit.SECONDS) >= PING_PONG_TIME) { + // drop session + lg.trace("Closing session {}[{}]", session.getId(), session.getRemoteAddress()); + session.close(); + globalCtx.removeSession(session); + continue; + } + + int pongNumber = rnd.nextInt(); + lg.trace("Sending ping message {} to the session {}[{}]", + pongNumber, + session.getId(), + session.getRemoteAddress()); + + session.sendMessage(new TextMessage("PING :" + pongNumber)); + userCtx.setLastPongNumber(pongNumber); + + } catch (IOException e) { + lg.warn("I/O error", e); + } + } + } + } diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/NickCommandHandler.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/NickCommandHandler.java index 1b6236b..fb2fd84 100644 --- a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/NickCommandHandler.java +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/NickCommandHandler.java @@ -2,6 +2,7 @@ package pl.polgrabiat.websockets.chat.websocketschat.handlers; import org.springframework.web.socket.WebSocketSession; import pl.polgrabiat.websockets.chat.websocketschat.dto.GlobalCtx; +import pl.polgrabiat.websockets.chat.websocketschat.dto.UserCtx; import java.io.IOException; import java.util.StringTokenizer; @@ -17,7 +18,14 @@ public class NickCommandHandler implements SessionCommandHandler { return true; } - globalCtx.getUserSessions().get(session).setNick(nick); + UserCtx userCtx = globalCtx.getUserSessions().get(session); + if (userCtx.getNick() != null) { + globalCtx.getUserNameSessions().remove(userCtx.getNick()); + // removing old mapping for destinations + } + + userCtx.setNick(nick); + globalCtx.getUserNameSessions().put(nick, session); return true; } diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PingMessageHandler.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PingMessageHandler.java new file mode 100644 index 0000000..65f7e3f --- /dev/null +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PingMessageHandler.java @@ -0,0 +1,14 @@ +package pl.polgrabiat.websockets.chat.websocketschat.handlers; + +import org.springframework.web.socket.WebSocketSession; +import pl.polgrabiat.websockets.chat.websocketschat.dto.GlobalCtx; + +import java.io.IOException; + +public class PingMessageHandler implements SessionCommandHandler { + @Override + public boolean handleCommand(GlobalCtx globalCtx, WebSocketSession session, String payload, String command, String data) throws IOException { + sendMessage(session,globalCtx,403, "MSG PING message is not accepted from the client-side"); + return true; + } +} diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PongMessageHandler.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PongMessageHandler.java new file mode 100644 index 0000000..e5efbf4 --- /dev/null +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/PongMessageHandler.java @@ -0,0 +1,43 @@ +package pl.polgrabiat.websockets.chat.websocketschat.handlers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.socket.WebSocketSession; +import pl.polgrabiat.websockets.chat.websocketschat.dto.GlobalCtx; +import pl.polgrabiat.websockets.chat.websocketschat.dto.UserCtx; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.StringTokenizer; + +public class PongMessageHandler implements SessionCommandHandler { + + + private static final Logger lg = LoggerFactory.getLogger(PongMessageHandler.class); + + @Override + public boolean handleCommand(GlobalCtx globalCtx, WebSocketSession session, String payload, String command, String data) throws IOException { + UserCtx userCtx = globalCtx.getUserSessions().get(session); + if (userCtx == null) { + lg.error("We lost an user context - dropping session, sorry..."); + globalCtx.removeSession(session); + return true; + } + + StringTokenizer tokenizer = new StringTokenizer(data); + String pongValue = tokenizer.nextToken(":"); + + lg.debug("Got pong value: {} for {}", pongValue, session); + try { + int pongVal = Integer.parseInt(pongValue); + if (userCtx.getLastPongNumber() == pongVal) { + userCtx.setLastPongTime(LocalDateTime.now()); + } + } catch (NumberFormatException e) { + lg.debug("Invalid format of pong message. Dropping session", e); + globalCtx.removeSession(session); + } + + return true; + } +} diff --git a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/UserCommandHandler.java b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/UserCommandHandler.java index 094c947..550076a 100644 --- a/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/UserCommandHandler.java +++ b/src/main/java/pl/polgrabiat/websockets/chat/websocketschat/handlers/UserCommandHandler.java @@ -1,5 +1,7 @@ package pl.polgrabiat.websockets.chat.websocketschat.handlers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.web.socket.WebSocketSession; import pl.polgrabiat.websockets.chat.websocketschat.dto.GlobalCtx; import pl.polgrabiat.websockets.chat.websocketschat.dto.UserCtx; @@ -8,6 +10,9 @@ import java.io.IOException; import java.util.StringTokenizer; public class UserCommandHandler implements SessionCommandHandler { + + private static final Logger lg = LoggerFactory.getLogger(UserCommandHandler.class); + @Override public boolean handleCommand(GlobalCtx globalCtx, WebSocketSession session, String payload, String command, String data) throws IOException { StringTokenizer tokenizer = new StringTokenizer(data," "); @@ -23,12 +28,15 @@ public class UserCommandHandler implements SessionCommandHandler { } String realName = tokenizer.nextToken(":"); - UserCtx userCtx = globalCtx.getUserSessions().get(session.getId()); - String nick = userCtx.getNick(); - if (nick == null || "".equals(nick)) { + UserCtx userCtx = globalCtx.getUserSessions().get(session); + if (userCtx == null) { + sendMessage(session, globalCtx, 403, + "MSG you nead to select the nick"); + return true; } + userCtx.setUserName(userName); userCtx.setRealName(realName); diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..ac9c93e --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/IrcClient.js b/src/main/resources/static/IrcClient.js new file mode 100644 index 0000000..c207746 --- /dev/null +++ b/src/main/resources/static/IrcClient.js @@ -0,0 +1,52 @@ +function IrcClient(sockJs, nick, userName, realName) { + this.sockJs = sockJs; + this.nick = nick; + this.userName = userName; + this.realName = realName; + + this.sendMsg = function(msg) { + console.log("OUT-MSG " + msg); + this.sockJs.send(msg); + }; + + this.sendNickMsg = function() { + this.sendMsg("NICK " + this.nick); + }; + + this.sendUserMsg = function() { + this.sendMsg("USER " + this.userName + " 0 * :" + this.realName); + }; + + this.init = function () { + this.sendNickMsg(); + this.sendUserMsg(); + }; + + var ircClientRef = this; + + this.sockJs.onopen = function () { + console.log("Socket opened"); + ircClientRef.init(); + }; + + this.sockJs.onclose = function () { + console.log("Socket closed"); + }; + + this.sendPongMsg = function(nr) { + this.sendMsg("PONG :" + nr); + }; + + this.handlePongMessage = function (payload, data) { + console.log("PING message: " + data); + this.sendPongMsg(data); + }; + + this.sockJs.onmessage = function (e) { + console.log("Received message: " + e.data); + if (e.data.startsWith("PING ")) { + ircClientRef.handlePongMessage(e.data, e.data.substr(7)); + } + }; + +}; \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 4837ecd..aabe0a6 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -4,7 +4,13 @@ SockJS example + + + + + + diff --git a/src/main/resources/static/index.js b/src/main/resources/static/index.js index 3e4a5a3..92cf78e 100644 --- a/src/main/resources/static/index.js +++ b/src/main/resources/static/index.js @@ -1,21 +1,32 @@ -window.onload = function() { - var sockJs = new SockJS("/chat"); - sockJs.onopen = function() { - console.log("Socket opened"); - // TODO send a nick message - // TODO send the userName and realName message - }; +window.initSockJs = function () { + window.sockJs = null; + window.nick = null; + bootbox.prompt({ + title: "Type the nick name for the chat", + inputType: 'text', + callback: function (result) { + console.log("Got nick: " + result); + window.nick = result; - sockJs.onclose = function() { - console.log("Socket closed"); - }; + if (window.nick == null || window.nick == undefined || window.nick.length < 1) { + console.log("Nick cannot be blank"); + bootbox.alert("Sorry, nick the cannot be blank", function (res) { + initSockJs(); + }); + return; + } - sockJs.onmessage = function(e) { - console.log("Received message: " + e.data); - }; + window.sockJs = new SockJS("/chat"); + window.ircClient = new IrcClient(window.sockJs, window.nick, window.nick, window.nick); + } + }); +}; + +window.onload = function () { + this.initSockJs(); var messageInput = document.getElementById("message"); - messageInput.onkeyup = function(e) { + messageInput.onkeyup = function (e) { if (e.keyCode == 13) { sockJs.send(messageInput.value); }