/*
* 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.server;
import aphelion.shared.net.game.GameProtoListener;
import aphelion.shared.net.SessionToken;
import aphelion.server.http.HttpServer;
import aphelion.shared.event.LoopEvent;
import aphelion.shared.event.Workable;
import aphelion.shared.net.PROTOCOL;
import aphelion.shared.net.game.GameProtocolConnection;
import aphelion.shared.net.WS_CLOSE_STATUS;
import aphelion.shared.net.WebSocketTransport;
import aphelion.shared.net.WebSocketTransport.NoSuitableConnection;
import aphelion.shared.net.WebSocketTransportListener;
import aphelion.shared.net.protobuf.Ping;
import aphelion.shared.swissarmyknife.AttachmentConsumer;
import aphelion.shared.swissarmyknife.ThreadSafe;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Joris
*/
public class AphelionServer implements LoopEvent, WebSocketTransportListener
{
private static final Logger log = Logger.getLogger("aphelion.server");
private static final AttachmentConsumer<SessionToken, GameProtocolConnection> gameAttachment = new AttachmentConsumer<>(SessionToken.attachmentManager);
private final static int SERVER_PING_PROTO_VERSION = 1;
private final static int SERVER_GAME_PROTO_VERSION = 5;
private final HttpServer httpServer;
private final Workable workable;
private final WebSocketTransport webSocketTransport;
private GameProtoListener gameClientListener;
private boolean hasSetup = false;
private boolean stop = false;
private volatile int ping_players = -1;
private volatile int ping_playing = -1;
public AphelionServer(ServerSocketChannel httpListen, File httpDocs, Workable workManager) throws IOException
{
this.workable = workManager;
webSocketTransport = new WebSocketTransport(this);
httpServer = new HttpServer(httpListen, httpDocs, webSocketTransport);
}
public void addHttpRouteStatic(String path, File file) throws IOException, SecurityException
{
httpServer.addRouteStatic(path, file);
}
public void setGameClientListener(GameProtoListener gameClientListener)
{
if (hasSetup)
{
// Otherwise we have to lock
throw new IllegalStateException();
}
this.gameClientListener = gameClientListener;
}
public void setup() throws IOException
{
if (stop)
{
throw new IllegalStateException();
}
hasSetup = true;
httpServer.setup();
}
/** Returns the TCP port that the HTTP server is currently listening on
* @return -1 if not yet listening, otherwise the listening port.
*/
public int getHTTPListeningPort()
{
return httpServer.getListeningPort();
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
if (stop)
{
throw new IllegalStateException();
}
httpServer.loop(systemNanoTime, sourceNanoTime);
webSocketTransport.loop(systemNanoTime, sourceNanoTime);
}
public void closeAll(WS_CLOSE_STATUS code)
{
webSocketTransport.closeAll(code);
}
public void closeAll(WS_CLOSE_STATUS code, String message)
{
webSocketTransport.closeAll(code, message);
}
public void stop()
{
closeAll(WS_CLOSE_STATUS.NORMAL);
httpServer.stop(); // thread join
stop = true;
log.log(Level.INFO, "AphelionServer has stopped");
}
/** Set the player count to be reported in the ping protocol.
*
* @param players The total amount of players in ships, spectator, whatever.
* Should not include server generated players.
* Pass -1 to disable sending this value (the default).
* @param playing The total amount of players actually participating in the game.
* This means not in spectator and not afk in a safety.
* Pass -1 to disable sending this value (the default).
*/
@ThreadSafe
public void setPingPlayerCount(int players, int playing)
{
ping_players = players;
ping_playing = playing;
}
@Override
@ThreadSafe
public boolean wstIsValidProtocol(PROTOCOL protocol)
{
if (protocol == PROTOCOL.PING)
{
return true;
}
if (protocol == PROTOCOL.GAME && this.gameClientListener != null)
{
return true;
}
return false;
}
@Override
@ThreadSafe
public int wstIsValidProtocolVersion(PROTOCOL protocol, int protocolVersion)
{
if (protocolVersion == 0)
{
// disable version check
return 0;
}
if (protocol == PROTOCOL.PING)
{
return Integer.compare(protocolVersion, SERVER_PING_PROTO_VERSION);
}
if (protocol == PROTOCOL.GAME)
{
return Integer.compare(protocolVersion, SERVER_GAME_PROTO_VERSION);
}
return 0;
}
@Override
@ThreadSafe
public void wstNewProtocol(boolean server, SessionToken sessionToken, PROTOCOL protocol)
{
assert server;
if (protocol == PROTOCOL.PING)
{
// nothing to do
}
else if (protocol == PROTOCOL.GAME)
{
GameProtocolConnection game;
synchronized(sessionToken)
{
game = gameAttachment.get(sessionToken);
if (game != null)
{
game.removed();
}
game = new GameProtocolConnection(workable, webSocketTransport, sessionToken, server);
game.addListener(gameClientListener);
gameAttachment.set(sessionToken, game);
}
game.created();
}
}
@Override
@ThreadSafe
public void wstDropProtocol(boolean server, SessionToken sessionToken, PROTOCOL protocol)
{
assert server;
if (protocol == PROTOCOL.PING)
{
// nothing to do
}
else if (protocol == PROTOCOL.GAME)
{
GameProtocolConnection game;
synchronized(sessionToken)
{
game = gameAttachment.get(sessionToken);
if (game != null)
{
game.removed();
gameAttachment.set(sessionToken, null);
}
}
}
}
@Override
@ThreadSafe
public void wstMessage(boolean server, SessionToken sessionToken, PROTOCOL protocol, int protocolVersion, ByteBuffer message, long receivedAt)
{
assert server;
// called from websocket thread
if (protocol == PROTOCOL.PING)
{
try
{
Ping.PingRequest pingRequest = Ping.PingRequest.parseFrom(new ByteArrayInputStream(message.array(), message.position(), message.remaining()));
Ping.PingResponse.Builder responseBuilder = Ping.PingResponse.newBuilder();
responseBuilder.setClientTime(pingRequest.getClientTime());
responseBuilder.setServerTime(System.nanoTime());
if (ping_players >= 0)
{
responseBuilder.setPlayers(ping_players);
}
if (ping_playing >= 0)
{
responseBuilder.setPlaying(ping_playing);
}
Ping.PingResponse response = responseBuilder.build();
int size = response.getSerializedSize();
byte[] result = new byte[size + WebSocketTransport.SEND_RESERVEDPREFIX_BYTES];
CodedOutputStream output = CodedOutputStream.newInstance(result, WebSocketTransport.SEND_RESERVEDPREFIX_BYTES, size);
response.writeTo(output);
output.checkNoSpaceLeft();
webSocketTransport.send(sessionToken, PROTOCOL.PING, result);
}
catch (InvalidProtocolBufferException ex)
{
log.log(Level.SEVERE, "Protobuf Exception while parsing a message as a server", ex);
}
catch (IOException ex)
{
log.log(Level.SEVERE, "IOException while parsing a message as a server", ex);
}
catch (NoSuitableConnection ex)
{
log.log(Level.SEVERE, "NoSuitableConnection while parsing a message as a server", ex);
}
}
else if (protocol == PROTOCOL.GAME)
{
GameProtocolConnection game;
synchronized(sessionToken)
{
game = gameAttachment.get(sessionToken);
if (game == null)
{
log.log(Level.WARNING, "Received a game message in an incorrect state");
return;
}
}
game.parseMessage(message, receivedAt);
}
}
@Override
@ThreadSafe
public void wstMessage(boolean server, SessionToken sessionToken, PROTOCOL protocol, int protocolVersion, String message, long receivedAt)
{
assert server;
if (protocol == PROTOCOL.GAME)
{
}
}
@Override
public void wstNewConnection(boolean server, SessionToken sessionToken, PROTOCOL protocol)
{
assert server;
if (protocol == PROTOCOL.GAME)
{
GameProtocolConnection game;
synchronized(sessionToken)
{
game = gameAttachment.get(sessionToken);
if (game == null)
{
return;
}
}
game.connectionAdded();
}
}
@Override
public void wstDropConnection(boolean server, SessionToken sessionToken, PROTOCOL protocol, WS_CLOSE_STATUS code, String reason)
{
assert server;
if (protocol == PROTOCOL.GAME)
{
GameProtocolConnection game;
synchronized(sessionToken)
{
game = gameAttachment.get(sessionToken);
if (game == null)
{
return;
}
}
game.connectionDropped(code, reason);
}
}
@Override
public void wstClientEstablishFailure(SessionToken sessionToken, PROTOCOL protocol, WS_CLOSE_STATUS code, String reason)
{
throw new Error("This method should not be used for servers");
}
}