Skip to content

Instantly share code, notes, and snippets.

@edolganov
Last active August 29, 2015 14:07
Show Gist options
  • Save edolganov/597090d81398b6f58586 to your computer and use it in GitHub Desktop.
Save edolganov/597090d81398b6f58586 to your computer and use it in GitHub Desktop.
Simple java web-sockets demo
This is java websockets exapmle:
Files:
- 01_GameServer.java - start server with Jetty Server inside
- 02_GameSocketServlet.java - mapping to ws://127.0.0.1:8210/gamestate url
- 03_GameSocket.java - handler of websocket connections
- 04_SocketPlayer.java - link between the server and the specific client
- 05_GameApp.java - the game engine
- 06_GameMap.java - map model
- 07_GameSession.java - game session with connected players
- 08_PlayerGameState.java - player's state in a session
- 11_game.js - client app
package game.server;
import static och.util.Util.*; //just util class - nothing special
import game.app.GameApp;
import java.util.List;
import javax.servlet.annotation.WebServlet;
import org.apache.commons.logging.Log;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
public class GameServer {
static Log log = getLog(GameServer.class);
private static GameApp game;
public static GameApp app(){
return game;
}
public static void main(String[] args) throws Exception {
log.info("start server...");
int port = args.length > 0 ? tryParseInt(args[0], 8210) : 8210;
int period = args.length > 1 ? tryParseInt(args[1], 80) : 80;
String mapsPath = args.length > 2? args[2] : "./extra/websocket-testgame/maps";
game = new GameApp(period);
List<String> loadedIds = game.loadMaps(mapsPath);
log.info("loaded maps: "+loadedIds.size());
if(loadedIds.size() > 0) game.createGameSessionsWithMap(loadedIds.get(0));
Server server = new Server(port);
server.setStopAtShutdown(true);
server.setHandler(createServlets());
server.start();
log.info("started");
server.join();
}
private static Handler createServlets() {
ServletHandler sh = new ServletHandler();
addServletWithMapping(sh, GameSocketServlet.class);
return sh;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void addServletWithMapping(ServletHandler sh, Class type){
WebServlet annotation = (WebServlet)type.getAnnotation(WebServlet.class);
String[] paths = annotation.value();
for (String path : paths) {
log.info("map path: "+path);
sh.addServletWithMapping(type, path);
}
}
}
package game.server;
import javax.servlet.annotation.WebServlet;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@SuppressWarnings("serial")
@WebServlet(value="/gamestate")
public class GameSocketServlet extends WebSocketServlet {
@Override
public void configure(WebSocketServletFactory factory) {
WebSocketPolicy policy = factory.getPolicy();
policy.setIdleTimeout(10000);
factory.register(GameSocket.class);
}
}
package game.server;
import static game.api.model.ClientMsg.*;
import static och.util.Util.*;
import game.api.model.ClientMsg;
import game.app.GameApp;
import org.apache.commons.logging.Log;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@WebSocket(maxMessageSize = 64 * 1024)
public class GameSocket {
Log log = getLog(getClass());
GameApp app;
public GameSocket() {
app = GameServer.app();
}
@OnWebSocketConnect
public void onConnect(Session session) {
try {
String clientId = getClientSessionId(session);
app.addPlayerToGameSession(new SocketPlayer(clientId, session));
} catch (Throwable t) {
log.error("error in onConnect", t);
}
}
@OnWebSocketMessage
public void onMessage(Session session, String msg) {
try {
String playerId = getClientSessionId(session);
if(isType(msg, READY_TO_START)){
app.putPlayerToMap(playerId);
return;
}
if(isType(msg, KEY_DOWN)){
app.onKeyDown(playerId, getMsgVal(msg, KEY_DOWN));
return;
}
if(isType(msg, KEY_UP)){
app.onKeyUp(playerId, getMsgVal(msg, KEY_UP));
return;
}
} catch (Throwable t) {
log.error("error in onMessage", t);
}
}
@OnWebSocketClose
public void onClose(Session session, int statusCode, String reason) {
try {
String playerId = getClientSessionId(session);
app.removePlayerFromMap(playerId);
} catch (Throwable t) {
log.error("error in onClose", t);
}
}
public static String getClientSessionId(Session session) {
String out = session.getRemoteAddress().toString()+"@"+session.hashCode();
return out;
}
public static boolean isType(String msg, ClientMsg type){
return msg != null && msg.startsWith(type.name());
}
public static String getMsgVal(String msg, ClientMsg type){
return msg.substring(type.name().length());
}
}
package game.server;
import static och.util.Util.*;
import game.api.model.ServerMsg;
import game.api.model.game.AddedToGameResp;
import game.api.model.game.GameMap;
import game.app.Player;
import org.apache.commons.logging.Log;
import org.eclipse.jetty.websocket.api.Session;
public class SocketPlayer implements Player {
static Log log = getLog(SocketPlayer.class);
String id;
Session session;
public SocketPlayer(String id, Session session) {
this.id = id;
this.session = session;
}
@Override
public String getId() {
return id;
}
@Override
public void onAddedToGameSession(GameMap map, int playerId) {
try {
if( ! session.isOpen()) return;
session.getRemote().sendStringByFuture(ServerMsg.IN_GAME+toJson(new AddedToGameResp(map, playerId)));
}catch(Throwable t){
logError("can't sendCurMapTemplate", t);
}
}
@Override
public void onStateUpdated(String state) {
try {
if( ! session.isOpen()) return;
session.getRemote().sendStringByFuture(ServerMsg.STATE+state);
}catch(Throwable t){
logError("can't sendCurMapTemplate", t);
}
}
private void logError(String msg, Throwable t){
log.error(msg+": id="+id+", error="+t);
}
}
package game.app;
import static och.util.StringUtil.*;
import static och.util.Util.*;
import game.api.model.game.GameMap;
import game.app.model.GameSession;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import och.util.FileUtil;
import och.util.concurrent.ExecutorsUtil;
import org.apache.commons.logging.Log;
public class GameApp {
private static Log log = getLog(GameApp.class);
private ReadWriteLock rw = new ReentrantReadWriteLock();
private Lock read = rw.readLock();
private Lock write = rw.writeLock();
private long period;
private ScheduledExecutorService pool;
//model
private Map<String, GameMap> allMapsTemplates = new HashMap<>();
private Map<String, Player> allPlayers = new HashMap<>();
private List<GameSession> gameSessions = new ArrayList<>();
private Map<String, Integer> playerSessionIndex = new HashMap<>();
public GameApp(long period) {
this.period = period;
reinitPool();
}
public void reinitPool() {
if(pool != null) {
try {
pool.shutdownNow();
}catch(Throwable t){
log.error("can't shutdownNow pool", t);
}
}
pool = ExecutorsUtil.newScheduledThreadPool("update-game-sessions", Runtime.getRuntime().availableProcessors());
}
public List<String> loadMaps(String dirPath) {
List<GameMap> loaded = new ArrayList<>();
List<String> ids = new ArrayList<>();
File dir = new File(dirPath);
File[] files = dir.listFiles();
if( ! isEmpty(files)){
for (File file : files) {
if( ! file.isFile()) continue;
GameMap map = null;
String name = file.getName();
if(name.endsWith(".csv")){
map = parseMapFromCsv(name, file);
}
if(map == null) continue;
loaded.add(map);
}
}
write.lock();
try {
for (GameMap map : loaded) {
allMapsTemplates.put(map.id, map);
ids.add(map.id);
}
}finally {
write.unlock();
}
return ids;
}
public void createGameSessionsWithMap(String mapId) {
write.lock();
try {
GameMap map = allMapsTemplates.get(mapId);
if(map == null) return;
//TODO close old sessions
gameSessions.clear();
reinitPool();
//create new sessions
GameSession gameSession = new GameSession(map);
gameSessions.add(gameSession);
//start to update state
pool.scheduleWithFixedDelay(()->{
gameSession.nextState();
}, period, period, TimeUnit.MILLISECONDS);
}finally {
write.unlock();
}
}
public void addPlayerToGameSession(Player p){
write.lock();
try {
if(gameSessions.isEmpty()) return;
String playerId = p.getId();
if(playerSessionIndex.containsKey(playerId)) throw new IllegalArgumentException("player already exists");
if( ! allPlayers.containsKey(playerId)) allPlayers.put(playerId, p);
int sessionIndex = 0;
GameSession gameSession = gameSessions.get(sessionIndex);
gameSession.addPlayer(p);
playerSessionIndex.put(playerId, sessionIndex);
}finally {
write.unlock();
}
}
public void putPlayerToMap(String playerId) {
GameSession gameSession = getGameSession(playerId);
if(gameSession == null) return;
gameSession.putPlayerToMap(playerId);
}
public void onKeyDown(String playerId, String keyVal) {
GameSession gameSession = getGameSession(playerId);
if(gameSession == null) return;
gameSession.onKeyDown(playerId, keyVal);
}
public void onKeyUp(String playerId, String keyVal) {
GameSession gameSession = getGameSession(playerId);
if(gameSession == null) return;
gameSession.onKeyUp(playerId, keyVal);
}
public void removePlayerFromMap(String playerId) {
GameSession gameSession = getGameSession(playerId);
if(gameSession == null) return;
gameSession.removePlayerFromMap(playerId);
write.lock();
try {
playerSessionIndex.remove(playerId);
allPlayers.remove(playerId);
}finally {
write.unlock();
}
}
private GameSession getGameSession(String playerId){
read.lock();
try {
Integer sessionIndex = playerSessionIndex.get(playerId);
if(sessionIndex == null) return null;
if(sessionIndex >= gameSessions.size()) return null;
return gameSessions.get(sessionIndex);
}finally {
read.unlock();
}
}
public static GameMap parseMapFromCsv(String id, File file) {
try {
List<List<Integer>> matrix = new ArrayList<>();
String str = FileUtil.readFileUTF8(file);
List<String> lines = strToList(str, "\n");
for (String line : lines) {
if( ! hasText(line) || line.startsWith("-1")) break;
String[] vals = line.trim().split(";");
List<String> cells = list(vals);
List<Integer> mLine = convert(cells, (s)->tryParseInt(s, 0));
matrix.add(mLine);
}
return new GameMap(id, matrix);
}catch(Throwable t){
log.error("can't parseMapFromCsv", t);
return null;
}
}
}
package game.api.model.game;
import static game.api.model.Const.*;
import static game.api.model.game.CellType.*;
import static och.util.Util.*;
import java.util.Arrays;
import java.util.List;
public class GameMap {
public final String id;
private final byte[][] world;
public GameMap(String id, List<List<Integer>> initWorld){
if(isEmpty(initWorld)) throw new IllegalArgumentException("initWorld is empty");
int height = initWorld.size();
int width = initWorld.get(0).size();
if(width == 0) throw new IllegalArgumentException("first line in initWorld is empty");
this.id = id;
this.world = new byte[height][width];
//fill world
for (int i = 0; i < height; i++) {
List<Integer> line = initWorld.get(i);
int minWidth = Math.min(width, line.size());
for (int j = 0; j < minWidth; j++) {
Integer cellType = line.get(j);
if(WALL.code == cellType) world[i][j] = WALL.code;
else world[i][j] = EMPTY.code;
}
}
}
public int height(){
return world != null? world.length : 0;
}
public int width(){
if(world == null)return 0;
return world.length > 0? world[0].length : 0;
}
public byte[] line(int lineIndex){
if(lineIndex < 0 || lineIndex >= world.length) return null;
return world[lineIndex];
}
public CellType getCellType(int lineIndex, int columnIndex){
byte[] line = line(lineIndex);
if(line == null) return null;
if(columnIndex < 0 || columnIndex >= line.length) return null;
int type = line[columnIndex];
return tryGetEnumByCode(type, CellType.class, null);
}
public boolean isBlocked(int x, int y) {
int lineIndex = y / cellWidth;
int columnIndex = x / cellWidth;
CellType cellType = getCellType(lineIndex, columnIndex);
if(cellType == null) return false;
return cellType != CellType.EMPTY;
}
@Override
public String toString() {
return "GameMap [id=" + id + ", world=" + Arrays.deepToString(world) + "]";
}
}
package game.app.model;
import static game.api.model.Const.*;
import game.api.model.game.GameMap;
import game.app.Player;
import game.app.model.PlayerGameState.Status;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GameSession {
private static Random r = new Random();
private GameMap map;
private List<PlayerGameState> playersState = new ArrayList<>();
private int height;
private int nextPlayerId;
public GameSession(GameMap map) {
this.map = map;
height = map.height();
}
public synchronized void addPlayer(Player p) {
int id = nextPlayerId++;
PlayerGameState state = new PlayerGameState(id, p);
playersState.add(state);
p.onAddedToGameSession(map, id);
}
public synchronized void putPlayerToMap(String playerId) {
PlayerGameState state = findState(playerId);
if(state == null) return;
if(state.status != Status.PREPARE_TO_GAME) return;
//add to map
Point p = findInsertPoint();
state.status = Status.ON_MAP;
state.setPoint(p);
}
public synchronized void nextState(){
int size = playersState.size();
if(size == 0) return;
updateAllStates();
StringBuilder sb = new StringBuilder();
PlayerGameState state;
for (int i = 0; i < size; i++) {
state = playersState.get(i);
sb.append(state.id).append(';');
sb.append(state.status.code).append(';');
sb.append(state.x()).append(';');
sb.append(state.y()).append(';');
sb.append('\n');
}
String sessionState = sb.toString();
for (int i = 0; i < size; i++) {
playersState.get(i).p.onStateUpdated(sessionState);
}
}
public synchronized void onKeyDown(String playerId, String keyVal) {
PlayerGameState state = findActiveState(playerId);
if(state == null) return;
int velocity = 10;
if(keyUp.equals(keyVal)){
state.velocityY = -velocity;
}
else if(keyDown.equals(keyVal)){
state.velocityY = velocity;
}
else if(keyLeft.equals(keyVal)){
state.velocityX = -velocity;
}
else if(keyRight.equals(keyVal)){
state.velocityX = velocity;
}
}
public synchronized void onKeyUp(String playerId, String keyVal) {
PlayerGameState state = findActiveState(playerId);
if(state == null) return;
if(keyUp.equals(keyVal)){
state.velocityY = 0;
}
else if(keyDown.equals(keyVal)){
state.velocityY = 0;
}
else if(keyLeft.equals(keyVal)){
state.velocityX = 0;
}
else if(keyRight.equals(keyVal)){
state.velocityX = 0;
}
}
public synchronized void removePlayerFromMap(String playerId) {
int size = playersState.size();
for (int i = 0; i < size; i++) {
if(playersState.get(i).p.getId().equals(playerId)){
playersState.remove(i);
break;
}
}
}
private void updateAllStates() {
double timeDelta = 1d;
int size = playersState.size();
int oldX;
int oldY;
PlayerGameState state;
for (int i = 0; i < size; i++) {
state = playersState.get(i);
oldX = state.x();
oldY = state.y();
state.updatePoint(timeDelta);
if(isBlocked(state.x(), state.y(), i)){
state.setPoint(oldX, oldY);
}
}
}
private Point findInsertPoint() {
int tryCount = 10;
while(tryCount > 0){
int yLine = r.nextInt(height);
int x = findInsertX(yLine);
if(x > -1){
int y = yLine * cellWidth;
return new Point(x, y);
}
tryCount--;
}
throw new IllegalStateException("can't findInsertPoint: "+this);
}
private int findInsertX(int lineIndex) {
int y = lineIndex * cellWidth;
int x;
byte[] line = map.line(lineIndex);
int length = line.length;
int start = r.nextInt(length);
for (int i = start; i < length; i++) {
x = i * cellWidth;
if( ! isBlocked(x, y)){
return x;
}
}
for (int i = start-1; i > -1; i--) {
x = i * cellWidth;
if( ! isBlocked(x, y)){
return x;
}
}
return -1;
}
private boolean isBlocked(int x, int y) {
return isBlocked(x, y, -1);
}
private boolean isBlocked(int x, int y, int skipStateIndex) {
if(map.isBlocked(x, y)) return true;
int size = playersState.size();
for (int i = 0; i < size; i++) {
if(i == skipStateIndex) continue;
if(playersState.get(i).isBlocked(x, y)) return true;
}
return false;
}
private PlayerGameState findActiveState(String playerId) {
PlayerGameState state = findState(playerId);
if(state == null) return null;
if(state.status != Status.ON_MAP) return null;
return state;
}
private PlayerGameState findState(String playerId) {
for (PlayerGameState state : playersState) {
if(state.p.getId().equals(playerId)) return state;
}
return null;
}
@Override
public synchronized String toString() {
return "GameSession [map=" + map + ", playersState=" + playersState
+ "]";
}
}
package game.app.model;
import static game.api.model.Const.*;
import game.app.Player;
import java.awt.Point;
import och.util.model.HasIntCode;
public class PlayerGameState {
public static enum Status implements HasIntCode {
PREPARE_TO_GAME(0),
ON_MAP(1)
;
public final int code;
private Status(int code) {
this.code = code;
}
@Override
public int getCode() {
return code;
}
}
public final int id;
public final Player p;
public Status status;
private double x;
private double y;
private int width = cellWidth;
private int height = cellWidth;
public int velocityX;
public int velocityY;
public PlayerGameState(int id, Player p) {
this.id = id;
this.p = p;
status = Status.PREPARE_TO_GAME;
}
public int x(){
return (int)x;
}
public int y(){
return (int)y;
}
public void setPoint(Point p){
setPoint(p.x, p.y);
}
public void setPoint(int x, int y){
this.x = x;
this.y = y;
}
public void updatePoint(double timeDelta) {
this.x = x + timeDelta * velocityX;
this.y = y + timeDelta * velocityY;
}
public boolean isBlocked(int x, int y) {
int thixX = x();
int thisY = y();
if(x >= thixX && x <= thixX + width){
if(y >= thisY && y <= thisY + height){
return true;
}
}
return false;
}
@Override
public String toString() {
return "[p=" + p + ", status=" + status + ", x=" + x
+ ", y=" + y + "]";
}
}
package game.app;
import game.api.model.game.GameMap;
public interface Player {
String getId();
void onAddedToGameSession(GameMap map, int playerId);
void onStateUpdated(String allStates);
}
<!DOCTYPE>
<html>
<head>
<meta charset='utf-8'/>
<link rel="stylesheet" href="css/app.css">
<script type="text/javascript" src="js/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="js/lang.js"></script>
<script type="text/javascript" src="js/util.js"></script>
<script type="text/javascript" src="js/key-util.js"></script>
<script type="text/javascript" src="js/game.js"></script>
</head>
<body>
<h3>
Players online: <span id="playersCount"></span>
</h3>
<div id="root" class="root">
</div>
<textarea id="log" style="width: 600px; height: 200px;">
</textarea>
</body>
</html>
$(function(){
new App().init();
});
var AppState = {
OFFLINE:"OFFLINE",
IN_GAME:"IN_GAME"
};
var ServerMsg = {
IN_GAME:"IN_GAME",
STATE:"STATE"
};
var ClientMsg = {
READY_TO_START:"READY_TO_START",
KEY_DOWN:"KEY_DOWN",
KEY_UP:"KEY_UP"
};
var KeyCodes = {
38: "up",
40: "dwn",
37: "lft",
39: "rht"
}
function App(){
var state = AppState.OFFLINE;
var logElem = $("#log");
logElem.text("");
var socket;
var connected = false;
var root = $("#root");
var countElem = $("#playersCount");
var playerId;
var playersElems = {};
var keysdown = {};
this.init = function(){
if( ! window.WebSocket) {
alert("Web sockets not supported by this browser!");
return;
}
socket = new WebSocket("ws://127.0.0.1:8210/gamestate");
socket.onopen = function () {
log("Connection opened");
connected = true;
};
socket.onclose = function () {
log("Connection closed");
connected = false;
};
socket.onmessage = function (event) {
try {
var msg = event.data;
if(Util.isEmptyString(msg)) return;
if(msg.startsWith(ServerMsg.IN_GAME)){
onInGame(getJsonMsg(ServerMsg.IN_GAME, msg));
return;
}
if(msg.startsWith(ServerMsg.STATE)){
onState(getMsg(ServerMsg.STATE, msg));
return;
}
log("Unknown server msg: "+msg);
}catch(e){
log("Error in server message hangler: "+Util.exceptionToString(e));
}
};
//key press
$(document).keydown(function(e){
return onKeyDown(e);
});
$(document).keyup(function(e){
return onKeyUp(e);
});
};
function onInGame(data){
if(state != AppState.OFFLINE) return;
showMap(data.map);
playerId = data.playerId;
state = AppState.IN_GAME;
socket.send(ClientMsg.READY_TO_START);
}
function onState(newState){
if(state != AppState.IN_GAME) return;
var usersStates = newState.split("\n");
if(Util.isEmptyArray(usersStates)) return;
var userState;
var x;
var y;
var id;
var status;
var elem;
var activePlayers = 0;
for (var i = 0; i < usersStates.length; i++) {
userState = usersStates[i].split(";");
if(userState.length < 4) continue;
id = userState[0];
status = userState[1];
x = userState[2];
y = userState[3];
elem = getUserElem(id);
if(status == 0) {
elem.hide();
continue;
}
activePlayers++;
elem.show();
elem.css("left", x);
elem.css("top", y);
}
countElem.text(activePlayers);
}
function onKeyDown(e){
if( ! connected) return true;
var code = KeyCodes[e.keyCode];
if( ! code) return true;
if(keysdown[code]) return false;
keysdown[code] = true;
try {
socket.send(ClientMsg.KEY_DOWN+code);
return false;
}catch(e){
log("can't onKeyDown: "+Util.exceptionToString(e));
}
return true;
}
function onKeyUp(e){
if( ! connected) return true;
var code = KeyCodes[e.keyCode];
if( ! code) return true;
if( ! keysdown[code]) return false;
keysdown[code] = false;
try {
socket.send(ClientMsg.KEY_UP+code);
return false;
}catch(e){
log("can't onKeyUp: "+Util.exceptionToString(e));
}
return true;
}
function showMap(map){
var matrix = map.world;
var table = $("<table></table>");
for (var i = 0; i < matrix.length; i++) {
var tr = $("<tr></tr>");
var line = matrix[i];
for (var j = 0; j < line.length; j++) {
var td = $("<td></td>");
var type = line[j];
td.addClass("type-"+type);
tr.append(td);
}
table.append(tr);
}
root.append(table);
}
function getUserElem(id){
var elem = playersElems[id];
if(elem) return elem;
elem = $("<div></div>");
elem.addClass("player");
elem.text(id);
if(playerId == id) elem.addClass("curPlayer");
root.append(elem);
playersElems[id] = elem;
return elem;
}
function getJsonMsg(msgType, msg){
return JSON.parse(getMsg(msgType, msg));
}
function getMsg(msgType, msg){
return msg.substr(msgType.length);
}
function log(val){
logElem.text(logElem.text()+"\n***\n"+val);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment