org.cometd.server.transport.LongPollingTransport Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2014 the original author or authors.
*
* Licensed 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 org.cometd.server.transport;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.AbstractServerTransport;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.ServerSessionImpl;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationListener;
import org.eclipse.jetty.continuation.ContinuationSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/* ------------------------------------------------------------ */
/**
* Abstract Long Polling Transport.
*
* Transports based on this class can be configured with servlet init parameters:
*
* - browserId
- The Cookie name used to save a browser ID.
* - maxSessionsPerBrowser
- The maximum number of long polling sessions allowed per browser.
* - multiSessionInterval
- The polling interval to use once max session per browser is exceeded.
* - autoBatch
- If true a batch will be automatically created to span the handling of messages received from a session.
* - allowMultiSessionsNoBrowser
- Allows multiple sessions even when the browser identifier cannot be retrieved.
*
*/
public abstract class LongPollingTransport extends HttpTransport
{
public final static String PREFIX = "long-polling";
public final static String BROWSER_ID_OPTION = "browserId";
public final static String BROWSER_COOKIE_NAME_OPTION = "browserCookieName";
public final static String BROWSER_COOKIE_DOMAIN_OPTION = "browserCookieDomain";
public final static String BROWSER_COOKIE_PATH_OPTION = "browserCookiePath";
public final static String MAX_SESSIONS_PER_BROWSER_OPTION = "maxSessionsPerBrowser";
public final static String MULTI_SESSION_INTERVAL_OPTION = "multiSessionInterval";
public final static String AUTOBATCH_OPTION = "autoBatch";
public final static String ALLOW_MULTI_SESSIONS_NO_BROWSER_OPTION = "allowMultiSessionsNoBrowser";
private final Logger _logger = LoggerFactory.getLogger(getClass());
private final ConcurrentHashMap _browserMap = new ConcurrentHashMap();
private final Map _browserSweep = new ConcurrentHashMap();
private String _browserCookieName;
private String _browserCookieDomain;
private String _browserCookiePath;
private int _maxSessionsPerBrowser;
private long _multiSessionInterval;
private boolean _autoBatch;
private boolean _allowMultiSessionsNoBrowser;
private long _lastSweep;
protected LongPollingTransport(BayeuxServerImpl bayeux, String name)
{
super(bayeux, name);
setOptionPrefix(PREFIX);
}
@Override
protected void init()
{
super.init();
_browserCookieName = getOption(BROWSER_COOKIE_NAME_OPTION, getOption(BROWSER_ID_OPTION, "BAYEUX_BROWSER"));
_browserCookieDomain = getOption(BROWSER_COOKIE_DOMAIN_OPTION, null);
_browserCookiePath = getOption(BROWSER_COOKIE_PATH_OPTION, "/");
_maxSessionsPerBrowser = getOption(MAX_SESSIONS_PER_BROWSER_OPTION, 1);
_multiSessionInterval = getOption(MULTI_SESSION_INTERVAL_OPTION, 2000);
_autoBatch = getOption(AUTOBATCH_OPTION, true);
_allowMultiSessionsNoBrowser = getOption(ALLOW_MULTI_SESSIONS_NO_BROWSER_OPTION, false);
}
protected String findBrowserId(HttpServletRequest request)
{
Cookie[] cookies = request.getCookies();
if (cookies != null)
{
for (Cookie cookie : cookies)
{
if (_browserCookieName.equals(cookie.getName()))
return cookie.getValue();
}
}
return null;
}
protected String setBrowserId(HttpServletRequest request, HttpServletResponse response)
{
String browserId = Long.toHexString(request.getRemotePort()) +
Long.toString(getBayeux().randomLong(), 36) +
Long.toString(System.currentTimeMillis(), 36) +
Long.toString(request.getRemotePort(), 36);
Cookie cookie = new Cookie(_browserCookieName, browserId);
if (_browserCookieDomain != null)
cookie.setDomain(_browserCookieDomain);
cookie.setPath(_browserCookiePath);
cookie.setMaxAge(-1);
response.addCookie(cookie);
return browserId;
}
/**
* Increment the browser ID count.
*
* @param browserId the browser ID to increment the count for
* @return true if the browser ID count is below the max sessions per browser value.
* If false is returned, the count is not incremented.
*/
protected boolean incBrowserId(String browserId)
{
if (_maxSessionsPerBrowser < 0)
return true;
if (_maxSessionsPerBrowser == 0)
return false;
AtomicInteger count = _browserMap.get(browserId);
if (count == null)
{
AtomicInteger newCount = new AtomicInteger();
count = _browserMap.putIfAbsent(browserId, newCount);
if (count == null)
count = newCount;
}
// Increment
int sessions = count.incrementAndGet();
// If was zero, remove from the sweep
if (sessions == 1)
_browserSweep.remove(browserId);
// TODO, the maxSessionsPerBrowser should be parameterized on user-agent
if (sessions > _maxSessionsPerBrowser)
{
count.decrementAndGet();
return false;
}
return true;
}
protected void decBrowserId(String browserId)
{
if (browserId == null)
return;
AtomicInteger count = _browserMap.get(browserId);
if (count != null && count.decrementAndGet() == 0)
{
_browserSweep.put(browserId, new AtomicInteger(0));
}
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
LongPollScheduler scheduler = (LongPollScheduler)request.getAttribute(LongPollScheduler.ATTRIBUTE);
if (scheduler == null)
{
// Not a resumed /meta/connect, process messages.
try
{
ServerMessage.Mutable[] messages = parseMessages(request);
processMessages(request, response, messages);
}
catch (ParseException x)
{
handleJSONParseException(request, response, x.getMessage(), x.getCause());
}
}
else
{
resume(request, response, scheduler.getSession(), scheduler.getReply());
}
}
protected void processMessages(HttpServletRequest request, HttpServletResponse response, ServerMessage.Mutable[] messages) throws IOException
{
boolean autoBatch = _autoBatch;
ServerSessionImpl session = null;
boolean batch = false;
boolean startInterval = false;
boolean suspended = false;
boolean disconnected = false;
try
{
for (int i = 0; i < messages.length; ++i)
{
ServerMessage.Mutable message = messages[i];
_logger.debug("Processing {}", message);
if (session == null && !disconnected)
session = (ServerSessionImpl)getBayeux().getSession(message.getClientId());
if (session != null)
{
disconnected = !session.isHandshook();
if (disconnected)
{
if (batch)
{
batch = false;
session.endBatch();
}
session = null;
}
else
{
if (autoBatch && !batch)
{
batch = true;
session.startBatch();
}
}
}
String channelName = message.getChannel();
if (channelName.equals(Channel.META_HANDSHAKE))
{
ServerMessage.Mutable reply = processMetaHandshake(request, response, session, message);
if (reply != null)
session = (ServerSessionImpl)getBayeux().getSession(reply.getClientId());
messages[i] = processReply(session, reply);
}
else if (channelName.equals(Channel.META_CONNECT))
{
ServerMessage.Mutable reply = processMetaConnect(request, response, session, message);
startInterval = reply != null;
if (reply == null)
suspended = messages.length == 1;
messages[i] = processReply(session, reply);
}
else
{
ServerMessage.Mutable reply = bayeuxServerHandle(session, message);
messages[i] = processReply(session, reply);
}
}
if (!suspended)
flush(request, response, session, startInterval, messages);
}
finally
{
if (batch)
session.endBatch();
}
}
protected ServerMessage.Mutable processMetaHandshake(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, ServerMessage.Mutable message)
{
ServerMessage.Mutable reply = bayeuxServerHandle(session, message);
if (reply != null)
{
session = (ServerSessionImpl)getBayeux().getSession(reply.getClientId());
if (session != null)
{
String browserId = findBrowserId(request);
if (browserId == null)
setBrowserId(request, response);
}
}
return reply;
}
protected ServerMessage.Mutable processMetaConnect(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, ServerMessage.Mutable message)
{
if (session != null)
{
// Cancel the previous scheduler to cancel any prior waiting long poll.
// This should also decrement the browser ID.
session.setScheduler(null);
}
boolean wasConnected = session != null && session.isConnected();
ServerMessage.Mutable reply = bayeuxServerHandle(session, message);
if (reply != null && session != null)
{
if (!session.hasNonLazyMessages() && reply.isSuccessful())
{
// Detect if we have multiple sessions from the same browser
// Note that CORS requests do not send cookies, so we need to handle them specially
// CORS requests always have the Origin header
String browserId = findBrowserId(request);
boolean allowSuspendConnect;
if (browserId != null)
allowSuspendConnect = incBrowserId(browserId);
else
allowSuspendConnect = _allowMultiSessionsNoBrowser || request.getHeader("Origin") != null;
if (allowSuspendConnect)
{
long timeout = session.calculateTimeout(getTimeout());
// Support old clients that do not send advice:{timeout:0} on the first connect
if (timeout > 0 && wasConnected && session.isConnected())
{
// Between the last time we checked for messages in the queue
// (which was false, otherwise we would not be in this branch)
// and now, messages may have been added to the queue.
// We will suspend anyway, but setting the scheduler on the
// session will decide atomically if we need to resume or not.
Continuation continuation = ContinuationSupport.getContinuation(request);
continuation.setTimeout(timeout);
continuation.suspend(response);
LongPollScheduler scheduler = newLongPollScheduler(session, continuation, reply, browserId);
request.setAttribute(LongPollScheduler.ATTRIBUTE, scheduler);
metaConnectSuspended(request, session, timeout);
// Setting the scheduler may resume the /meta/connect
session.setScheduler(scheduler);
reply = null;
}
else
{
decBrowserId(browserId);
}
}
else
{
// There are multiple sessions from the same browser
Map advice = reply.getAdvice(true);
if (browserId != null)
advice.put("multiple-clients", true);
long multiSessionInterval = _multiSessionInterval;
if (multiSessionInterval > 0)
{
advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_RETRY_VALUE);
advice.put(Message.INTERVAL_FIELD, multiSessionInterval);
}
else
{
advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);
reply.setSuccessful(false);
}
session.reAdvise();
}
}
if (reply != null && session.isDisconnected())
reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);
}
return reply;
}
protected ServerMessage.Mutable processReply(ServerSessionImpl session, ServerMessage.Mutable reply)
{
if (reply != null)
{
reply = getBayeux().extendReply(session, session, reply);
if (reply != null)
getBayeux().freeze(reply);
}
return reply;
}
protected void resume(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, ServerMessage.Mutable reply)
{
metaConnectResumed(request, session);
Map advice = session.takeAdvice();
if (advice != null)
reply.put(Message.ADVICE_FIELD, advice);
if (session.isDisconnected())
reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);
flush(request, response, session, true, processReply(session, reply));
}
protected void flush(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, boolean startInterval, ServerMessage.Mutable... replies)
{
List messages = Collections.emptyList();
if (session != null)
{
if (startInterval || !isMetaConnectDeliveryOnly() && !session.isMetaConnectDeliveryOnly())
messages = session.takeQueue();
}
write(request, response, session, startInterval, messages, replies);
}
protected void write(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, boolean startInterval, List messages, ServerMessage.Mutable[] replies)
{
try
{
ServletOutputStream output;
try
{
output = beginWrite(request, response, session);
// Write the messages first.
for (int i = 0; i < messages.size(); ++i)
{
ServerMessage message = messages.get(i);
if (i > 0)
output.write(',');
writeMessage(output, session, message);
}
}
finally
{
// Start the interval timeout after writing the messages
// since they may take time to be written, even in case
// of exceptions to make sure the session can be swept.
if (startInterval && session != null && session.isConnected())
session.startIntervalTimeout(getInterval());
}
// Write the replies, if any.
boolean needsComma = !messages.isEmpty();
for (int i = 0; i < replies.length; ++i)
{
ServerMessage reply = replies[i];
if (reply == null)
continue;
if (needsComma)
output.write(',');
needsComma = true;
writeMessage(output, session, reply);
}
endWrite(output, session);
}
catch (Exception x)
{
try
{
if (!response.isCommitted())
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
catch (Exception xx)
{
_logger.trace("Could not send " + HttpServletResponse.SC_INTERNAL_SERVER_ERROR + " response", xx);
}
}
}
protected void writeMessage(ServletOutputStream output, ServerSessionImpl session, ServerMessage message) throws IOException
{
output.write(message.getJSON().getBytes("UTF-8"));
}
protected abstract ServletOutputStream beginWrite(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session) throws IOException;
protected abstract void endWrite(ServletOutputStream output, ServerSessionImpl session) throws IOException;
protected LongPollScheduler newLongPollScheduler(ServerSessionImpl session, Continuation continuation, ServerMessage.Mutable metaConnectReply, String browserId)
{
return new LongPollScheduler(session, continuation, metaConnectReply, browserId);
}
protected ServerMessage.Mutable bayeuxServerHandle(ServerSessionImpl session, ServerMessage.Mutable message)
{
return getBayeux().handle(session, message);
}
protected void metaConnectSuspended(HttpServletRequest request, ServerSession session, long timeout)
{
}
protected void metaConnectResumed(HttpServletRequest request, ServerSession session)
{
}
protected void handleJSONParseException(HttpServletRequest request, HttpServletResponse response, String json, Throwable exception) throws ServletException, IOException
{
_logger.warn("Error parsing JSON: " + json, exception);
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
/**
* Sweep the transport for old Browser IDs
*
* @see org.cometd.server.AbstractServerTransport#sweep()
*/
protected void sweep()
{
long now = System.currentTimeMillis();
long elapsed = now - _lastSweep;
if (_lastSweep > 0 && elapsed > 0)
{
// Calculate the maximum sweeps that a browser ID can be 0 as the
// maximum interval time divided by the sweep period, doubled for safety
int maxSweeps = (int)(2 * getMaxInterval() / elapsed);
for (Map.Entry entry : _browserSweep.entrySet())
{
AtomicInteger count = entry.getValue();
// if the ID has been in the sweep map for 3 sweeps
if (count != null && count.incrementAndGet() > maxSweeps)
{
String key = entry.getKey();
// remove it from both browser Maps
if (_browserSweep.remove(key) == count && _browserMap.get(key).get() == 0)
{
_browserMap.remove(key);
_logger.debug("Swept browserId {}", key);
}
}
}
}
_lastSweep = now;
}
protected ServerMessage.Mutable[] parseMessages(String[] requestParameters) throws IOException, ParseException
{
if (requestParameters == null || requestParameters.length == 0)
throw new IOException("Missing '" + MESSAGE_PARAM + "' request parameter");
if (requestParameters.length == 1)
return parseMessages(requestParameters[0]);
List messages = new ArrayList();
for (String batch : requestParameters)
{
if (batch == null)
continue;
messages.addAll(Arrays.asList(parseMessages(batch)));
}
return messages.toArray(new ServerMessage.Mutable[messages.size()]);
}
protected abstract ServerMessage.Mutable[] parseMessages(HttpServletRequest request) throws IOException, ParseException;
/**
* @return true if the transport always flushes at the end of a call to {@link #handle(HttpServletRequest, HttpServletResponse)}.
*/
protected abstract boolean isAlwaysFlushingAfterHandle();
protected class LongPollScheduler implements AbstractServerTransport.OneTimeScheduler, ContinuationListener
{
private static final String ATTRIBUTE = "org.cometd.scheduler";
private final ServerSessionImpl _session;
private final Continuation _continuation;
private final ServerMessage.Mutable _reply;
private final String _browserId;
public LongPollScheduler(ServerSessionImpl session, Continuation continuation, ServerMessage.Mutable reply, String browserId)
{
_session = session;
_continuation = continuation;
_continuation.addContinuationListener(this);
_reply = reply;
_browserId = browserId;
}
public void cancel()
{
if (_continuation != null && _continuation.isSuspended() && !_continuation.isExpired())
{
try
{
decBrowserId();
((HttpServletResponse)_continuation.getServletResponse()).sendError(HttpServletResponse.SC_REQUEST_TIMEOUT);
}
catch (IOException x)
{
_logger.trace("", x);
}
try
{
_continuation.complete();
}
catch (Exception x)
{
_logger.trace("", x);
}
}
}
public void schedule()
{
decBrowserId();
_continuation.resume();
}
public ServerSessionImpl getSession()
{
return _session;
}
public ServerMessage.Mutable getReply()
{
Map advice = _session.takeAdvice();
if (advice != null)
_reply.put(Message.ADVICE_FIELD, advice);
return _reply;
}
public void onComplete(Continuation continuation)
{
}
public void onTimeout(Continuation continuation)
{
decBrowserId();
_session.setScheduler(null);
}
private void decBrowserId()
{
LongPollingTransport.this.decBrowserId(_browserId);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy