소스 파일 최초 업로드
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user