/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.gravity.websocket; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import javax.servlet.http.HttpSession; import org.granite.context.GraniteContext; import org.granite.context.SimpleGraniteContext; import org.granite.gravity.AbstractChannel; import org.granite.gravity.AsyncHttpContext; import org.granite.gravity.Channel; import org.granite.gravity.ChannelFactory; import org.granite.gravity.GravityConfig; import org.granite.gravity.GravityInternal; import org.granite.gravity.MessageReceivingException; import org.granite.logging.Logger; import org.granite.messaging.jmf.JMFDeserializer; import org.granite.messaging.jmf.JMFSerializer; import org.granite.messaging.webapp.ServletGraniteContext; import org.granite.util.ContentType; import flex.messaging.messages.AsyncMessage; import flex.messaging.messages.Message; /** * @author Franck WOLFF */ public abstract class AbstractWebSocketChannel extends AbstractChannel { /////////////////////////////////////////////////////////////////////////// // Fields. private static final int DEFAULT_MAX_BINARY_MESSAGE_BUFFER_SIZE = 16384; private static final Logger log = Logger.getLogger(AbstractWebSocketChannel.class); private static final Logger logFine = Logger.getLogger(AbstractWebSocketChannel.class.getName() + "_fine"); private HttpSession session; private ContentType contentType; private Object clientId; private byte[] connectAckMessage; private int maxBinaryMessageBufferSize = DEFAULT_MAX_BINARY_MESSAGE_BUFFER_SIZE; /////////////////////////////////////////////////////////////////////////// // Constructor. protected AbstractWebSocketChannel(GravityInternal gravity, String id, ChannelFactory<? extends Channel> factory, String clientType) { super(gravity, id, factory, clientType); } protected void setMaxBinaryMessageBufferSize(int maxBinaryMessageBufferSize) { if (maxBinaryMessageBufferSize < 512) log.warn("Trying to set WebSocket maxBinaryMessageBufferSize too low: %d (ignored)", maxBinaryMessageBufferSize); else { log.debug("Setting MaxBinaryMessageBufferSize to: %d", maxBinaryMessageBufferSize); this.maxBinaryMessageBufferSize = maxBinaryMessageBufferSize; } } public int getMaxBinaryMessageBufferSize() { return maxBinaryMessageBufferSize; } public void setSession(HttpSession session) { this.session = session; } public void setConnectAckMessage(Message ackMessage) { try { // Return an acknowledge message with the server-generated clientId clientId = ackMessage.getClientId(); connectAckMessage = serialize(getGravity(), new Message[] { ackMessage }); } catch (IOException e) { throw new RuntimeException("Could not serialize connect acknowledge", e); } } protected void connect() { log.debug("Channel %s websocket connect clientId %s %s", getId(), clientId, connectAckMessage == null ? "(no ack)" : ""); if (connectAckMessage == null) return; try { // Return an acknowledge message with the server-generated clientId sendBytes(connectAckMessage); connectAckMessage = null; } catch (IOException e) { log.error(e, "Channel %s could not send connect acknowledge", getId()); } } public ContentType getContentType() { return contentType; } public void setContentType(ContentType contentType) { this.contentType = contentType; } protected GravityInternal initializeRequest() { if (session != null) ServletGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), session.getServletContext(), session, clientType); else SimpleGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), sessionId, new HashMap<String, Object>(), clientType); return gravity; } protected Message[] deserialize(GravityInternal gravity, byte[] data, int offset, int length) throws ClassNotFoundException, IOException { ByteArrayInputStream is = new ByteArrayInputStream(data, offset, length); try { Message[] messages = null; if (ContentType.JMF_AMF.equals(contentType)) { @SuppressWarnings("all") // JDK7 warning (Resource leak: 'deserializer' is never closed)... JMFDeserializer deserializer = new JMFDeserializer(is, gravity.getGraniteConfig().getSharedContext()); messages = (Message[])deserializer.readObject(); } else { ObjectInput amf3Deserializer = gravity.getGraniteConfig().newAMF3Deserializer(is); Object[] objects = (Object[])amf3Deserializer.readObject(); messages = new Message[objects.length]; System.arraycopy(objects, 0, messages, 0, objects.length); } return messages; } finally { is.close(); } } protected byte[] serialize(GravityInternal gravity, Message[] messages) throws IOException { ByteArrayOutputStream os = null; try { os = new ByteArrayOutputStream(200*messages.length); if (ContentType.JMF_AMF.equals(contentType)) { @SuppressWarnings("all") // JDK7 warning (Resource leak: 'serializer' is never closed)... JMFSerializer serializer = new JMFSerializer(os, gravity.getGraniteConfig().getSharedContext()); serializer.writeObject(messages); } else { ObjectOutput amf3Serializer = gravity.getGraniteConfig().newAMF3Serializer(os); amf3Serializer.writeObject(messages); amf3Serializer.flush(); } return os.toByteArray(); } finally { if (os != null) os.close(); } } protected static void cleanupRequest() { GraniteContext.release(); } public abstract boolean isConnected(); protected abstract void sendBytes(byte[] msg) throws IOException; protected void receiveBytes(byte[] data, int offset, int length) { log.debug("Channel %s websocket receive %d bytes", getId(), data.length); try { initializeRequest(); Message[] messages = deserialize(getGravity(), data, offset, length); logFine.debug(">> [AMF3 REQUESTS] %s", (Object)messages); Message[] responses = null; boolean accessed = false; int responseIndex = 0; for (int i = 0; i < messages.length; i++) { Message message = messages[i]; // Ask gravity to create a specific response (will be null with a connect request from tunnel). Message response = getGravity().handleMessage(getFactory(), message); String clientId = (String)message.getClientId(); log.debug("Channel %s received message %s for clientId %s", getId(), message.getMessageId(), clientId); // Mark current channel (if any) as accessed. if (!accessed) accessed = getGravity().access(clientId); if (response != null) { if (responses == null) responses = new Message[1]; else responses = Arrays.copyOf(responses, responses.length+1); responses[responseIndex++] = response; } } if (isConnected()) { // Check in case of disconnect message logFine.debug("<< [AMF3 RESPONSES] %s", (Object)responses); for (Message response : responses) { if (response instanceof AsyncMessage) receive((AsyncMessage)response); } } } catch (MessageReceivingException e) { log.error(e, "Could not handle incoming message data"); } catch (ClassNotFoundException e) { log.error(e, "Could not handle incoming message data"); } catch (IOException e) { log.error(e, "Could not handle incoming message data"); } finally { cleanupRequest(); } } @Override public boolean runReceived(AsyncHttpContext asyncHttpContext) { LinkedList<AsyncMessage> messages = null; ByteArrayOutputStream os = null; try { receivedQueueLock.lock(); try { // Do we have any pending messages? if (receivedQueue.isEmpty()) return false; // Both conditions are ok, get all pending messages. messages = receivedQueue; receivedQueue = new LinkedList<AsyncMessage>(); } finally { receivedQueueLock.unlock(); } if (!isConnected()) { log.debug("Channel %s is not connected", getId()); return false; } // Setup serialization context (thread local) GravityInternal gravity = getGravity(); SimpleGraniteContext.createThreadInstance( gravity.getGraniteConfig(), gravity.getServicesConfig(), sessionId, new HashMap<String, Object>(), clientType ); AsyncMessage[] messagesArray = messages.toArray(new AsyncMessage[messages.size()]); logFine.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray); gravity.access(getId()); // Notify that the channel has been active byte[] msg = serialize(gravity, messagesArray); if (msg.length <= maxBinaryMessageBufferSize) { log.debug("Channel %s send binary message: %d msgs (%d bytes)", getId(), messagesArray.length, msg.length); sendBytes(msg); } else { int index = 1; for (AsyncMessage message : messagesArray) { msg = serialize(gravity, new AsyncMessage[]{ message }); log.debug("Channel %s send chunked binary message: %d/%d msgs (%d bytes)", getId(), index++, messagesArray.length, msg.length); sendBytes(msg); } } log.debug("Channel %s binary messages sent", getId()); return true; // Messages were delivered, http context isn't valid anymore. } catch (IOException e) { log.warn(e, "Could not send messages to channel: %s (retrying later)", this); GravityConfig gravityConfig = getGravity().getGravityConfig(); if (gravityConfig.isRetryOnError()) { receivedQueueLock.lock(); try { if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) { log.warn( "Channel %s has reached its maximum queue capacity %s (throwing %s messages)", this, gravityConfig.getMaxMessagesQueuedPerChannel(), messages.size() ); } else receivedQueue.addAll(0, messages); } finally { receivedQueueLock.unlock(); } } return true; // Messages weren't delivered, but http context isn't valid anymore. } finally { if (os != null) { try { os.close(); } catch (Exception e) { // Could not close bytearray ??? } } // Cleanup serialization context (thread local) try { GraniteContext.release(); } catch (Exception e) { // should never happen... } } } @Override public void destroy(boolean timeout) { try { super.destroy(timeout); } finally { close(timeout); } } @Override protected boolean hasAsyncHttpContext() { return true; } @Override protected void releaseAsyncHttpContext(AsyncHttpContext context) { } @Override protected AsyncHttpContext acquireAsyncHttpContext() { return null; } }