소스 파일 최초 업로드

This commit is contained in:
ByeonJungHun
2024-04-05 10:31:45 +09:00
commit 718e6822af
682 changed files with 90848 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket;
import java.util.HashSet;
import java.util.Set;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
import websocket.drawboard.DrawboardEndpoint;
import websocket.echo.EchoEndpoint;
public class ExamplesConfig implements ServerApplicationConfig {
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(
Set<Class<? extends Endpoint>> scanned) {
Set<ServerEndpointConfig> result = new HashSet<>();
if (scanned.contains(EchoEndpoint.class)) {
result.add(ServerEndpointConfig.Builder.create(
EchoEndpoint.class,
"/websocket/echoProgrammatic").build());
}
if (scanned.contains(DrawboardEndpoint.class)) {
result.add(ServerEndpointConfig.Builder.create(
DrawboardEndpoint.class,
"/websocket/drawboard").build());
}
return result;
}
@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
// Deploy all WebSocket endpoints defined by annotations in the examples
// web application. Filter out all others to avoid issues when running
// tests on Gump
Set<Class<?>> results = new HashSet<>();
for (Class<?> clazz : scanned) {
if (clazz.getPackage().getName().startsWith("websocket.")) {
results.add(clazz);
}
}
return results;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.chat;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import util.HTMLFilter;
@ServerEndpoint(value = "/websocket/chat")
public class ChatAnnotation {
private static final Log log = LogFactory.getLog(ChatAnnotation.class);
private static final String GUEST_PREFIX = "Guest";
private static final AtomicInteger connectionIds = new AtomicInteger(0);
private static final Set<ChatAnnotation> connections =
new CopyOnWriteArraySet<>();
private final String nickname;
private Session session;
public ChatAnnotation() {
nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
}
@OnOpen
public void start(Session session) {
this.session = session;
connections.add(this);
String message = String.format("* %s %s", nickname, "has joined.");
broadcast(message);
}
@OnClose
public void end() {
connections.remove(this);
String message = String.format("* %s %s",
nickname, "has disconnected.");
broadcast(message);
}
@OnMessage
public void incoming(String message) {
// Never trust the client
String filteredMessage = String.format("%s: %s",
nickname, HTMLFilter.filter(message.toString()));
broadcast(filteredMessage);
}
@OnError
public void onError(Throwable t) throws Throwable {
log.error("Chat Error: " + t.toString(), t);
}
private static void broadcast(String msg) {
for (ChatAnnotation client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
log.debug("Chat Error: Failed to send message to client", e);
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
// Ignore
}
String message = String.format("* %s %s",
client.nickname, "has been disconnected.");
broadcast(message);
}
}
}
}

View File

@@ -0,0 +1,231 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.RemoteEndpoint.Async;
import javax.websocket.SendHandler;
import javax.websocket.SendResult;
import javax.websocket.Session;
import websocket.drawboard.wsmessages.AbstractWebsocketMessage;
import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
import websocket.drawboard.wsmessages.CloseWebsocketMessage;
import websocket.drawboard.wsmessages.StringWebsocketMessage;
/**
* Represents a client with methods to send messages asynchronously.
*/
public class Client {
private final Session session;
private final Async async;
/**
* Contains the messages which are buffered until the previous
* send operation has finished.
*/
private final Deque<AbstractWebsocketMessage> messagesToSend = new ArrayDeque<>();
/**
* If this client is currently sending a messages asynchronously.
*/
private volatile boolean isSendingMessage = false;
/**
* If this client is closing. If <code>true</code>, new messages to
* send will be ignored.
*/
private volatile boolean isClosing = false;
/**
* The length of all current buffered messages, to avoid iterating
* over a linked list.
*/
private volatile long messagesToSendLength = 0;
public Client(Session session) {
this.session = session;
this.async = session.getAsyncRemote();
}
/**
* Asynchronously closes the Websocket session. This will wait until all
* remaining messages have been sent to the Client and then close
* the Websocket session.
*/
public void close() {
sendMessage(new CloseWebsocketMessage());
}
/**
* Sends the given message asynchronously to the client.
* If there is already a async sending in progress, then the message
* will be buffered and sent when possible.<br><br>
*
* This method can be called from multiple threads.
*
* @param msg The message to send
*/
public void sendMessage(AbstractWebsocketMessage msg) {
synchronized (messagesToSend) {
if (!isClosing) {
// Check if we have a Close message
if (msg instanceof CloseWebsocketMessage) {
isClosing = true;
}
if (isSendingMessage) {
// Check if the buffered messages exceed
// a specific amount - in that case, disconnect the client
// to prevent DoS.
// In this case we check if there are >= 1000 messages
// or length(of all messages) >= 1000000 bytes.
if (messagesToSend.size() >= 1000
|| messagesToSendLength >= 1000000) {
isClosing = true;
// Discard the new message and close the session immediately.
CloseReason cr = new CloseReason(
CloseCodes.VIOLATED_POLICY,
"Send Buffer exceeded");
try {
// TODO: close() may block if the remote endpoint doesn't read the data
// (eventually there will be a TimeoutException). However, this method
// (sendMessage) is intended to run asynchronous code and shouldn't
// block. Otherwise it would temporarily stop processing of messages
// from other clients.
// Maybe call this method on another thread.
// Note that when this method is called, the RemoteEndpoint.Async
// is still in the process of sending data, so there probably should
// be another way to cancel the Websocket connection.
// Ideally, there should be some method that cancels the connection
// immediately...
session.close(cr);
} catch (IOException e) {
// Ignore
}
} else {
// Check if the last message and the new message are
// String messages - in that case we concatenate them
// to reduce TCP overhead (using ";" as separator).
if (msg instanceof StringWebsocketMessage
&& !messagesToSend.isEmpty()
&& messagesToSend.getLast()
instanceof StringWebsocketMessage) {
StringWebsocketMessage ms =
(StringWebsocketMessage) messagesToSend.removeLast();
messagesToSendLength -= calculateMessageLength(ms);
String concatenated = ms.getString() + ";" +
((StringWebsocketMessage) msg).getString();
msg = new StringWebsocketMessage(concatenated);
}
messagesToSend.add(msg);
messagesToSendLength += calculateMessageLength(msg);
}
} else {
isSendingMessage = true;
internalSendMessageAsync(msg);
}
}
}
}
private long calculateMessageLength(AbstractWebsocketMessage msg) {
if (msg instanceof BinaryWebsocketMessage) {
return ((BinaryWebsocketMessage) msg).getBytes().capacity();
} else if (msg instanceof StringWebsocketMessage) {
return ((StringWebsocketMessage) msg).getString().length() * 2;
}
return 0;
}
/**
* Internally sends the messages asynchronously.
*
* @param msg Message to send
*/
private void internalSendMessageAsync(AbstractWebsocketMessage msg) {
try {
if (msg instanceof StringWebsocketMessage) {
StringWebsocketMessage sMsg = (StringWebsocketMessage) msg;
async.sendText(sMsg.getString(), sendHandler);
} else if (msg instanceof BinaryWebsocketMessage) {
BinaryWebsocketMessage bMsg = (BinaryWebsocketMessage) msg;
async.sendBinary(bMsg.getBytes(), sendHandler);
} else if (msg instanceof CloseWebsocketMessage) {
// Close the session.
session.close();
}
} catch (IllegalStateException|IOException ex) {
// Trying to write to the client when the session has
// already been closed.
// Ignore
}
}
/**
* SendHandler that will continue to send buffered messages.
*/
private final SendHandler sendHandler = new SendHandler() {
@Override
public void onResult(SendResult result) {
if (!result.isOK()) {
// Message could not be sent. In this case, we don't
// set isSendingMessage to false because we must assume the connection
// broke (and onClose will be called), so we don't try to send
// other messages.
// As a precaution, we close the session (e.g. if a send timeout occurred).
// TODO: session.close() blocks, while this handler shouldn't block.
// Ideally, there should be some method that cancels the connection
// immediately...
try {
session.close();
} catch (IOException ex) {
// Ignore
}
}
synchronized (messagesToSend) {
if (!messagesToSend.isEmpty()) {
AbstractWebsocketMessage msg = messagesToSend.remove();
messagesToSendLength -= calculateMessageLength(msg);
internalSendMessageAsync(msg);
} else {
isSendingMessage = false;
}
}
}
};
}

View File

@@ -0,0 +1,253 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Arc2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
/**
* A message that represents a drawing action.
* Note that we use primitive types instead of Point, Color etc.
* to reduce object allocation.<br><br>
*
* TODO: But a Color objects needs to be created anyway for drawing this
* onto a Graphics2D object, so this probably does not save much.
*/
public final class DrawMessage {
private int type;
private byte colorR, colorG, colorB, colorA;
private double thickness;
private double x1, y1, x2, y2;
/**
* The type.
*
* @return 1: Brush<br>2: Line<br>3: Rectangle<br>4: Ellipse
*/
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public double getThickness() {
return thickness;
}
public void setThickness(double thickness) {
this.thickness = thickness;
}
public byte getColorR() {
return colorR;
}
public void setColorR(byte colorR) {
this.colorR = colorR;
}
public byte getColorG() {
return colorG;
}
public void setColorG(byte colorG) {
this.colorG = colorG;
}
public byte getColorB() {
return colorB;
}
public void setColorB(byte colorB) {
this.colorB = colorB;
}
public byte getColorA() {
return colorA;
}
public void setColorA(byte colorA) {
this.colorA = colorA;
}
public double getX1() {
return x1;
}
public void setX1(double x1) {
this.x1 = x1;
}
public double getX2() {
return x2;
}
public void setX2(double x2) {
this.x2 = x2;
}
public double getY1() {
return y1;
}
public void setY1(double y1) {
this.y1 = y1;
}
public double getY2() {
return y2;
}
public void setY2(double y2) {
this.y2 = y2;
}
public DrawMessage(int type, byte colorR, byte colorG, byte colorB,
byte colorA, double thickness, double x1, double x2, double y1,
double y2) {
this.type = type;
this.colorR = colorR;
this.colorG = colorG;
this.colorB = colorB;
this.colorA = colorA;
this.thickness = thickness;
this.x1 = x1;
this.x2 = x2;
this.y1 = y1;
this.y2 = y2;
}
/**
* Draws this DrawMessage onto the given Graphics2D.
*
* @param g The target for the DrawMessage
*/
public void draw(Graphics2D g) {
g.setStroke(new BasicStroke((float) thickness,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF,
colorA & 0xFF));
if (x1 == x2 && y1 == y2) {
// Always draw as arc to meet the behavior in the HTML5 Canvas.
Arc2D arc = new Arc2D.Double(x1, y1, 0, 0,
0d, 360d, Arc2D.OPEN);
g.draw(arc);
} else if (type == 1 || type == 2) {
// Draw a line.
Line2D line = new Line2D.Double(x1, y1, x2, y2);
g.draw(line);
} else if (type == 3 || type == 4) {
double x1 = this.x1, x2 = this.x2,
y1 = this.y1, y2 = this.y2;
if (x1 > x2) {
x1 = this.x2;
x2 = this.x1;
}
if (y1 > y2) {
y1 = this.y2;
y2 = this.y1;
}
// TODO: If (x1 == x2 || y1 == y2) draw as line.
if (type == 3) {
// Draw a rectangle.
Rectangle2D rect = new Rectangle2D.Double(x1, y1,
x2 - x1, y2 - y1);
g.draw(rect);
} else if (type == 4) {
// Draw an ellipse.
Arc2D arc = new Arc2D.Double(x1, y1, x2 - x1, y2 - y1,
0d, 360d, Arc2D.OPEN);
g.draw(arc);
}
}
}
/**
* Converts this message into a String representation that
* can be sent over WebSocket.<br>
* Since a DrawMessage consists only of numbers,
* we concatenate those numbers with a ",".
*/
@Override
public String toString() {
return type + "," + (colorR & 0xFF) + "," + (colorG & 0xFF) + ","
+ (colorB & 0xFF) + "," + (colorA & 0xFF) + "," + thickness
+ "," + x1 + "," + y1 + "," + x2 + "," + y2;
}
public static DrawMessage parseFromString(String str)
throws ParseException {
int type;
byte[] colors = new byte[4];
double thickness;
double[] coords = new double[4];
try {
String[] elements = str.split(",");
type = Integer.parseInt(elements[0]);
if (!(type >= 1 && type <= 4)) {
throw new ParseException("Invalid type: " + type);
}
for (int i = 0; i < colors.length; i++) {
colors[i] = (byte) Integer.parseInt(elements[1 + i]);
}
thickness = Double.parseDouble(elements[5]);
if (Double.isNaN(thickness) || thickness < 0 || thickness > 100) {
throw new ParseException("Invalid thickness: " + thickness);
}
for (int i = 0; i < coords.length; i++) {
coords[i] = Double.parseDouble(elements[6 + i]);
if (Double.isNaN(coords[i])) {
throw new ParseException("Invalid coordinate: "
+ coords[i]);
}
}
} catch (RuntimeException ex) {
throw new ParseException(ex);
}
DrawMessage m = new DrawMessage(type, colors[0], colors[1],
colors[2], colors[3], thickness, coords[0], coords[2],
coords[1], coords[3]);
return m;
}
public static class ParseException extends Exception {
private static final long serialVersionUID = -6651972769789842960L;
public ParseException(Throwable root) {
super(root);
}
public ParseException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public final class DrawboardContextListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent sce) {
// Shutdown our room.
Room room = DrawboardEndpoint.getRoom(false);
if (room != null) {
room.shutdown();
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard;
import java.io.EOFException;
import java.io.IOException;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import websocket.drawboard.DrawMessage.ParseException;
import websocket.drawboard.wsmessages.StringWebsocketMessage;
public final class DrawboardEndpoint extends Endpoint {
private static final Log log =
LogFactory.getLog(DrawboardEndpoint.class);
/**
* Our room where players can join.
*/
private static volatile Room room = null;
private static final Object roomLock = new Object();
public static Room getRoom(boolean create) {
if (create) {
if (room == null) {
synchronized (roomLock) {
if (room == null) {
room = new Room();
}
}
}
return room;
} else {
return room;
}
}
/**
* The player that is associated with this Endpoint and the current room.
* Note that this variable is only accessed from the Room Thread.<br><br>
*
* TODO: Currently, Tomcat uses an Endpoint instance once - however
* the java doc of endpoint says:
* "Each instance of a websocket endpoint is guaranteed not to be called by
* more than one thread at a time per active connection."
* This could mean that after calling onClose(), the instance
* could be reused for another connection so onOpen() will get called
* (possibly from another thread).<br>
* If this is the case, we would need a variable holder for the variables
* that are accessed by the Room thread, and read the reference to the holder
* at the beginning of onOpen, onMessage, onClose methods to ensure the room
* thread always gets the correct instance of the variable holder.
*/
private Room.Player player;
@Override
public void onOpen(Session session, EndpointConfig config) {
// Set maximum messages size to 10.000 bytes.
session.setMaxTextMessageBufferSize(10000);
session.addMessageHandler(stringHandler);
final Client client = new Client(session);
final Room room = getRoom(true);
room.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
// Create a new Player and add it to the room.
try {
player = room.createAndAddPlayer(client);
} catch (IllegalStateException ex) {
// Probably the max. number of players has been
// reached.
client.sendMessage(new StringWebsocketMessage(
"0" + ex.getLocalizedMessage()));
// Close the connection.
client.close();
}
} catch (RuntimeException ex) {
log.error("Unexpected exception: " + ex.toString(), ex);
}
}
});
}
@Override
public void onClose(Session session, CloseReason closeReason) {
Room room = getRoom(false);
if (room != null) {
room.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
// Player can be null if it couldn't enter the room
if (player != null) {
// Remove this player from the room.
player.removeFromRoom();
// Set player to null to prevent NPEs when onMessage events
// are processed (from other threads) after onClose has been
// called from different thread which closed the Websocket session.
player = null;
}
} catch (RuntimeException ex) {
log.error("Unexpected exception: " + ex.toString(), ex);
}
}
});
}
}
@Override
public void onError(Session session, Throwable t) {
// Most likely cause is a user closing their browser. Check to see if
// the root cause is EOF and if it is ignore it.
// Protect against infinite loops.
int count = 0;
Throwable root = t;
while (root.getCause() != null && count < 20) {
root = root.getCause();
count ++;
}
if (root instanceof EOFException) {
// Assume this is triggered by the user closing their browser and
// ignore it.
} else if (!session.isOpen() && root instanceof IOException) {
// IOException after close. Assume this is a variation of the user
// closing their browser (or refreshing very quickly) and ignore it.
} else {
log.error("onError: " + t.toString(), t);
}
}
private final MessageHandler.Whole<String> stringHandler =
new MessageHandler.Whole<String>() {
@Override
public void onMessage(final String message) {
// Invoke handling of the message in the room.
room.invokeAndWait(new Runnable() {
@Override
public void run() {
try {
// Currently, the only types of messages the client will send
// are draw messages prefixed by a Message ID
// (starting with char '1'), and pong messages (starting
// with char '0').
// Draw messages should look like this:
// ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2,lastInChain
boolean dontSwallowException = false;
try {
char messageType = message.charAt(0);
String messageContent = message.substring(1);
switch (messageType) {
case '0':
// Pong message.
// Do nothing.
break;
case '1':
// Draw message
int indexOfChar = messageContent.indexOf('|');
long msgId = Long.parseLong(
messageContent.substring(0, indexOfChar));
DrawMessage msg = DrawMessage.parseFromString(
messageContent.substring(indexOfChar + 1));
// Don't ignore RuntimeExceptions thrown by
// this method
// TODO: Find a better solution than this variable
dontSwallowException = true;
if (player != null) {
player.handleDrawMessage(msg, msgId);
}
dontSwallowException = false;
break;
}
} catch (ParseException e) {
// Client sent invalid data
// Ignore, TODO: maybe close connection
} catch (RuntimeException e) {
// Client sent invalid data.
// Ignore, TODO: maybe close connection
if (dontSwallowException) {
throw e;
}
}
} catch (RuntimeException ex) {
log.error("Unexpected exception: " + ex.toString(), ex);
}
}
});
}
};
}

