Ping pong mechanism works.
parent
592074457e
commit
3c6119369d
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -53,4 +53,16 @@ public class GlobalCtx {
|
|||
public void setUserNameSessions(Map<String, WebSocketSession> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<WebSocketSession> sessions = new HashSet<>();
|
||||
private Map<String, SessionCommandHandler> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="pl.polgrabiat.websockets"
|
||||
level="TRACE"
|
||||
appender-ref="STDOUT"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
};
|
|
@ -4,7 +4,13 @@
|
|||
<title>SockJS example</title>
|
||||
<meta charset="UTF-8" />
|
||||
<script type="application/javascript" src="/webjars/sockjs-client/0.3.4/sockjs.js"></script>
|
||||
<script type="application/javascript" src="/webjars/jquery/3.2.1/jquery.js"></script>
|
||||
<script type="application/javascript" src="/webjars/bootstrap/3.3.7-1/js/bootstrap.js"></script>
|
||||
<script type="application/javascript" src="/webjars/bootbox/4.4.0/bootbox.js"></script>
|
||||
<script type="application/javascript" src="index.js"></script>
|
||||
<script type="application/javascript" src="IrcClient.js"></script>
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="/webjars/bootstrap/3.3.7-1/css/bootstrap.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<input id="message" type="text" />
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
window.sockJs = new SockJS("/chat");
|
||||
window.ircClient = new IrcClient(window.sockJs, window.nick, window.nick, window.nick);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
sockJs.onclose = function() {
|
||||
console.log("Socket closed");
|
||||
};
|
||||
|
||||
sockJs.onmessage = function(e) {
|
||||
console.log("Received message: " + e.data);
|
||||
};
|
||||
this.initSockJs();
|
||||
|
||||
var messageInput = document.getElementById("message");
|
||||
messageInput.onkeyup = function (e) {
|
||||
|
|
Loading…
Reference in New Issue