- add long polling support
- broadcast a message when a new user connects - utilize the long polling to deliver the message to existing clients
This commit is contained in:
parent
727f1cf3c0
commit
5aacc0ac32
|
@ -32,3 +32,7 @@
|
|||
span.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
span.debug {
|
||||
color: blue;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ cah.ajax = {};
|
|||
cah.ajax.ErrorHandlers = {};
|
||||
cah.ajax.SuccessHandlers = {};
|
||||
|
||||
cah.DEBUG = true;
|
||||
cah.LONG_POLL_TIMEOUT = 2 * 60 * 1000;
|
||||
|
||||
cah.ajax.ErrorHandlers.register = ajax_register_error;
|
||||
cah.ajax.ErrorHandlers.firstload = ajax_firstload_error;
|
||||
|
||||
|
@ -20,6 +23,8 @@ $(document).ready(function() {
|
|||
cache : false,
|
||||
error : ajaxError,
|
||||
success : ajaxDone,
|
||||
timeout : cah.DEBUG ? undefined : 10 * 1000, // 10 second timeout for normal requests
|
||||
// timeout : 1, // 10 second timeout for normal requests
|
||||
type : 'POST',
|
||||
url : '/cah/AjaxServlet'
|
||||
});
|
||||
|
@ -51,47 +56,77 @@ function addLog(text) {
|
|||
$("#log").prop("scrollTop", $("#log").prop("scrollHeight"));
|
||||
}
|
||||
|
||||
function addLogError(text) {
|
||||
addLog("<span class='error'>Error: " + text + "</span>");
|
||||
}
|
||||
|
||||
function addLogDebug(text) {
|
||||
if (cah.DEBUG) {
|
||||
addLog("<span class='debug'>DEBUG: " + text + "</span>");
|
||||
}
|
||||
}
|
||||
|
||||
function addLogDebugObject(text, obj) {
|
||||
if (cah.DEBUG) {
|
||||
if (JSON && JSON.stringify) {
|
||||
addLogDebug(text + ": " + JSON.stringify(obj));
|
||||
} else {
|
||||
addLogDebug(text + ": TODO: debug log without JSON.stringify()");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an ajax request to the server, and store that the request was sent so we know when it gets
|
||||
* responded to.
|
||||
*
|
||||
* @param op
|
||||
* {string} Operation code for the request.
|
||||
* @param data
|
||||
* {object} Parameter map to send for the request.
|
||||
* This should be used for data sent to the server, not long-polling.
|
||||
*
|
||||
* @param {string}
|
||||
* op Operation code for the request.
|
||||
* @param {object}
|
||||
* data Parameter map to send for the request.
|
||||
* @param {?function(jqXHR,textStatus,errorThrown)}
|
||||
* [opt_errback] Optional error callback.
|
||||
*/
|
||||
function ajaxRequest(op, data) {
|
||||
function ajaxRequest(op, data, opt_errback) {
|
||||
data.op = op;
|
||||
data.serial = serial++;
|
||||
$.ajax({
|
||||
var jqXHR = $.ajax({
|
||||
data : data
|
||||
});
|
||||
pendingRequests[data.serial] = data;
|
||||
addLogDebugObject("ajax req", data);
|
||||
if (opt_errback) {
|
||||
jqXHR.fail(opt_errback);
|
||||
}
|
||||
}
|
||||
|
||||
function ajaxError(jqXHR, textStatus, errorThrown) {
|
||||
// TODO deal with this somehow
|
||||
// and figure out which request it was so we can remove it from pending
|
||||
addLog("<span class='error'>Error: " + textStatus + "</span>");
|
||||
debugger;
|
||||
addLogError(textStatus);
|
||||
}
|
||||
|
||||
function ajaxDone(data) {
|
||||
addLogDebugObject("ajax done", data);
|
||||
if (data['error']) {
|
||||
// TODO cancel any timers or whatever we may have, and disable interface
|
||||
var req = pendingRequests[data.serial];
|
||||
if (req && cah.ajax.ErrorHandlers[req.op]) {
|
||||
cah.ajax.ErrorHandlers[req.op](data);
|
||||
} else {
|
||||
addLog("<span class='error'>Error: " + data.error_message + "</span>");
|
||||
addLogError(data.error_message);
|
||||
}
|
||||
} else {
|
||||
var req = pendingRequests[data.serial];
|
||||
if (req && cah.ajax.SuccessHandlers[req.op]) {
|
||||
cah.ajax.SuccessHandlers[req.op](data);
|
||||
} else if (req) {
|
||||
addLog("<span class='error'>Error: Unhandled response for op " + req.op + "</span>");
|
||||
addLogError("Unhandled response for op " + req.op);
|
||||
} else {
|
||||
addLog("<span class='error'>Error: Unknown response for serial " + data.serial + "</span>");
|
||||
addLogError("Unknown response for serial " + data.serial);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +140,8 @@ function ajax_register_success(data) {
|
|||
addLog("You are connected as " + nickname);
|
||||
$("#nickbox").hide();
|
||||
$("#canvass").show();
|
||||
|
||||
after_registered();
|
||||
}
|
||||
|
||||
function ajax_register_error(data) {
|
||||
|
@ -120,9 +157,40 @@ function ajax_firstload_success(data) {
|
|||
addLog("You have reconnected as " + nickname);
|
||||
$("#nickbox").hide();
|
||||
$("#canvass").show();
|
||||
after_registered();
|
||||
}
|
||||
}
|
||||
|
||||
function ajax_firstload_error(data) {
|
||||
|
||||
// TODO dunno what to do here
|
||||
}
|
||||
|
||||
/**
|
||||
* This should only be called after we have a valid registration with the server, as we start doing
|
||||
* long polling here.
|
||||
*/
|
||||
function after_registered() {
|
||||
addLogDebug("done registering");
|
||||
long_poll();
|
||||
}
|
||||
|
||||
function long_poll() {
|
||||
addLogDebug("starting long poll");
|
||||
$.ajax({
|
||||
complete : long_poll,
|
||||
error : long_poll_error,
|
||||
success : long_poll_done,
|
||||
timeout : cah.LONG_POLL_TIMEOUT,
|
||||
url : '/cah/LongPollServlet',
|
||||
});
|
||||
}
|
||||
|
||||
function long_poll_done(data) {
|
||||
addLogDebugObject("long poll done", data);
|
||||
}
|
||||
|
||||
function long_poll_error(jqXHR, textStatus, errorThrown) {
|
||||
// TODO deal with this somehow
|
||||
debugger;
|
||||
addLogError(textStatus);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package net.socialgamer.cah;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.annotation.WebServlet;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import net.socialgamer.cah.data.QueuedMessage;
|
||||
import net.socialgamer.cah.data.User;
|
||||
|
||||
|
||||
/**
|
||||
* Servlet implementation class LongPollServlet
|
||||
*/
|
||||
@WebServlet("/LongPollServlet")
|
||||
public class LongPollServlet extends CahServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final long TIMEOUT = 60 * 1000;
|
||||
|
||||
/**
|
||||
* @see HttpServlet#HttpServlet()
|
||||
*/
|
||||
public LongPollServlet() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
|
||||
*/
|
||||
@Override
|
||||
protected void handleRequest(final HttpServletRequest request,
|
||||
final HttpServletResponse response, final HttpSession hSession) throws ServletException,
|
||||
IOException {
|
||||
final PrintWriter out = response.getWriter();
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
final User user = (User) hSession.getAttribute("user");
|
||||
// TODO we might have to synchronize on the user object?
|
||||
while (!(user.hasQueuedMessages()) && start + TIMEOUT > System.currentTimeMillis()) {
|
||||
try {
|
||||
user.waitForNewMessageNotification(start + TIMEOUT - System.currentTimeMillis());
|
||||
} catch (final InterruptedException ie) {
|
||||
// I don't think we care?
|
||||
}
|
||||
}
|
||||
if (user.hasQueuedMessages()) {
|
||||
final QueuedMessage qm = user.getNextQueuedMessage();
|
||||
// just in case...
|
||||
if (qm != null) {
|
||||
returnData(out, qm.getData());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// otherwise, return that there is no new data
|
||||
final Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("event", "noop");
|
||||
data.put("timestamp", System.currentTimeMillis());
|
||||
returnData(out, data);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ package net.socialgamer.cah.data;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import net.socialgamer.cah.data.QueuedMessage.Type;
|
||||
|
||||
|
||||
public class ConnectedUsers {
|
||||
|
||||
|
@ -14,10 +16,23 @@ public class ConnectedUsers {
|
|||
|
||||
public void newUser(final User user) {
|
||||
// TODO fire an event for a new user to interested parties
|
||||
users.put(user.getNickname(), user);
|
||||
synchronized (users) {
|
||||
users.put(user.getNickname(), user);
|
||||
for (final User u : users.values()) {
|
||||
final Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("event", "newplayer");
|
||||
data.put("nickname", user.getNickname());
|
||||
data.put("timestamp", System.currentTimeMillis());
|
||||
final QueuedMessage qm = new QueuedMessage(Type.NEW_PLAYER, data);
|
||||
u.enqueueMessage(qm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeUser(final User user, final User.DisconnectReason reason) {
|
||||
// TODO fire an event for a disconnected user to interested parties
|
||||
synchronized (users) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package net.socialgamer.cah.data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class QueuedMessage implements Comparable<QueuedMessage> {
|
||||
|
||||
private final Type messageType;
|
||||
private final Map<String, Object> data;
|
||||
|
||||
public QueuedMessage(final Type messageType, final Map<String, Object> data) {
|
||||
this.messageType = messageType;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Type getMessageType() {
|
||||
return messageType;
|
||||
}
|
||||
|
||||
public Map<String, Object> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is not guaranteed to be consistent with .equals() since we do not care about the data for
|
||||
* ordering.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(final QueuedMessage qm) {
|
||||
return this.messageType.getWeight() - qm.messageType.getWeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Types of messages that can be queued. The numerical value is the priority that this message
|
||||
* should be delivered (lower = more important) compared to other queued messages.
|
||||
*
|
||||
* @author ajanata
|
||||
*
|
||||
*/
|
||||
public enum Type {
|
||||
PING(0), NEW_PLAYER(5);
|
||||
|
||||
private final int weight;
|
||||
|
||||
Type(final int weight) {
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package net.socialgamer.cah.data;
|
||||
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
|
||||
|
||||
public class User {
|
||||
|
||||
enum DisconnectReason {
|
||||
|
@ -8,8 +11,52 @@ public class User {
|
|||
|
||||
private final String nickname;
|
||||
|
||||
private final PriorityBlockingQueue<QueuedMessage> queuedMessages;
|
||||
|
||||
private final Object queuedMessageSynchronization = new Object();
|
||||
|
||||
public User(final String nickname) {
|
||||
this.nickname = nickname;
|
||||
queuedMessages = new PriorityBlockingQueue<QueuedMessage>();
|
||||
}
|
||||
|
||||
public void enqueueMessage(final QueuedMessage message) {
|
||||
synchronized (queuedMessageSynchronization) {
|
||||
queuedMessages.add(message);
|
||||
queuedMessageSynchronization.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasQueuedMessages() {
|
||||
return !queuedMessages.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a new message to be queued.
|
||||
*
|
||||
* @see java.lang.Object#wait(long timeout)
|
||||
* @param timeout
|
||||
* Maximum time to wait in milliseconds.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void waitForNewMessageNotification(final long timeout) throws InterruptedException {
|
||||
synchronized (queuedMessageSynchronization) {
|
||||
queuedMessageSynchronization.wait(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method blocks if there are no messages to return, or perhaps if the queue is being
|
||||
* modified by another thread.
|
||||
*
|
||||
* @return The next message in the queue, or null if interrupted.
|
||||
*/
|
||||
public QueuedMessage getNextQueuedMessage() {
|
||||
try {
|
||||
return queuedMessages.take();
|
||||
} catch (final InterruptedException ie) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import java.util.Map;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import net.socialgamer.cah.Server;
|
||||
import net.socialgamer.cah.data.ConnectedUsers;
|
||||
import net.socialgamer.cah.data.User;
|
||||
|
||||
|
@ -17,9 +18,14 @@ public class RegisterHandler implements Handler {
|
|||
|
||||
private final ConnectedUsers users;
|
||||
|
||||
// @Inject
|
||||
// public RegisterHandler(final ConnectedUsers users) {
|
||||
// this.users = users;
|
||||
// }
|
||||
|
||||
@Inject
|
||||
public RegisterHandler(final ConnectedUsers users) {
|
||||
this.users = users;
|
||||
public RegisterHandler(final Server server) {
|
||||
this.users = server.getConnectedUsers();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Loading…
Reference in New Issue