소스 파일 최초 업로드
This commit is contained in:
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 {
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 {
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user