/*
* 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.shared.net;
import aphelion.client.net.SingleGameConnection;
import aphelion.server.AphelionServer;
import aphelion.server.http.HttpServer;
import aphelion.shared.event.TickedEventLoop;
import aphelion.shared.net.game.GameProtocolConnection;
import aphelion.shared.net.game.GameProtoListener;
import aphelion.shared.net.protobuf.GameC2S;
import aphelion.shared.net.protobuf.GameC2S.C2S;
import aphelion.shared.net.protobuf.GameOperation.ActorMove;
import aphelion.shared.net.protobuf.GameS2C;
import aphelion.shared.net.protobuf.GameS2C.ArenaSync;
import aphelion.shared.net.protobuf.GameS2C.S2C;
import aphelion.shared.physics.EnvironmentConf;
import aphelion.shared.physics.valueobjects.PhysicsMovement;
import com.google.protobuf.ByteString;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ServerSocketChannel;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.*;
import static org.junit.Assert.*;
/**
*
* @author Joris
*/
public class WebSocketTest
{
private static final Logger log = Logger.getLogger(WebSocketTest.class.getName());
TickedEventLoop loop;
SessionToken sessionToken;
private long sentC2SMove;
private long receivedC2SMove;
int serverNewClient; int clientNewClient;
boolean clientRemoved; boolean serverRemoved;
int serverMessages; int clientMessages;
int clientConnections; int serverConnections;
int serverPidSum;
int clientPidSum;
@Before
public void setUp()
{
serverNewClient = 0;
clientNewClient = 0;
clientRemoved = false;
serverRemoved = false;
serverMessages = 0;
clientMessages = 0;
clientConnections = 0;
serverConnections = 0;
clientPidSum = 0;
serverPidSum = 0;
}
@After
public void breakDown()
{
loop = null;
sessionToken = null;
}
@Test(timeout=20000)
public void testSingleGameWebSocket() throws IOException, URISyntaxException
{
// use the same loop for client and server
loop = new TickedEventLoop(EnvironmentConf.TICK_LENGTH, 1, null);
// Set up the server
// use an ephemeral port. aka a temporary port number
try (ServerSocketChannel ssChannel = HttpServer.openServerChannel(new InetSocketAddress("127.0.0.1", 0)))
{
AphelionServer server = new AphelionServer(ssChannel, new File("./www"), loop);
loop.addLoopEvent(server);
server.setGameClientListener(new testSingleGameWebSocket_ServerGameListener());
// set up the client
SingleGameConnection client = new SingleGameConnection(
new URI("ws://127.0.0.1:"+server.getHTTPListeningPort()+"/aphelion"),
loop,
1); // 1 connection;
client.addListener(new testSingleGameWebSocket_ClientGameListener());
loop.addLoopEvent(client);
server.setup();
client.connect();
log.log(Level.INFO, "Starting loop");
loop.run();
assertEquals(1, clientNewClient);
assertEquals(1, serverNewClient);
assertTrue(clientRemoved);
assertTrue(serverRemoved);
assertEquals(1, clientMessages);
assertEquals(1, serverMessages);
double latency = (receivedC2SMove - sentC2SMove) / 1000000d;
log.log(Level.INFO, "Latency: {0}ms", latency);
client.close(WS_CLOSE_STATUS.NORMAL);
server.closeAll(WS_CLOSE_STATUS.NORMAL);
server.stop();
client.stop();
}
}
private class testSingleGameWebSocket_ServerGameListener implements GameProtoListener
{
@Override
public void gameNewClient(GameProtocolConnection game)
{
log.log(Level.INFO, "Server: new client");
++serverNewClient;
assertEquals(0, serverConnections);
assertEquals(0, clientNewClient);
assertEquals(1, serverNewClient);
assertTrue(game.server);
sessionToken = game.session;
}
@Override
public void gameRemovedClient(GameProtocolConnection game)
{
log.log(Level.INFO, "Server: removed client");
assertEquals(0, serverConnections);
serverRemoved = true;
if (clientRemoved)
{
loop.interrupt();
}
}
@Override
public void gameC2SMessage(GameProtocolConnection game, GameC2S.C2S c2s, long receivedAt)
{
++serverMessages;
log.log(Level.INFO, "Server: C2S message: {0}", serverMessages);
assertEquals(1, c2s.getActorMoveCount());
assertEquals(1, c2s.getAllFields().size());
ActorMove move = c2s.getActorMove(0);
receivedC2SMove = System.nanoTime();
assertEquals(123, move.getTick());
assertEquals(456, move.getPid());
List<PhysicsMovement> moves = PhysicsMovement.unserializeListLE(move.getMove().asReadOnlyByteBuffer());
assertEquals(4, moves.size());
assertEquals(0x1, moves.get(0).bits);
assertEquals(0x1 | 0x4, moves.get(1).bits);
assertEquals(0x0, moves.get(2).bits);
assertEquals(0x8, moves.get(3).bits);
S2C.Builder s2c = S2C.newBuilder();
ArenaSync.Builder arenaSync = s2c.addArenaSyncBuilder();
arenaSync.setName("jowie");
arenaSync.setShip("warbird");
arenaSync.setCurrentTicks(5021034);
arenaSync.setCurrentNanoTime(164029301000000000L);
arenaSync.setYourPid(102);
arenaSync.setYourSeed(1234567890);
game.send(s2c.build());
}
@Override
public void gameS2CMessage(GameProtocolConnection game, GameS2C.S2C s2c, long receivedAt)
{
assertTrue(false);
}
@Override
public void gameNewConnection(GameProtocolConnection game)
{
++serverConnections;
log.log(Level.INFO, "Server: gameNewConnection: {0}", serverConnections);
assertEquals(1, serverNewClient);
}
@Override
public void gameDropConnection(GameProtocolConnection game, WS_CLOSE_STATUS code, String reason)
{
--serverConnections;
log.log(Level.INFO, "Server: gameDropConnection: {0}", serverConnections);
assertEquals(1, serverNewClient);
}
@Override
public void gameEstablishFailure(WS_CLOSE_STATUS code, String reason)
{
assert false;
}
}
private class testSingleGameWebSocket_ClientGameListener implements GameProtoListener
{
@Override
public void gameNewClient(GameProtocolConnection game)
{
log.log(Level.INFO, "Client: new client");
++clientNewClient;
assertEquals(0, clientConnections);
assertEquals(1, serverNewClient);
assertEquals(1, clientNewClient);
assertFalse(game.server);
assertEquals(sessionToken, game.session);
C2S.Builder c2s = C2S.newBuilder();
ActorMove.Builder actorMove = c2s.addActorMoveBuilder();
actorMove.setTick(123);
actorMove.setPid(456);
actorMove.setMove(ByteString.copyFrom(PhysicsMovement.serializeListLE(Arrays.asList(
PhysicsMovement.get(0x1),
PhysicsMovement.get(0x1 | 0x4),
PhysicsMovement.get(0x0),
PhysicsMovement.get(0x8)))));
sentC2SMove = System.nanoTime();
game.send(c2s.build());
}
@Override
public void gameRemovedClient(GameProtocolConnection game)
{
assertEquals(0, clientConnections);
log.log(Level.INFO, "Client: removed client");
clientRemoved = true;
if (serverRemoved)
{
loop.interrupt();
}
}
@Override
public void gameC2SMessage(GameProtocolConnection game, GameC2S.C2S c2s, long receivedAt)
{
assertTrue(false);
}
@Override
public void gameS2CMessage(GameProtocolConnection game, GameS2C.S2C s2c, long receivedAt)
{
++clientMessages;
log.log(Level.INFO, "Client: S2C message: {0}", clientMessages);
assertEquals(1, s2c.getArenaSyncCount());
assertEquals(1, s2c.getAllFields().size());
ArenaSync arenaSync = s2c.getArenaSync(0);
assertEquals("jowie", arenaSync.getName());
assertEquals("warbird", arenaSync.getShip());
assertEquals(5021034, arenaSync.getCurrentTicks());
assertEquals(164029301000000000L, arenaSync.getCurrentNanoTime());
assertEquals(102, arenaSync.getYourPid());
// done with the test
game.requestClose(WS_CLOSE_STATUS.NORMAL);
}
@Override
public void gameNewConnection(GameProtocolConnection game)
{
assertEquals(1, clientNewClient);
++clientConnections;
log.log(Level.INFO, "Client: gameNewConnection: {0}", clientConnections);
}
@Override
public void gameDropConnection(GameProtocolConnection game, WS_CLOSE_STATUS code, String reason)
{
assertEquals(1, clientNewClient);
--clientConnections;
log.log(Level.INFO, "Client: gameDropConnection: {0}", clientNewClient);
}
@Override
public void gameEstablishFailure(WS_CLOSE_STATUS code, String reason)
{
assert false;
}
}
@Test(timeout=20000)
public void testMultiGameWebSocket() throws IOException, URISyntaxException
{
// use the same loop for client and server
loop = new TickedEventLoop(EnvironmentConf.TICK_LENGTH, 1, null);
// Set up the server
// use an ephemeral port. aka a temporary port number
try (ServerSocketChannel ssChannel = HttpServer.openServerChannel(new InetSocketAddress("127.0.0.1", 0)))
{
AphelionServer server = new AphelionServer(ssChannel, new File("./www"), loop);
loop.addLoopEvent(server);
server.setGameClientListener(new testMultiGameWebSocket_ServerGameListener());
server.setup();
// set up the client
SingleGameConnection client = new SingleGameConnection(
new URI("ws://127.0.0.1:"+server.getHTTPListeningPort()+"/aphelion"),
loop,
5); // 5 connections;
client.addListener(new testMultiGameWebSocket_ClientGameListener());
loop.addLoopEvent(client);
client.connect();
log.log(Level.INFO, "Start loop");
loop.run();
assertEquals(1, clientNewClient);
assertEquals(1, serverNewClient);
assertTrue(clientRemoved);
assertTrue(serverRemoved);
assertEquals(18, clientMessages);
assertEquals(28, serverMessages);
assertEquals(210, clientPidSum);
assertEquals(465, serverPidSum);
client.close(WS_CLOSE_STATUS.NORMAL);
server.closeAll(WS_CLOSE_STATUS.NORMAL);
server.stop();
client.stop();
}
}
private class testMultiGameWebSocket_ServerGameListener implements GameProtoListener
{
@Override
public void gameNewClient(GameProtocolConnection game)
{
++serverNewClient;
log.log(Level.INFO, "Server: new client {0}", serverNewClient);
assertEquals(0, serverConnections);
assertEquals(0, clientNewClient);
assertEquals(1, serverNewClient);
assertTrue(game.server);
sessionToken = game.session;
}
@Override
public void gameRemovedClient(GameProtocolConnection game)
{
log.log(Level.INFO, "Server: removed client");
assertEquals(0, serverConnections);
serverRemoved = true;
if (clientRemoved)
{
loop.interrupt();
}
}
@Override
public void gameC2SMessage(GameProtocolConnection game, GameC2S.C2S c2s, long receivedAt)
{
log.log(Level.INFO, "Server: Received message {0}", serverMessages);
++serverMessages;
assertTrue(c2s.getActorMoveCount() > 0);
assertEquals(1, c2s.getAllFields().size());
for (int a = 0; a < c2s.getActorMoveCount(); ++a)
{
ActorMove move = c2s.getActorMove(a);
assertEquals(123, move.getTick());
serverPidSum += move.getPid();
List<PhysicsMovement> moves = PhysicsMovement.unserializeListLE(move.getMove().asReadOnlyByteBuffer());
assertEquals(4, moves.size());
assertEquals(0x1, moves.get(0).bits);
assertEquals(0x1 | 0x4, moves.get(1).bits);
assertEquals(0x0, moves.get(2).bits);
assertEquals(0x8, moves.get(3).bits);
}
if (clientMessages == 18 && serverMessages == 28)
{
game.requestClose(WS_CLOSE_STATUS.NORMAL);
}
}
@Override
public void gameS2CMessage(GameProtocolConnection game, GameS2C.S2C s2c, long receivedAt)
{
assertTrue(false);
}
@Override
public void gameNewConnection(GameProtocolConnection game)
{
++serverConnections;
log.log(Level.INFO, "Server: gameNewConnection {0}", serverConnections);
if (serverConnections == 5)
{
S2C.Builder s2c = S2C.newBuilder();
for (int a = 0; a < 20; ++a)
{
if (a <= 17)
{
// send the last 3 in a batch
s2c = S2C.newBuilder();
}
ActorMove.Builder actorMove = s2c.addActorMoveBuilder();
actorMove.setTick(123);
actorMove.setPid(a + 1);
actorMove.setMove(ByteString.copyFrom(PhysicsMovement.serializeListLE(Arrays.asList(
PhysicsMovement.get(0x1),
PhysicsMovement.get(0x1 | 0x4),
PhysicsMovement.get(0x0),
PhysicsMovement.get(0x8)))));
if (a < 17)
{
log.log(Level.INFO, "Server: gameNewConnection: send(s2c) {0}", s2c.getActorMoveCount());
game.send(s2c);
}
}
log.log(Level.INFO, "Server: gameNewConnection: send(s2c) {0} (after loop)", s2c.getActorMoveCount());
game.send(s2c);
}
}
@Override
public void gameDropConnection(GameProtocolConnection game, WS_CLOSE_STATUS code, String reason)
{
--serverConnections;
log.log(Level.INFO, "Server: gameDropConnection {0}", serverConnections);
}
@Override
public void gameEstablishFailure(WS_CLOSE_STATUS code, String reason)
{
assert false;
}
}
private class testMultiGameWebSocket_ClientGameListener implements GameProtoListener
{
@Override
public void gameNewClient(GameProtocolConnection game)
{
++clientNewClient;
log.log(Level.INFO, "Client: new client {0} clientNewClient");
assertEquals(0, clientConnections);
assertEquals(1, serverNewClient);
assertEquals(1, clientNewClient);
assertFalse(game.server);
assertEquals(sessionToken, game.session);
}
@Override
public void gameRemovedClient(GameProtocolConnection game)
{
log.log(Level.INFO, "Client: removed client");
assertEquals(0, clientConnections);
clientRemoved = true;
if (serverRemoved)
{
loop.interrupt();
}
}
@Override
public void gameC2SMessage(GameProtocolConnection game, GameC2S.C2S c2s, long receivedAt)
{
assertTrue(false);
}
@Override
public void gameS2CMessage(GameProtocolConnection game, GameS2C.S2C s2c, long receivedAt)
{
++clientMessages;
log.log(Level.INFO, "Client: received message {0}", clientMessages);
assertTrue(s2c.getActorMoveCount() > 0);
assertEquals(1, s2c.getAllFields().size());
for (int a = 0; a < s2c.getActorMoveCount(); ++a)
{
ActorMove move = s2c.getActorMove(a);
assertEquals(123, move.getTick());
clientPidSum += move.getPid();
List<PhysicsMovement> moves = PhysicsMovement.unserializeListLE(move.getMove().asReadOnlyByteBuffer());
assertEquals(4, moves.size());
assertEquals(0x1, moves.get(0).bits);
assertEquals(0x1 | 0x4, moves.get(1).bits);
assertEquals(0x0, moves.get(2).bits);
assertEquals(0x8, moves.get(3).bits);
}
if (clientMessages == 18 && serverMessages == 28)
{
game.requestClose(WS_CLOSE_STATUS.NORMAL);
}
}
@Override
public void gameNewConnection(GameProtocolConnection game)
{
++clientConnections;
log.log(Level.INFO, "Client: gameNewConnection {0}", clientConnections);
if (clientConnections == 5)
{
C2S.Builder c2s = C2S.newBuilder();
for (int a = 0; a < 30; ++a)
{
if (a <= 27)
{
// send the last 3 in a batch
c2s = C2S.newBuilder();
}
ActorMove.Builder actorMove = c2s.addActorMoveBuilder();
actorMove.setTick(123);
actorMove.setPid(a + 1);
actorMove.setMove(ByteString.copyFrom(PhysicsMovement.serializeListLE(Arrays.asList(
PhysicsMovement.get(0x1),
PhysicsMovement.get(0x1 | 0x4),
PhysicsMovement.get(0x0),
PhysicsMovement.get(0x8)))));
if (a < 27)
{
log.log(Level.INFO, "Client: gameNewConnection: send(c2s) {0}", c2s.getActorMoveCount());
game.send(c2s);
}
}
log.log(Level.INFO, "Client: gameNewConnection: send(c2s) {0} (after loop)", c2s.getActorMoveCount());
game.send(c2s);
}
}
@Override
public void gameDropConnection(GameProtocolConnection game, WS_CLOSE_STATUS code, String reason)
{
--clientConnections;
log.log(Level.INFO, "Client: gameDropConnection {0}", clientConnections);
}
@Override
public void gameEstablishFailure(WS_CLOSE_STATUS code, String reason)
{
assert false;
}
}
}