/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.client.net; import aphelion.shared.net.PROTOCOL; import aphelion.shared.net.SessionToken; import aphelion.shared.net.WS_CLOSE_STATUS; import aphelion.shared.net.WebSocketTransport; import aphelion.shared.net.WebSocketTransportListener; import aphelion.shared.net.protobuf.Ping.PingRequest; import aphelion.shared.net.protobuf.Ping.PingResponse; import aphelion.shared.swissarmyknife.ThreadSafe; import com.google.protobuf.CodedOutputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import org.java_websocket.framing.CloseFrame; /** * * @author Joris */ public class Ping extends Thread { private static final Logger log = Logger.getLogger("aphelion.net.ping"); private final static int CLIENT_PROTO_VERSION = 1; // use 0 to disable checking private final static long PING_INTERVAL_NANO = 5_000_000_000L; private final HashMap<URI, ServerData> servers = new HashMap<>(); private PingListener listener; private volatile boolean firstResult = true; // todo: atm this launches a thread per connection (see WebSocketTransport). Improve? private class ServerData implements WebSocketTransportListener { URI uri; WebSocketTransport webSocketTransport; SessionToken session = null; Long lastRequest_nanos = null; boolean attemptConnect = false; boolean disconnected = true; boolean stop; ServerData(URI uri) { this.uri = uri; webSocketTransport = new WebSocketTransport(this); } @ThreadSafe public synchronized void send() { if (disconnected) { if (!attemptConnect) { webSocketTransport.establishClientConnection(uri, session, PROTOCOL.PING, CLIENT_PROTO_VERSION); attemptConnect = true; } return; } PingRequest.Builder builder = PingRequest.newBuilder(); lastRequest_nanos = System.nanoTime(); builder.setClientTime(lastRequest_nanos); PingRequest request = builder.build(); int size = request.getSerializedSize(); byte[] result = new byte[size + WebSocketTransport.SEND_RESERVEDPREFIX_BYTES]; CodedOutputStream output = CodedOutputStream.newInstance(result, WebSocketTransport.SEND_RESERVEDPREFIX_BYTES, size); try { request.writeTo(output); } catch (IOException ex) { throw new Error(ex); } // assert that there are no bytes left output.checkNoSpaceLeft(); try { webSocketTransport.send(session, PROTOCOL.PING, result); } catch (WebSocketTransport.NoSuitableConnection ex) { log.log(Level.WARNING, null, ex); // should not happen } } @Override @ThreadSafe public boolean wstIsValidProtocol(PROTOCOL protocol) { throw new Error("This method should not be used for clients"); } @Override @ThreadSafe public int wstIsValidProtocolVersion(PROTOCOL protocol, int protocolVersion) { throw new Error("This method should not be used for clients"); } @Override @ThreadSafe public void wstClientEstablishFailure(SessionToken sessionToken, PROTOCOL protocol, WS_CLOSE_STATUS code, String reason) { synchronized(this) { assert protocol == PROTOCOL.PING; disconnected = true; // reconnect attemptConnect = false; } listener.pingResult(uri, -1, -1, -1); } @Override @ThreadSafe public synchronized void wstNewProtocol(boolean server, SessionToken sessionToken, PROTOCOL protocol) { assert protocol == PROTOCOL.PING; this.session = sessionToken; disconnected = false; attemptConnect = false; send(); // do the first ping immediately } @Override @ThreadSafe public void wstDropProtocol(boolean server, SessionToken sessionToken, PROTOCOL protocol) { synchronized(this) { assert protocol == PROTOCOL.PING; disconnected = true; // reconnect } listener.pingResult(uri, -1, -1, -1); } @Override @ThreadSafe public void wstNewConnection(boolean server, SessionToken sessionToken, PROTOCOL protocol) { } @Override @ThreadSafe public void wstDropConnection(boolean server, SessionToken sessionToken, PROTOCOL protocol, WS_CLOSE_STATUS code, String reason) { } @Override @ThreadSafe public void wstMessage(boolean server, SessionToken sessionToken, PROTOCOL protocol, int protocolVersion, ByteBuffer message, long receivedAt) { assert protocol == PROTOCOL.PING; long result_rttLatency; int result_players; int result_playing; synchronized(this) { try { PingResponse pingResponse = PingResponse.parseFrom(new ByteArrayInputStream(message.array(), message.position(), message.remaining())); // race condition on this variable is okay, it would at most make us loose more than 1 result. // anyways, the very first result is inaccurate because of java lazy class loading. if (firstResult) { firstResult = false; return; } if (pingResponse.getClientTime() >= lastRequest_nanos) { result_rttLatency = receivedAt - pingResponse.getClientTime(); result_players = pingResponse.hasPlayers() ? pingResponse.getPlayers() : -1; result_playing = pingResponse.hasPlaying() ? pingResponse.getPlaying() : -1; } else { return; } } catch (IOException ex) { log.log(Level.SEVERE, "Error parsing ping response", ex); return; } } listener.pingResult(uri, result_rttLatency, result_players, result_playing); } @Override @ThreadSafe public void wstMessage(boolean server, SessionToken sessionToken, PROTOCOL protocol, int protocolVersion, String message, long receivedAt) { } } public Ping(PingListener listener) { this.listener = listener; } /** Perform pings to an aphelion server at uri. * The pings to do not stop until stop() is called * @param uri */ @ThreadSafe public void startPing(URI uri) { synchronized(this) { ServerData data = servers.get(uri); if (data != null && !data.stop) { return; // already pinging it } servers.put(uri, new ServerData(uri)); } } /** Stop pinging a server. * @param uri */ @ThreadSafe public void stopPing(URI uri) { synchronized(this) { ServerData data = servers.get(uri); if (data == null) { return; } data.webSocketTransport.closeAll(WS_CLOSE_STATUS.NORMAL); data.stop = true; } } /** Stop pinging all servers */ @ThreadSafe public void stopPings() { synchronized(this) { for (ServerData data : servers.values()) { data.stop = true; data.webSocketTransport.closeAll(WS_CLOSE_STATUS.NORMAL); } } } @Override public void run() { setName("Ping-" + this.getId()); while (true) { long nanoTime = System.nanoTime(); try { synchronized(this) { Iterator<ServerData> it = servers.values().iterator(); while(it.hasNext()) { ServerData data = it.next(); data.webSocketTransport.loop(nanoTime, nanoTime); // make sure that any value that had stopPing() called gets atleast 1 loop() call! if (data.stop) { it.remove(); } } } synchronized(this) { for (ServerData data : servers.values()) { if (data.lastRequest_nanos == null || nanoTime - data.lastRequest_nanos >= PING_INTERVAL_NANO) { data.lastRequest_nanos = nanoTime; data.send(); } } } Thread.sleep(10); } catch (InterruptedException ex) { break; } } synchronized(this) { for (ServerData data : servers.values()) { data.webSocketTransport.stopClients(); } } } }