You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by kp...@apache.org on 2013/10/08 16:41:33 UTC
svn commit: r1530296 - in /tomcat/trunk/webapps: docs/
examples/WEB-INF/classes/websocket/drawboard/
examples/WEB-INF/classes/websocket/drawboard/wsmessages/ examples/websocket/
Author: kpreisser
Date: Tue Oct 8 14:41:32 2013
New Revision: 1530296
URL: http://svn.apache.org/r1530296
Log:
Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=55639
Add a Drawboard WebSocket example.
Added:
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java
tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java
tomcat/trunk/webapps/examples/websocket/drawboard.xhtml
Modified:
tomcat/trunk/webapps/docs/changelog.xml
tomcat/trunk/webapps/examples/websocket/index.xhtml
Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1530296&r1=1530295&r2=1530296&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Tue Oct 8 14:41:32 2013
@@ -260,6 +260,9 @@
the documentation web application. Patches provided by Konstantin
PreiÃer. (markt)
</update>
+ <add>
+ <bug>55639</bug>: Add a Drawboard WebSocket example. (kpreisser)
+ </add>
</changelog>
</subsection>
<subsection name="Tribes">
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java Tue Oct 8 14:41:32 2013
@@ -0,0 +1,124 @@
+/*
+ * 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.util.LinkedList;
+
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import websocket.drawboard.wsmessages.AbstractWebsocketMessage;
+import websocket.drawboard.wsmessages.BinaryWebsocketMessage;
+import websocket.drawboard.wsmessages.StringWebsocketMessage;
+
+/**
+ * Represents a client with methods to send messages.
+ */
+public class Client {
+
+ private final RemoteEndpoint.Async async;
+
+ /**
+ * Contains the messages wich are buffered until the previous
+ * send operation has finished.
+ */
+ private final LinkedList<AbstractWebsocketMessage> messagesToSend =
+ new LinkedList<>();
+ /**
+ * If this client is currently sending a messages asynchronously.
+ */
+ private volatile boolean isSendingMessage = false;
+
+ public Client(RemoteEndpoint.Async async) {
+ this.async = async;
+ }
+
+
+ /**
+ * 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
+ */
+ public void sendMessage(AbstractWebsocketMessage msg) {
+ synchronized (messagesToSend) {
+ if (isSendingMessage) {
+ // TODO: Check if the buffered messages exceed
+ // a specific amount - in that case, disconnect the client
+ // to prevent DoS.
+
+ // TODO: Check if the last message is a
+ // String message - in that case we should concatenate them
+ // to reduce TCP overhead (using ";" as separator).
+
+ messagesToSend.add(msg);
+ } else {
+ isSendingMessage = true;
+ internalSendMessageAsync(msg);
+ }
+
+
+ }
+ }
+
+ /**
+ * Internally sends the messages asynchronously.
+ * @param msg
+ */
+ 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);
+ }
+ } catch (IllegalStateException 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) {
+ synchronized (messagesToSend) {
+
+ if (!messagesToSend.isEmpty()) {
+ AbstractWebsocketMessage msg = messagesToSend.remove();
+ internalSendMessageAsync(msg);
+
+ } else {
+ isSendingMessage = false;
+ }
+
+ }
+ }
+ };
+
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java Tue Oct 8 14:41:32 2013
@@ -0,0 +1,211 @@
+/*
+ * 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;
+
+/**
+ * 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 int x1, y1, x2, y2;
+
+ /**
+ * The type. 1: Line.
+ * @return
+ */
+ 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 long getX1() {
+ return x1;
+ }
+ public void setX1(int x1) {
+ this.x1 = x1;
+ }
+ public int getX2() {
+ return x2;
+ }
+ public void setX2(int x2) {
+ this.x2 = x2;
+ }
+ public int getY1() {
+ return y1;
+ }
+ public void setY1(int y1) {
+ this.y1 = y1;
+ }
+ public int getY2() {
+ return y2;
+ }
+ public void setY2(int y2) {
+ this.y2 = y2;
+ }
+
+
+
+ public DrawMessage(int type, byte colorR, byte colorG, byte colorB,
+ byte colorA, double thickness, int x1, int x2, int y1, int 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
+ */
+ public void draw(Graphics2D g) {
+ switch (type) {
+ case 1:
+ // Draw a line.
+ g.setStroke(new BasicStroke((float) thickness,
+ BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
+ g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF,
+ colorA & 0xFF));
+ g.drawLine(x1, y1, x2, y2);
+ break;
+ }
+ }
+
+ /**
+ * 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;
+ int[] coords = new int[4];
+
+ try {
+ String[] elements = str.split(",");
+
+ type = Integer.parseInt(elements[0]);
+ if (type != 1)
+ 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] = Integer.parseInt(elements[6 + i]);
+ if (coords[i] < -1000000L || coords[i] > 1000000L)
+ 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);
+ }
+ }
+
+
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java Tue Oct 8 14:41:32 2013
@@ -0,0 +1,210 @@
+/*
+ * 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 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 final Room room = new Room();
+
+ public static Room getRoom() {
+ 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.getAsyncRemote());
+
+ room.invoke(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()));
+ }
+
+ } catch (RuntimeException ex) {
+ log.error("Unexpected exception: " + ex.toString(), ex);
+ }
+ }
+ });
+
+ }
+
+
+ @Override
+ public void onClose(Session session, CloseReason closeReason) {
+ room.invoke(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();
+ }
+
+ } 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 {
+ 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.invoke(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
+
+ boolean dontSwallowException = false;
+ try {
+ char messageType = message.charAt(0);
+ switch (messageType) {
+ case '0':
+ // Pong message.
+ // Do nothing.
+ break;
+
+ case '1':
+ // Draw message
+ int indexOfChar = message.indexOf('|');
+ long msgId = Long.parseLong(
+ message.substring(0, indexOfChar));
+
+ DrawMessage msg = DrawMessage.parseFromString(
+ message.substring(indexOfChar + 1));
+
+ // Don't ingore 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 (RuntimeException|ParseException ex) {
+ // Client sent invalid data.
+ // Ignore, TODO: maybe close connection
+ if (dontSwallowException
+ && ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ }
+
+ } catch (RuntimeException ex) {
+ log.error("Unexpected exception: " + ex.toString(), ex);
+ }
+ }
+ });
+
+ }
+ };
+
+
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java Tue Oct 8 14:41:32 2013
@@ -0,0 +1,436 @@
+/*
+ * 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.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+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>
+ *
+ * Each Room has its own "Room Thread" which manages all the actions
+ * to be done in this Room. Instance methods should only be invoked
+ * from this Room's thread by calling {@link #invoke(Runnable)} or
+ * {@link #invokeAndWait(Runnable)}.
+ */
+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 static enum MessageType {
+ /**
+ * '0': Error: contains error message.
+ */
+ ERROR('0'),
+ /**
+ * '1': DrawMesssage: 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;
+
+ private MessageType(char flag) {
+ this.flag = flag;
+ }
+
+ }
+
+
+
+ /**
+ * 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 single-threaded ExecutorService where tasks
+ * are scheduled that are to be run in the Room Thread.
+ */
+ private final ExecutorService roomExecutor =
+ Executors.newSingleThreadExecutor();
+
+ /**
+ * 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();
+
+
+ /**
+ * 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 = 2;
+
+ /**
+ * 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());
+
+ // Schedule a TimerTask that broadcasts draw messages.
+ drawmessageBroadcastTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ invokeAndWait(new Runnable() {
+ @Override
+ public void run() {
+ broadcastTimerTick();
+ }
+ });
+ } catch (InterruptedException | ExecutionException e) {
+ // TODO
+ }
+ }
+ }, 30, 30);
+ }
+
+ /**
+ * Creates a Player from the given Client and adds it to this room.
+ * @param c the client
+ * @return
+ */
+ public Player createAndAddPlayer(Client client) {
+ if (players.size() >= MAX_PLAYER_COUNT) {
+ throw new IllegalStateException("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);
+
+ // Send him 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
+ */
+ private void internalRemovePlayer(Player p) {
+ players.remove(p);
+
+ // Broadcast that one player is removed.
+ broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-");
+ }
+
+ /**
+ * @see Player#handleDrawMessage(DrawMessage, long)
+ * @param p
+ * @param msg
+ * @param msgId
+ */
+ 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.
+ * 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
+ * @param 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
+ */
+ 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
+ // lastReceuvedMessageId.
+ // 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());
+ }
+ }
+ }
+
+
+
+
+ /**
+ * Submits the given Runnable to the Room Executor.
+ * @param run
+ */
+ public void invoke(Runnable task) {
+ roomExecutor.submit(task);
+ }
+
+ /**
+ * Submits the given Runnable to the Room Executor and waits until it
+ * has been executed.
+ * @param task
+ * @throws InterruptedException if the current thread was interrupted
+ * while waiting
+ * @throws ExecutionException if the computation threw an exception
+ */
+ public void invokeAndWait(Runnable task)
+ throws InterruptedException, ExecutionException {
+ Future<?> f = roomExecutor.submit(task);
+ f.get();
+ }
+
+ /**
+ * Shuts down the roomExecutor and the drawmessageBroadcastTimer.
+ */
+ public void shutdown() {
+ roomExecutor.shutdown();
+ drawmessageBroadcastTimer.cancel();
+ }
+
+
+
+ /**
+ * 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
+ * Endpoint.
+ */
+ public 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.
+ * TODO: This should be refactored to be in a Room-Player join class
+ * as this is room-specific.
+ */
+ 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() {
+ 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 sender
+ * @param msg
+ * @param msgId
+ */
+ public void handleDrawMessage(DrawMessage msg, long msgId) {
+ room.internalHandleDrawMessage(this, msg, msgId);
+ }
+
+
+ /**
+ * Sends the given room message.
+ * @param type
+ * @param content
+ */
+ private void sendRoomMessage(MessageType type, String content) {
+ if (content == null || type == null)
+ throw null;
+
+ String completeMsg = String.valueOf(type.flag) + content;
+
+ client.sendMessage(new StringWebsocketMessage(completeMsg));
+ }
+
+
+
+ }
+
+
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/WsConfigListener.java Tue Oct 8 14:41:32 2013
@@ -0,0 +1,48 @@
+/*
+ * 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;
+import javax.servlet.annotation.WebListener;
+import javax.websocket.DeploymentException;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
+
+@WebListener
+public final class WsConfigListener implements ServletContextListener {
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+
+ ServerContainer sc =
+ (ServerContainer) sce.getServletContext().getAttribute(
+ "javax.websocket.server.ServerContainer");
+ try {
+ sc.addEndpoint(ServerEndpointConfig.Builder.create(
+ DrawboardEndpoint.class, "/websocket/drawboard").build());
+ } catch (DeploymentException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ // Shutdown our room.
+ DrawboardEndpoint.getRoom().shutdown();
+ }
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java Tue Oct 8 14:41:32 2013
@@ -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 {
+
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java Tue Oct 8 14:41:32 2013
@@ -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;
+ }
+}
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java Tue Oct 8 14:41:32 2013
@@ -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;
+ }
+
+}
Added: tomcat/trunk/webapps/examples/websocket/drawboard.xhtml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/drawboard.xhtml?rev=1530296&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/drawboard.xhtml (added)
+++ tomcat/trunk/webapps/examples/websocket/drawboard.xhtml Tue Oct 8 14:41:32 2013
@@ -0,0 +1,611 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+ <title>Apache Tomcat WebSocket Examples: Drawboard</title>
+ <style type="text/css"><![CDATA[
+
+ body {
+ font-family: Arial, sans-serif;
+ font-size: 11pt;
+ background-color: #eeeeea;
+ padding: 10px;
+ }
+
+ #console-container {
+ float: left;
+ background-color: #fff;
+ width: 250px;
+ }
+
+ #console {
+ font-size: 10pt;
+ height: 600px;
+ overflow-y: scroll;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ #console p {
+ padding: 0;
+ margin: 0;
+ }
+
+ #drawContainer {
+ float: left;
+ display: none;
+ margin-right: 25px;
+ }
+
+ #drawContainer canvas {
+ display: block;
+ -ms-touch-action: none; /* Disable touch behaviors, like pan and zoom */
+ }
+
+ #labelContainer {
+ margin-bottom: 15px;
+ }
+
+ #drawContainer, #console-container {
+ box-shadow: 0px 0px 8px 3px #bbb;
+ border: 1px solid #CCCCCC;
+
+ }
+
+ ]]></style>
+ <script type="application/javascript"><![CDATA[
+ "use strict";
+
+ (function() {
+
+ document.addEventListener("DOMContentLoaded", function() {
+ // Remove elements with "noscript" class - <noscript> is not
+ // allowed in XHTML
+ var noscripts = document.getElementsByClassName("noscript");
+ for (var i = 0; i < noscripts.length; i++) {
+ noscripts[i].parentNode.removeChild(noscripts[i]);
+ }
+
+
+ var Console = {};
+
+ Console.log = (function() {
+ var consoleContainer =
+ document.getElementById("console-container");
+ var console = document.createElement("div");
+ console.setAttribute("id", "console");
+ consoleContainer.appendChild(console);
+
+ return function(message) {
+ var p = document.createElement('p');
+ p.style.wordWrap = "break-word";
+ p.appendChild(document.createTextNode(message));
+ console.appendChild(p);
+ while (console.childNodes.length > 25) {
+ console.removeChild(console.firstChild);
+ }
+ console.scrollTop = console.scrollHeight;
+ }
+ })();
+
+
+ function Room(drawContainer) {
+
+ // The WebSocket object.
+ var socket;
+ // ID of the timer which sends ping messages.
+ var pingTimerId;
+
+ var isStarted = false;
+ var playerCount = 0;
+
+ // An array of PathIdContainer objects that the server
+ // did not yet handle.
+ // They are ordered by id (ascending).
+ var pathsNotHandled = [];
+
+ var nextMsgId = 1;
+
+ var canvasDisplay = document.createElement("canvas");
+ var canvasBackground = document.createElement("canvas");
+ var canvasServerImage = document.createElement("canvas");
+ var canvasArray = [canvasDisplay, canvasBackground,
+ canvasServerImage];
+
+ var labelPlayerCount = document.createTextNode("0");
+ var optionContainer = document.createElement("div");
+
+
+ var canvasDisplayCtx = canvasDisplay.getContext("2d");
+ var canvasBackgroundCtx = canvasBackground.getContext("2d");
+ var canvasServerImageCtx = canvasServerImage.getContext("2d");
+
+ var mouseInWindow = false;
+ var mouseDown = false;
+ var currentMouseX = 0, currentMouseY = 0;
+
+ var availableColors = [];
+ var currentColorIndex;
+ var colorContainers;
+
+ var availableThicknesses = [2, 3, 6, 10, 16, 28, 50];
+ var currentThicknessIndex;
+ var thicknessContainers;
+
+
+ var placeholder = document.createElement("div");
+ placeholder.appendChild(document.createTextNode("Loading... "));
+ var progressElem = document.createElement("progress");
+ placeholder.appendChild(progressElem);
+
+ labelContainer.appendChild(placeholder);
+
+ function rgb(color) {
+ return "rgba(" + color[0] + "," + color[1] + ","
+ + color[2] + "," + color[3] + ")";
+ }
+
+ function PathIdContainer(path, id) {
+ this.path = path;
+ this.id = id;
+ }
+
+ function Path(type, color, thickness, x1, y1, x2, y2) {
+ this.type = type;
+ this.color = color;
+ this.thickness = thickness;
+ this.x1 = x1;
+ this.y1 = y1;
+ this.x2 = x2;
+ this.y2 = y2;
+
+ this.draw = function(ctx) {
+ ctx.beginPath();
+ ctx.lineCap = "round";
+ ctx.lineWidth = thickness;
+ var style = rgb(color);
+ ctx.strokeStyle = style;
+
+ if (x1 == x2 && y1 == y2) {
+ // Always draw as arc to meet the behavior
+ // in Java2D.
+ ctx.fillStyle = style;
+ ctx.arc(x1, y1, thickness / 2.0, 0,
+ Math.PI * 2.0, false);
+ ctx.fill();
+ } else {
+ if (type == 1) {
+ // Draw a line.
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.stroke();
+ }
+ }
+ };
+ }
+
+
+ function connect() {
+ var host = (window.location.protocol == "https:"
+ ? "wss://" : "ws://") + window.location.host
+ + "/examples/websocket/drawboard";
+ socket = new WebSocket(host);
+
+ socket.onopen = function () {
+ // Socket has opened. Now wait for the server to
+ // send us the initial packet.
+ Console.log("WebSocket connection opened.");
+
+ // Set up a timer for pong messages.
+ pingTimerId = window.setInterval(function() {
+ socket.send("0");
+ }, 30000);
+ }
+
+ socket.onclose = function () {
+ Console.log("WebSocket connection closed.");
+ disableControls();
+
+ // Disable pong timer.
+ window.clearInterval(pingTimerId);
+ }
+
+ socket.onmessage = function(message) {
+
+ // Split joined message and process them
+ // invidividually.
+ var messages = message.data.split(";");
+ for (var msgArrIdx = 0; msgArrIdx < messages.length;
+ msgArrIdx++) {
+ var msg = messages[msgArrIdx];
+ var type = msg.substring(0, 1);
+
+ if (type == "0") {
+ // Error message.
+ var error = msg.substring(1);
+ // Log it to the console and show an alert.
+ Console.log("Error: " + error);
+ alert(error);
+
+ } else {
+ if (!isStarted) {
+ if (type == "2") {
+ // Initial message. It contains the
+ // number of players.
+ // After this message we will receive
+ // a binary message containing the current
+ // room image as PNG.
+ playerCount = parseInt(msg.substring(1));
+
+ refreshPlayerCount();
+
+ // The next message will be a binary
+ // message containing the room images
+ // as PNG. Therefore we temporarily swap
+ // the message handler.
+ var originalHandler = socket.onmessage;
+ socket.onmessage = function(message) {
+ // First, we restore the original handler.
+ socket.onmessage = originalHandler;
+
+ // Read the image.
+ var blob = message.data;
+ // Create new blob with correct MIME type.
+ blob = new Blob([blob], {type : "image/png"});
+
+ var url = URL.createObjectURL(blob);
+
+ var img = new Image();
+
+ // We must wait until the onload event is
+ // raised until we can draw the image onto
+ // the canvas.
+
+ // TODO: I don't know if there is a guarantee
+ // that no WebSocket events (onmessage) will
+ // be raised until the onload event of this
+ // image is raised. Maybe we need to need to
+ // push websocket messages on a queue until
+ // this onload function is called.
+ img.onload = function() {
+
+ // Release the object URL.
+ URL.revokeObjectURL(url);
+
+ // Set the canvases to the correct size.
+
+ for (var i = 0; i < canvasArray.length; i++) {
+ canvasArray[i].width = img.width;
+ canvasArray[i].height = img.height;
+ }
+
+ // Now draw the image on the last canvas.
+ canvasServerImageCtx.clearRect(0, 0,
+ canvasServerImage.width,
+ canvasServerImage.height);
+ canvasServerImageCtx.drawImage(img, 0, 0);
+
+ // Draw it on the background canvas.
+ canvasBackgroundCtx.drawImage(canvasServerImage,
+ 0, 0);
+
+ // Refresh the display canvas.
+ refreshDisplayCanvas();
+
+ isStarted = true;
+ startControls();
+ };
+
+ img.src = url;
+ };
+ }
+ } else {
+ if (type == "3") {
+ // The number of players in this room changed.
+ var playerAdded = msg.substring(1) == "+";
+ playerCount += playerAdded ? 1 : -1;
+ refreshPlayerCount();
+
+ Console.log("Player " + (playerAdded
+ ? "joined." : "left."));
+
+ } else if (type == "1") {
+ // We received a new DrawMessage.
+ var maxLastHandledId = -1;
+ var drawMessages = msg.substring(1).split("|");
+ for (var i = 0; i < drawMessages.length; i++) {
+ var elements = drawMessages[i].split(",");
+ var lastHandledId = parseInt(elements[0]);
+ maxLastHandledId = Math.max(maxLastHandledId,
+ lastHandledId);
+
+ var path = new Path(
+ parseInt(elements[1]),
+ [parseInt(elements[2]),
+ parseInt(elements[3]),
+ parseInt(elements[4]),
+ parseInt(elements[5]) / 255.0],
+ parseFloat(elements[6]),
+ parseInt(elements[7]),
+ parseInt(elements[8]),
+ parseInt(elements[9]),
+ parseInt(elements[10]));
+
+ // Draw the path onto the last canvas.
+ path.draw(canvasServerImageCtx);
+ }
+
+ // Draw the last canvas onto the background one.
+ canvasBackgroundCtx.drawImage(canvasServerImage,
+ 0, 0);
+
+ // Now go through the pathsNotHandled array and
+ // remove the paths that were already handled by
+ // the server.
+ while (pathsNotHandled.length > 0
+ && pathsNotHandled[0].id <= maxLastHandledId)
+ pathsNotHandled.shift();
+
+ // Now me must draw the remaining paths onto
+ // the background canvas.
+ for (var i = 0; i < pathsNotHandled.length; i++) {
+ pathsNotHandled[i].path.draw(canvasBackgroundCtx);
+ }
+
+ refreshDisplayCanvas();
+ }
+ }
+ }
+ }
+ };
+
+ }
+
+ function refreshPlayerCount() {
+ labelPlayerCount.nodeValue = String(playerCount);
+ }
+
+ function refreshDisplayCanvas() {
+ canvasDisplayCtx.drawImage(canvasBackground, 0, 0);
+ if (mouseInWindow && !mouseDown) {
+ canvasDisplayCtx.beginPath();
+ var color = availableColors[currentColorIndex].slice(0);
+ color[3] = 0.5;
+ canvasDisplayCtx.fillStyle = rgb(color);
+
+ canvasDisplayCtx.arc(currentMouseX, currentMouseY, availableThicknesses[currentThicknessIndex] / 2, 0, Math.PI * 2.0, true);
+
+ canvasDisplayCtx.fill();
+ }
+ }
+
+ function startControls() {
+ var labelContainer = document.getElementById("labelContainer");
+ labelContainer.removeChild(placeholder);
+ placeholder = undefined;
+
+ labelContainer.appendChild(
+ document.createTextNode("Number of Players: "));
+ labelContainer.appendChild(labelPlayerCount);
+
+
+ drawContainer.style.display = "block";
+ drawContainer.appendChild(canvasDisplay);
+
+ drawContainer.appendChild(optionContainer);
+
+ canvasDisplay.onmousemove = function(e) {
+ mouseInWindow = true;
+ var oldMouseX = currentMouseX, oldMouseY = currentMouseY;
+ currentMouseX = e.pageX - canvasDisplay.offsetLeft;
+ currentMouseY = e.pageY - canvasDisplay.offsetTop;
+
+ if (mouseDown) {
+ var path = new Path(1, availableColors[currentColorIndex],
+ availableThicknesses[currentThicknessIndex],
+ oldMouseX, oldMouseY, currentMouseX,
+ currentMouseY);
+ // Draw it on the background canvas.
+ path.draw(canvasBackgroundCtx);
+
+ // Send it to the sever.
+ pushPath(path);
+ }
+
+ refreshDisplayCanvas();
+ };
+
+ canvasDisplay.onmousedown = function(e) {
+ currentMouseX = e.pageX - canvasDisplay.offsetLeft;
+ currentMouseY = e.pageY - canvasDisplay.offsetTop;
+
+ if (e.button == 0) {
+ mouseDown = true;
+
+ var path = new Path(1, availableColors[currentColorIndex],
+ availableThicknesses[currentThicknessIndex],
+ currentMouseX, currentMouseY, currentMouseX,
+ currentMouseY);
+ // Draw it on the background canvas.
+ path.draw(canvasBackgroundCtx);
+
+ // Send it to the sever.
+ pushPath(path);
+
+ refreshDisplayCanvas();
+
+ } else if (mouseDown) {
+ // Cancel drawing.
+ mouseDown = false;
+
+ refreshDisplayCanvas();
+ }
+ }
+
+ canvasDisplay.onmouseup = function(e) {
+ if (e.button == 0) {
+ if (mouseDown) {
+ mouseDown = false;
+
+ refreshDisplayCanvas();
+ }
+ }
+ };
+
+ canvasDisplay.onmouseout = function() {
+ mouseInWindow = false;
+ refreshDisplayCanvas();
+ };
+
+
+ // Create color and thickness controls.
+ var colorContainersBox = document.createElement("div");
+ colorContainersBox.setAttribute("style",
+ "margin: 4px; border: 1px solid #bbb; border-radius: 3px;");
+ optionContainer.appendChild(colorContainersBox);
+
+
+ colorContainers = new Array(3 * 3 * 3);
+ for (var i = 0; i < colorContainers.length; i++) {
+ var colorContainer = colorContainers[i] =
+ document.createElement("div");
+ var color = availableColors[i] =
+ [
+ Math.floor((i % 3) * 255 / 2),
+ Math.floor((Math.floor(i / 3) % 3) * 255 / 2),
+ Math.floor((Math.floor(i / (3 * 3)) % 3) * 255 / 2),
+ 1.0
+ ];
+ colorContainer.setAttribute("style",
+ "margin: 3px; width: 18px; height: 18px; "
+ + "float: left; background-color: " + rgb(color));
+ colorContainer.style.border = '2px solid #000';
+ colorContainer.onmousedown = (function(ix) {
+ return function() {
+ setColor(ix);
+ };
+ })(i);
+
+ colorContainersBox.appendChild(colorContainer);
+ }
+
+ var divClearLeft = document.createElement("div");
+ divClearLeft.setAttribute("style", "clear: left;");
+ colorContainersBox.appendChild(divClearLeft);
+
+ var thicknessContainersBox = document.createElement("div");
+ thicknessContainersBox.setAttribute("style",
+ "margin: 3px; border: 1px solid #bbb; border-radius: 3px;");
+ optionContainer.appendChild(thicknessContainersBox);
+
+
+ thicknessContainers = new Array(availableThicknesses.length);
+ for (var i = 0; i < thicknessContainers.length; i++) {
+ var thicknessContainer = thicknessContainers[i] =
+ document.createElement("div");
+ thicknessContainer.setAttribute("style",
+ "text-align: center; margin: 3px; width: 18px; "
+ + "height: 18px; float: left;");
+ thicknessContainer.style.border = "2px solid #000";
+ thicknessContainer.appendChild(document.createTextNode(
+ String(availableThicknesses[i])));
+ thicknessContainer.onmousedown = (function(ix) {
+ return function() {
+ setThickness(ix);
+ };
+ })(i);
+
+ thicknessContainersBox.appendChild(thicknessContainer);
+ }
+
+ divClearLeft = document.createElement("div");
+ divClearLeft.setAttribute("style", "clear: left;");
+ thicknessContainersBox.appendChild(divClearLeft);
+
+
+ setColor(0);
+ setThickness(0);
+
+ }
+
+ function disableControls() {
+ canvasDisplay.onmousemove = canvasDisplay.onmousedown =
+ undefined;
+ mouseInWindow = false;
+ refreshDisplayCanvas();
+ }
+
+ function pushPath(path) {
+
+ // Push it into the pathsNotHandled array.
+ var container = new PathIdContainer(path, nextMsgId++);
+ pathsNotHandled.push(container);
+
+ // Send the path to the server.
+ var message = container.id + "|" + path.type + ","
+ + path.color[0] + "," + path.color[1] + ","
+ + path.color[2] + ","
+ + Math.round(path.color[3] * 255.0) + ","
+ + path.thickness + "," + path.x1 + ","
+ + path.y1 + "," + path.x2 + "," + path.y2;
+
+ socket.send("1" + message);
+ }
+
+ function setThickness(thicknessIndex) {
+ if (typeof currentThicknessIndex !== "undefined")
+ thicknessContainers[currentThicknessIndex]
+ .style.borderColor = "#000";
+ currentThicknessIndex = thicknessIndex;
+ thicknessContainers[currentThicknessIndex]
+ .style.borderColor = "#d08";
+ }
+
+ function setColor(colorIndex) {
+ if (typeof currentColorIndex !== "undefined")
+ colorContainers[currentColorIndex]
+ .style.borderColor = "#000";
+ currentColorIndex = colorIndex;
+ colorContainers[currentColorIndex]
+ .style.borderColor = "#d08";
+ }
+
+
+ connect();
+
+ }
+
+
+ // Initialize the room
+ var room = new Room(document.getElementById("drawContainer"));
+
+
+ }, false);
+
+ })();
+ ]]></script>
+</head>
+<body>
+ <div class="noscript"><h2 style="color: #ff0000;">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
+ Javascript and reload this page!</h2></div>
+ <div id="labelContainer"/>
+ <div id="drawContainer"/>
+ <div id="console-container"/>
+
+</body>
+</html>
\ No newline at end of file
Modified: tomcat/trunk/webapps/examples/websocket/index.xhtml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/index.xhtml?rev=1530296&r1=1530295&r2=1530296&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/index.xhtml (original)
+++ tomcat/trunk/webapps/examples/websocket/index.xhtml Tue Oct 8 14:41:32 2013
@@ -25,6 +25,8 @@
<li><a href="echo.xhtml">Echo example</a></li>
<li><a href="chat.xhtml">Chat example</a></li>
<li><a href="snake.xhtml">Multiplayer snake example</a></li>
+ <li><a href="drawboard.xhtml">Multiplayer drawboard example</a></li>
+
</ul>
</body>
</html>
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org