View File

@@ -0,0 +1,497 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReentrantLock;
import javax.imageio.ImageIO;
import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
import websocket.drawboard.wsmessages.StringWebsocketMessage;
/**
* A Room represents a drawboard where a number of
* users participate.<br><br>
*
* Note: Instance methods should only be invoked by calling
* {@link #invokeAndWait(Runnable)} to ensure access is correctly synchronized.
*/
public final class Room {
/**
* Specifies the type of a room message that is sent to a client.<br>
* Note: Currently we are sending simple string messages - for production
* apps, a JSON lib should be used for object-level messages.<br><br>
*
* The number (single char) will be prefixed to the string when sending
* the message.
*/
public enum MessageType {
/**
* '0': Error: contains error message.
*/
ERROR('0'),
/**
* '1': DrawMessage: contains serialized DrawMessage(s) prefixed
* with the current Player's {@link Player#lastReceivedMessageId}
* and ",".<br>
* Multiple draw messages are concatenated with "|" as separator.
*/
DRAW_MESSAGE('1'),
/**
* '2': ImageMessage: Contains number of current players in this room.
* After this message a Binary Websocket message will follow,
* containing the current Room image as PNG.<br>
* This is the first message that a Room sends to a new Player.
*/
IMAGE_MESSAGE('2'),
/**
* '3': PlayerChanged: contains "+" or "-" which indicate a player
* was added or removed to this Room.
*/
PLAYER_CHANGED('3');
private final char flag;
MessageType(char flag) {
this.flag = flag;
}
}
/**
* The lock used to synchronize access to this Room.
*/
private final ReentrantLock roomLock = new ReentrantLock();
/**
* Indicates if this room has already been shutdown.
*/
private volatile boolean closed = false;
/**
* If <code>true</code>, outgoing DrawMessages will be buffered until the
* drawmessageBroadcastTimer ticks. Otherwise they will be sent
* immediately.
*/
private static final boolean BUFFER_DRAW_MESSAGES = true;
/**
* A timer which sends buffered drawmessages to the client at once
* at a regular interval, to avoid sending a lot of very small
* messages which would cause TCP overhead and high CPU usage.
*/
private final Timer drawmessageBroadcastTimer = new Timer();
private static final int TIMER_DELAY = 30;
/**
* The current active broadcast timer task. If null, then no Broadcast task is scheduled.
* The Task will be scheduled if the first player enters the Room, and
* cancelled if the last player exits the Room, to avoid unnecessary timer executions.
*/
private TimerTask activeBroadcastTimerTask;
/**
* The current image of the room drawboard. DrawMessages that are
* received from Players will be drawn onto this image.
*/
private final BufferedImage roomImage =
new BufferedImage(900, 600, BufferedImage.TYPE_INT_RGB);
private final Graphics2D roomGraphics = roomImage.createGraphics();
/**
* The maximum number of players that can join this room.
*/
private static final int MAX_PLAYER_COUNT = 100;
/**
* List of all currently joined players.
*/
private final List<Player> players = new ArrayList<>();
public Room() {
roomGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Clear the image with white background.
roomGraphics.setBackground(Color.WHITE);
roomGraphics.clearRect(0, 0, roomImage.getWidth(),
roomImage.getHeight());
}
private TimerTask createBroadcastTimerTask() {
return new TimerTask() {
@Override
public void run() {
invokeAndWait(new Runnable() {
@Override
public void run() {
broadcastTimerTick();
}
});
}
};
}
/**
* Creates a Player from the given Client and adds it to this room.
*
* @param client the client
*
* @return The newly created player
*/
public Player createAndAddPlayer(Client client) {
if (players.size() >= MAX_PLAYER_COUNT) {
throw new IllegalStateException("Maximum player count ("
+ MAX_PLAYER_COUNT + ") has been reached.");
}
Player p = new Player(this, client);
// Broadcast to the other players that one player joined.
broadcastRoomMessage(MessageType.PLAYER_CHANGED, "+");
// Add the new player to the list.
players.add(p);
// If currently no Broadcast Timer Task is scheduled, then we need to create one.
if (activeBroadcastTimerTask == null) {
activeBroadcastTimerTask = createBroadcastTimerTask();
drawmessageBroadcastTimer.schedule(activeBroadcastTimerTask,
TIMER_DELAY, TIMER_DELAY);
}
// Send the current number of players and the current room image.
String content = String.valueOf(players.size());
p.sendRoomMessage(MessageType.IMAGE_MESSAGE, content);
// Store image as PNG
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
ImageIO.write(roomImage, "PNG", bout);
} catch (IOException e) { /* Should never happen */ }
// Send the image as binary message.
BinaryWebsocketMessage msg = new BinaryWebsocketMessage(
ByteBuffer.wrap(bout.toByteArray()));
p.getClient().sendMessage(msg);
return p;
}
/**
* @see Player#removeFromRoom()
* @param p player to remove
*/
private void internalRemovePlayer(Player p) {
boolean removed = players.remove(p);
assert removed;
// If the last player left the Room, we need to cancel the Broadcast Timer Task.
if (players.size() == 0) {
// Cancel the task.
// Note that it can happen that the TimerTask is just about to execute (from
// the Timer thread) but waits until all players are gone (or even until a new
// player is added to the list), and then executes. This is OK. To prevent it,
// a TimerTask subclass would need to have some boolean "cancel" instance variable and
// query it in the invocation of Room#invokeAndWait.
activeBroadcastTimerTask.cancel();
activeBroadcastTimerTask = null;
}
// Broadcast that one player is removed.
broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-");
}
/**
* @see Player#handleDrawMessage(DrawMessage, long)
* @param p player
* @param msg message containing details of new shapes to draw
* @param msgId message ID
*/
private void internalHandleDrawMessage(Player p, DrawMessage msg,
long msgId) {
p.setLastReceivedMessageId(msgId);
// Draw the RoomMessage onto our Room Image.
msg.draw(roomGraphics);
// Broadcast the Draw Message.
broadcastDrawMessage(msg);
}
/**
* Broadcasts the given drawboard message to all connected players.<br>
* Note: For DrawMessages, please use
* {@link #broadcastDrawMessage(DrawMessage)}
* as this method will buffer them and prefix them with the correct
* last received Message ID.
* @param type message type
* @param content message content
*/
private void broadcastRoomMessage(MessageType type, String content) {
for (Player p : players) {
p.sendRoomMessage(type, content);
}
}
/**
* Broadcast the given DrawMessage. This will buffer the message
* and the {@link #drawmessageBroadcastTimer} will broadcast them
* at a regular interval, prefixing them with the player's current
* {@link Player#lastReceivedMessageId}.
* @param msg message to broadcast
*/
private void broadcastDrawMessage(DrawMessage msg) {
if (!BUFFER_DRAW_MESSAGES) {
String msgStr = msg.toString();
for (Player p : players) {
String s = String.valueOf(p.getLastReceivedMessageId())
+ "," + msgStr;
p.sendRoomMessage(MessageType.DRAW_MESSAGE, s);
}
} else {
for (Player p : players) {
p.getBufferedDrawMessages().add(msg);
}
}
}
/**
* Tick handler for the broadcastTimer.
*/
private void broadcastTimerTick() {
// For each Player, send all per Player buffered
// DrawMessages, prefixing each DrawMessage with the player's
// lastReceivedMessageId.
// Multiple messages are concatenated with "|".
for (Player p : players) {
StringBuilder sb = new StringBuilder();
List<DrawMessage> drawMessages = p.getBufferedDrawMessages();
if (drawMessages.size() > 0) {
for (int i = 0; i < drawMessages.size(); i++) {
DrawMessage msg = drawMessages.get(i);
String s = String.valueOf(p.getLastReceivedMessageId())
+ "," + msg.toString();
if (i > 0) {
sb.append('|');
}
sb.append(s);
}
drawMessages.clear();
p.sendRoomMessage(MessageType.DRAW_MESSAGE, sb.toString());
}
}
}
/**
* A list of cached {@link Runnable}s to prevent recursive invocation of Runnables
* by one thread. This variable is only used by one thread at a time and then
* set to <code>null</code>.
*/
private List<Runnable> cachedRunnables = null;
/**
* Submits the given Runnable to the Room Executor and waits until it
* has been executed. Currently, this simply means that the Runnable
* will be run directly inside of a synchronized() block.<br>
* Note that if a runnable recursively calls invokeAndWait() with another
* runnable on this Room, it will not be executed recursively, but instead
* cached until the original runnable is finished, to keep the behavior of
* using an Executor.
*
* @param task The task to be executed
*/
public void invokeAndWait(Runnable task) {
// Check if the current thread already holds a lock on this room.
// If yes, then we must not directly execute the Runnable but instead
// cache it until the original invokeAndWait() has finished.
if (roomLock.isHeldByCurrentThread()) {
if (cachedRunnables == null) {
cachedRunnables = new ArrayList<>();
}
cachedRunnables.add(task);
} else {
roomLock.lock();
try {
// Explicitly overwrite value to ensure data consistency in
// current thread
cachedRunnables = null;
if (!closed) {
task.run();
}
// Run the cached runnables.
if (cachedRunnables != null) {
for (Runnable cachedRunnable : cachedRunnables) {
if (!closed) {
cachedRunnable.run();
}
}
cachedRunnables = null;
}
} finally {
roomLock.unlock();
}
}
}
/**
* Shuts down the roomExecutor and the drawmessageBroadcastTimer.
*/
public void shutdown() {
invokeAndWait(new Runnable() {
@Override
public void run() {
closed = true;
drawmessageBroadcastTimer.cancel();
roomGraphics.dispose();
}
});
}
/**
* A Player participates in a Room. It is the interface between the
* {@link Room} and the {@link Client}.<br><br>
*
* Note: This means a player object is actually a join between Room and
* Client.
*/
public static final class Player {
/**
* The room to which this player belongs.
*/
private Room room;
/**
* The room buffers the last draw message ID that was received from
* this player.
*/
private long lastReceivedMessageId = 0;
private final Client client;
/**
* Buffered DrawMessages that will be sent by a Timer.
*/
private final List<DrawMessage> bufferedDrawMessages =
new ArrayList<>();
private List<DrawMessage> getBufferedDrawMessages() {
return bufferedDrawMessages;
}
private Player(Room room, Client client) {
this.room = room;
this.client = client;
}
public Room getRoom() {
return room;
}
public Client getClient() {
return client;
}
/**
* Removes this player from its room, e.g. when
* the client disconnects.
*/
public void removeFromRoom() {
if (room != null) {
room.internalRemovePlayer(this);
room = null;
}
}
private long getLastReceivedMessageId() {
return lastReceivedMessageId;
}
private void setLastReceivedMessageId(long value) {
lastReceivedMessageId = value;
}
/**
* Handles the given DrawMessage by drawing it onto this Room's
* image and by broadcasting it to the connected players.
*
* @param msg The draw message received
* @param msgId The ID for the draw message received
*/
public void handleDrawMessage(DrawMessage msg, long msgId) {
room.internalHandleDrawMessage(this, msg, msgId);
}
/**
* Sends the given room message.
* @param type message type
* @param content message content
*/
private void sendRoomMessage(MessageType type, String content) {
Objects.requireNonNull(content);
Objects.requireNonNull(type);
String completeMsg = String.valueOf(type.flag) + content;
client.sendMessage(new StringWebsocketMessage(completeMsg));
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard.wsmessages;
/**
* Abstract base class for Websocket Messages (binary or string)
* that can be buffered.
*/
public abstract class AbstractWebsocketMessage {
}

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard.wsmessages;
import java.nio.ByteBuffer;
/**
* Represents a binary websocket message.
*/
public final class BinaryWebsocketMessage extends AbstractWebsocketMessage {
private final ByteBuffer bytes;
public BinaryWebsocketMessage(ByteBuffer bytes) {
this.bytes = bytes;
}
public ByteBuffer getBytes() {
return bytes;
}
}

View File

@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard.wsmessages;
/**
* Represents a "close" message that closes the session.
*/
public class CloseWebsocketMessage extends AbstractWebsocketMessage {
}

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.drawboard.wsmessages;
/**
* Represents a string websocket message.
*
*/
public final class StringWebsocketMessage extends AbstractWebsocketMessage {
private final String string;
public StringWebsocketMessage(String string) {
this.string = string;
}
public String getString() {
return string;
}
}

View File

@@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.echo;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.websocket.OnMessage;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/**
* The three annotated echo endpoints can be used to test with Autobahn and
* the following command "wstest -m fuzzingclient -s servers.json". See the
* Autobahn documentation for setup and general information.
*/
@ServerEndpoint("/websocket/echoAnnotation")
public class EchoAnnotation {
@OnMessage
public void echoTextMessage(Session session, String msg, boolean last) {
try {
if (session.isOpen()) {
session.getBasicRemote().sendText(msg, last);
}
} catch (IOException e) {
try {
session.close();
} catch (IOException e1) {
// Ignore
}
}
}
@OnMessage
public void echoBinaryMessage(Session session, ByteBuffer bb,
boolean last) {
try {
if (session.isOpen()) {
session.getBasicRemote().sendBinary(bb, last);
}
} catch (IOException e) {
try {
session.close();
} catch (IOException e1) {
// Ignore
}
}
}
/**
* Process a received pong. This is a NO-OP.
*
* @param pm Ignored.
*/
@OnMessage
public void echoPongMessage(PongMessage pm) {
// NO-OP
}
}

View File

@@ -0,0 +1,128 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.echo;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.websocket.OnMessage;
import javax.websocket.PongMessage;
import javax.websocket.Session;
/**
* The three annotated echo endpoints can be used to test with Autobahn and
* the following command "wstest -m fuzzingclient -s servers.json". See the
* Autobahn documentation for setup and general information.
*
* Note: This one is disabled by default since it allocates memory, and needs
* to be enabled back.
*/
//@javax.websocket.server.ServerEndpoint("/websocket/echoAsyncAnnotation")
public class EchoAsyncAnnotation {
private static final Future<Void> COMPLETED = new CompletedFuture();
Future<Void> f = COMPLETED;
StringBuilder sb = null;
ByteArrayOutputStream bytes = null;
@OnMessage
public void echoTextMessage(Session session, String msg, boolean last) {
if (sb == null) {
sb = new StringBuilder();
}
sb.append(msg);
if (last) {
// Before we send the next message, have to wait for the previous
// message to complete
try {
f.get();
} catch (InterruptedException | ExecutionException e) {
// Let the container deal with it
throw new RuntimeException(e);
}
f = session.getAsyncRemote().sendText(sb.toString());
sb = null;
}
}
@OnMessage
public void echoBinaryMessage(byte[] msg, Session session, boolean last)
throws IOException {
if (bytes == null) {
bytes = new ByteArrayOutputStream();
}
bytes.write(msg);
if (last) {
// Before we send the next message, have to wait for the previous
// message to complete
try {
f.get();
} catch (InterruptedException | ExecutionException e) {
// Let the container deal with it
throw new RuntimeException(e);
}
f = session.getAsyncRemote().sendBinary(ByteBuffer.wrap(bytes.toByteArray()));
bytes = null;
}
}
/**
* Process a received pong. This is a NO-OP.
*
* @param pm Ignored.
*/
@OnMessage
public void echoPongMessage(PongMessage pm) {
// NO-OP
}
private static class CompletedFuture implements Future<Void> {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return true;
}
@Override
public Void get() throws InterruptedException, ExecutionException {
return null;
}
@Override
public Void get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException,
TimeoutException {
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.echo;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
public class EchoEndpoint extends Endpoint {
@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
RemoteEndpoint.Basic remoteEndpointBasic = session.getBasicRemote();
session.addMessageHandler(new EchoMessageHandlerText(remoteEndpointBasic));
session.addMessageHandler(new EchoMessageHandlerBinary(remoteEndpointBasic));
}
private static class EchoMessageHandlerText
implements MessageHandler.Partial<String> {
private final RemoteEndpoint.Basic remoteEndpointBasic;
private EchoMessageHandlerText(RemoteEndpoint.Basic remoteEndpointBasic) {
this.remoteEndpointBasic = remoteEndpointBasic;
}
@Override
public void onMessage(String message, boolean last) {
try {
if (remoteEndpointBasic != null) {
remoteEndpointBasic.sendText(message, last);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private static class EchoMessageHandlerBinary
implements MessageHandler.Partial<ByteBuffer> {
private final RemoteEndpoint.Basic remoteEndpointBasic;
private EchoMessageHandlerBinary(RemoteEndpoint.Basic remoteEndpointBasic) {
this.remoteEndpointBasic = remoteEndpointBasic;
}
@Override
public void onMessage(ByteBuffer message, boolean last) {
try {
if (remoteEndpointBasic != null) {
remoteEndpointBasic.sendBinary(message, last);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.echo;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import javax.websocket.OnMessage;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/**
* The three annotated echo endpoints can be used to test with Autobahn and
* the following command "wstest -m fuzzingclient -s servers.json". See the
* Autobahn documentation for setup and general information.
*/
@ServerEndpoint("/websocket/echoStreamAnnotation")
public class EchoStreamAnnotation {
Writer writer;
OutputStream stream;
@OnMessage
public void echoTextMessage(Session session, String msg, boolean last)
throws IOException {
if (writer == null) {
writer = session.getBasicRemote().getSendWriter();
}
writer.write(msg);
if (last) {
writer.close();
writer = null;
}
}
@OnMessage
public void echoBinaryMessage(byte[] msg, Session session, boolean last)
throws IOException {
if (stream == null) {
stream = session.getBasicRemote().getSendStream();
}
stream.write(msg);
stream.flush();
if (last) {
stream.close();
stream = null;
}
}
/**
* Process a received pong. This is a NO-OP.
*
* @param pm Ignored.
*/
@OnMessage
public void echoPongMessage(PongMessage pm) {
// NO-OP
}
}

View File

@@ -0,0 +1,20 @@
{
"options": {"failByDrop": false},
"outdir": "./reports/servers",
"servers": [
{"agent": "Basic",
"url": "ws://localhost:8080/examples/websocket/echoAnnotation",
"options": {"version": 18}},
{"agent": "Stream",
"url": "ws://localhost:8080/examples/websocket/echoStreamAnnotation",
"options": {"version": 18}},
{"agent": "Async",
"url": "ws://localhost:8080/examples/websocket/echoAsyncAnnotation",
"options": {"version": 18}}
],
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}

View File

@@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.snake;
public enum Direction {
NONE, NORTH, SOUTH, EAST, WEST
}

View File

@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.snake;
public class Location {
public int x;
public int y;
public Location(int x, int y) {
this.x = x;
this.y = y;
}
public Location getAdjacentLocation(Direction direction) {
switch (direction) {
case NORTH:
return new Location(x, y - SnakeAnnotation.GRID_SIZE);
case SOUTH:
return new Location(x, y + SnakeAnnotation.GRID_SIZE);
case EAST:
return new Location(x + SnakeAnnotation.GRID_SIZE, y);
case WEST:
return new Location(x - SnakeAnnotation.GRID_SIZE, y);
case NONE:
// fall through
default:
return this;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Location location = (Location) o;
if (x != location.x) {
return false;
}
if (y != location.y) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.snake;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.Session;
public class Snake {
private static final int DEFAULT_LENGTH = 5;
private final int id;
private final Session session;
private Direction direction;
private int length = DEFAULT_LENGTH;
private Location head;
private final Deque<Location> tail = new ArrayDeque<>();
private final String hexColor;
public Snake(int id, Session session) {
this.id = id;
this.session = session;
this.hexColor = SnakeAnnotation.getRandomHexColor();
resetState();
}
private void resetState() {
this.direction = Direction.NONE;
this.head = SnakeAnnotation.getRandomLocation();
this.tail.clear();
this.length = DEFAULT_LENGTH;
}
private synchronized void kill() {
resetState();
sendMessage("{\"type\": \"dead\"}");
}
private synchronized void reward() {
length++;
sendMessage("{\"type\": \"kill\"}");
}
protected void sendMessage(String msg) {
try {
session.getBasicRemote().sendText(msg);
} catch (IOException ioe) {
CloseReason cr =
new CloseReason(CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage());
try {
session.close(cr);
} catch (IOException ioe2) {
// Ignore
}
}
}
public synchronized void update(Collection<Snake> snakes) {
Location nextLocation = head.getAdjacentLocation(direction);
if (nextLocation.x >= SnakeAnnotation.PLAYFIELD_WIDTH) {
nextLocation.x = 0;
}
if (nextLocation.y >= SnakeAnnotation.PLAYFIELD_HEIGHT) {
nextLocation.y = 0;
}
if (nextLocation.x < 0) {
nextLocation.x = SnakeAnnotation.PLAYFIELD_WIDTH;
}
if (nextLocation.y < 0) {
nextLocation.y = SnakeAnnotation.PLAYFIELD_HEIGHT;
}
if (direction != Direction.NONE) {
tail.addFirst(head);
if (tail.size() > length) {
tail.removeLast();
}
head = nextLocation;
}
handleCollisions(snakes);
}
private void handleCollisions(Collection<Snake> snakes) {
for (Snake snake : snakes) {
boolean headCollision = id != snake.id && snake.getHead().equals(head);
boolean tailCollision = snake.getTail().contains(head);
if (headCollision || tailCollision) {
kill();
if (id != snake.id) {
snake.reward();
}
}
}
}
public synchronized Location getHead() {
return head;
}
public synchronized Collection<Location> getTail() {
return tail;
}
public synchronized void setDirection(Direction direction) {
this.direction = direction;
}
public synchronized String getLocationsJson() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("{\"x\": %d, \"y\": %d}",
Integer.valueOf(head.x), Integer.valueOf(head.y)));
for (Location location : tail) {
sb.append(',');
sb.append(String.format("{\"x\": %d, \"y\": %d}",
Integer.valueOf(location.x), Integer.valueOf(location.y)));
}
return String.format("{\"id\":%d,\"body\":[%s]}",
Integer.valueOf(id), sb.toString());
}
public int getId() {
return id;
}
public String getHexColor() {
return hexColor;
}
}

View File

@@ -0,0 +1,135 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.snake;
import java.awt.Color;
import java.io.EOFException;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/websocket/snake")
public class SnakeAnnotation {
public static final int PLAYFIELD_WIDTH = 640;
public static final int PLAYFIELD_HEIGHT = 480;
public static final int GRID_SIZE = 10;
private static final AtomicInteger snakeIds = new AtomicInteger(0);
private static final Random random = new Random();
private final int id;
private Snake snake;
public static String getRandomHexColor() {
float hue = random.nextFloat();
// sat between 0.1 and 0.3
float saturation = (random.nextInt(2000) + 1000) / 10000f;
float luminance = 0.9f;
Color color = Color.getHSBColor(hue, saturation, luminance);
return '#' + Integer.toHexString(
(color.getRGB() & 0xffffff) | 0x1000000).substring(1);
}
public static Location getRandomLocation() {
int x = roundByGridSize(random.nextInt(PLAYFIELD_WIDTH));
int y = roundByGridSize(random.nextInt(PLAYFIELD_HEIGHT));
return new Location(x, y);
}
private static int roundByGridSize(int value) {
value = value + (GRID_SIZE / 2);
value = value / GRID_SIZE;
value = value * GRID_SIZE;
return value;
}
public SnakeAnnotation() {
this.id = snakeIds.getAndIncrement();
}
@OnOpen
public void onOpen(Session session) {
this.snake = new Snake(id, session);
SnakeTimer.addSnake(snake);
StringBuilder sb = new StringBuilder();
for (Iterator<Snake> iterator = SnakeTimer.getSnakes().iterator();
iterator.hasNext();) {
Snake snake = iterator.next();
sb.append(String.format("{\"id\": %d, \"color\": \"%s\"}",
Integer.valueOf(snake.getId()), snake.getHexColor()));
if (iterator.hasNext()) {
sb.append(',');
}
}
SnakeTimer.broadcast(String.format("{\"type\": \"join\",\"data\":[%s]}",
sb.toString()));
}
@OnMessage
public void onTextMessage(String message) {
if ("west".equals(message)) {
snake.setDirection(Direction.WEST);
} else if ("north".equals(message)) {
snake.setDirection(Direction.NORTH);
} else if ("east".equals(message)) {
snake.setDirection(Direction.EAST);
} else if ("south".equals(message)) {
snake.setDirection(Direction.SOUTH);
}
}
@OnClose
public void onClose() {
SnakeTimer.removeSnake(snake);
SnakeTimer.broadcast(String.format("{\"type\": \"leave\", \"id\": %d}",
Integer.valueOf(id)));
}
@OnError
public void onError(Throwable t) throws Throwable {
// Most likely cause is a user closing their browser. Check to see if
// the root cause is EOF and if it is ignore it.
// Protect against infinite loops.
int count = 0;
Throwable root = t;
while (root.getCause() != null && count < 20) {
root = root.getCause();
count ++;
}
if (root instanceof EOFException) {
// Assume this is triggered by the user closing their browser and
// ignore it.
} else {
throw t;
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package websocket.snake;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
/**
* Sets up the timer for the multi-player snake game WebSocket example.
*/
public class SnakeTimer {
private static final Log log =
LogFactory.getLog(SnakeTimer.class);
private static Timer gameTimer = null;
private static final long TICK_DELAY = 100;
private static final ConcurrentHashMap<Integer, Snake> snakes =
new ConcurrentHashMap<>();
protected static synchronized void addSnake(Snake snake) {
if (snakes.size() == 0) {
startTimer();
}
snakes.put(Integer.valueOf(snake.getId()), snake);
}
protected static Collection<Snake> getSnakes() {
return Collections.unmodifiableCollection(snakes.values());
}
protected static synchronized void removeSnake(Snake snake) {
snakes.remove(Integer.valueOf(snake.getId()));
if (snakes.size() == 0) {
stopTimer();
}
}
protected static void tick() {
StringBuilder sb = new StringBuilder();
for (Iterator<Snake> iterator = SnakeTimer.getSnakes().iterator();
iterator.hasNext();) {
Snake snake = iterator.next();
snake.update(SnakeTimer.getSnakes());
sb.append(snake.getLocationsJson());
if (iterator.hasNext()) {
sb.append(',');
}
}
broadcast(String.format("{\"type\": \"update\", \"data\" : [%s]}",
sb.toString()));
}
protected static void broadcast(String message) {
for (Snake snake : SnakeTimer.getSnakes()) {
try {
snake.sendMessage(message);
} catch (IllegalStateException ise) {
// An ISE can occur if an attempt is made to write to a
// WebSocket connection after it has been closed. The
// alternative to catching this exception is to synchronise
// the writes to the clients along with the addSnake() and
// removeSnake() methods that are already synchronised.
}
}
}
public static void startTimer() {
gameTimer = new Timer(SnakeTimer.class.getSimpleName() + " Timer");
gameTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
tick();
} catch (RuntimeException e) {
log.error("Caught to prevent timer from shutting down", e);
}
}
}, TICK_DELAY, TICK_DELAY);
}
public static void stopTimer() {
if (gameTimer != null) {
gameTimer.cancel();
}
}
}