Skip to content

Instantly share code, notes, and snippets.

@afarber
Created March 1, 2021 09:42
Show Gist options
  • Save afarber/4f82205881ddb0223130f74b4e87abda to your computer and use it in GitHub Desktop.
Save afarber/4f82205881ddb0223130f74b4e87abda to your computer and use it in GitHub Desktop.
A static map of maps to handle WebSocket sessions for users
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