Created
March 1, 2021 09:42
-
-
Save afarber/4f82205881ddb0223130f74b4e87abda to your computer and use it in GitHub Desktop.
A static map of maps to handle WebSocket sessions for users
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package de.afarber; | |
import java.io.IOException; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Objects; | |
import java.util.concurrent.ConcurrentHashMap; | |
import java.util.stream.Collectors; | |
import org.eclipse.jetty.websocket.api.Session; | |
public final class Client implements Common { | |
// epoch timestamp - when were the old connections cleaned last time | |
private static int lastRun; | |
// multi value map with keys: numeric user id, values: remote address -> Client object | |
private static final Map<Integer, Map<String, Client>> CLIENTS = new ConcurrentHashMap<>(); | |
public static void add(Integer uid, String address, Session session) { | |
Client client = new Client(uid, address, session); | |
// if there are no entries for the uid, create a new map, in a thread-safe way | |
CLIENTS.computeIfAbsent(uid, (x -> new ConcurrentHashMap<>())).put(client.address, client); | |
} | |
public static List<Session> getOpenSessions(Integer uid) { | |
// the fallback will never be called, so it is okay performancewise | |
Map<String, Client> map = CLIENTS.computeIfAbsent(uid, (x -> new ConcurrentHashMap<>())); | |
return map | |
.values() | |
.stream() | |
.filter(x -> x.session.isOpen()) | |
.map(x -> x.session) | |
.collect(Collectors.toList()); | |
} | |
public static void remove(Integer uid, String address, boolean shouldClose) { | |
// the fallback will never be called, so it is okay performancewise | |
Map<String, Client> map = CLIENTS.computeIfAbsent(uid, (x -> new ConcurrentHashMap<>())); | |
Client client = map.remove(address); | |
LOG.info("removing uid={} address={} shouldClose={} client={}", uid, address, shouldClose, client); | |
// if there are no entries for the uid, delete the empty map | |
if (map.isEmpty()) { | |
CLIENTS.remove(uid); | |
} | |
if (shouldClose && client != null && client.session.isOpen()) { | |
try { | |
client.session.close(); | |
client.session.disconnect(); | |
} catch (IOException ex) { | |
// ignore | |
} | |
} | |
} | |
public static void updateStamp(Integer uid, String address) { | |
CLIENTS.get(uid).get(address).human = (int) (System.currentTimeMillis() / 1000); | |
} | |
public static void disconnectStale() { | |
int now = (int) (System.currentTimeMillis() / 1000); | |
if (now - lastRun < IDLE_TIMEOUT_SECONDS) { | |
return; | |
} | |
Iterator<Map.Entry<Integer, Map<String, Client>>> it1 = CLIENTS.entrySet().iterator(); | |
while (it1.hasNext()) { | |
// get the map: remote address -> Client object | |
Map<String, Client> map = it1.next().getValue(); | |
Iterator<Map.Entry<String, Client>> it2 = map.entrySet().iterator(); | |
while (it2.hasNext()) { | |
Client client = it2.next().getValue(); | |
if (!client.session.isOpen()) { | |
LOG.info("disconnectStale session not open, removing client={}", client); | |
it2.remove(); | |
} else if (now - client.human > IDLE_TIMEOUT_SECONDS) { | |
LOG.info("disconnectStale connection stale, removing client={}", client); | |
try { | |
client.session.close(); | |
client.session.disconnect(); | |
} catch (IOException ex) { | |
// ignore | |
} | |
it2.remove(); | |
} | |
} | |
if (map.isEmpty()) { | |
it1.remove(); | |
} | |
} | |
lastRun = now; | |
} | |
public static void clear() { | |
Iterator<Map.Entry<Integer, Map<String, Client>>> it = CLIENTS.entrySet().iterator(); | |
while (it.hasNext()) { | |
// get the map: remote address -> Client object | |
Map<String, Client> map = it.next().getValue(); | |
map.clear(); | |
it.remove(); | |
} | |
} | |
// numeric user id | |
private final int uid; | |
// the remote IP address ":" port | |
private final String address; | |
// the WebSocket session | |
private final Session session; | |
// timestamp in seconds, when the last action by the human player was received | |
private int human; | |
public Client(int uid, String address, Session session) { | |
this.uid = uid; | |
this.address = address; | |
this.session = session; | |
this.human = (int) (System.currentTimeMillis() / 1000); | |
if ( | |
uid <= 0 || | |
address == null || | |
address.length() < "1.1.1.1:1".length() || | |
session == null || | |
!session.isOpen() | |
) { | |
throw new IllegalArgumentException(toString()); | |
} | |
} | |
@Override | |
public String toString() { | |
return Client.class.getSimpleName() + | |
", uid: " + uid + | |
", address: " + address + | |
", session: " + session + | |
", stamp: " + human; | |
} | |
@Override | |
public boolean equals(Object other){ | |
return other instanceof Client && | |
uid == ((Client) other).uid && | |
address.equals(((Client) other).address); | |
} | |
@Override | |
public int hashCode() { | |
int hash = 3; | |
hash = 19 * hash + this.uid; | |
hash = 19 * hash + Objects.hashCode(this.address); | |
return hash; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment