All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.websocket.common.message.MessageInputStream Maven / Gradle / Ivy

There is a newer version: 9.4.56.v20240826
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.websocket.common.message;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.NullByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.common.WebSocketSession;

/**
 * Support class for reading a (single) WebSocket BINARY message via a InputStream.
 * 

* An InputStream that can access a queue of ByteBuffer payloads, along with expected InputStream blocking behavior. */ public class MessageInputStream extends InputStream implements MessageAppender { private static final Logger LOG = Log.getLogger(MessageInputStream.class); private static final ByteBuffer EOF = ByteBuffer.allocate(0).asReadOnlyBuffer(); private final Session session; private final ByteBufferPool bufferPool; private final BlockingDeque buffers = new LinkedBlockingDeque<>(); private final long timeoutMs; private ByteBuffer activeBuffer = null; private SuspendToken suspendToken; private State state = State.RESUMED; private enum State { /** * Open and waiting for a frame to be delivered in {@link #appendFrame(ByteBuffer, boolean)}. */ RESUMED, /** * We have suspended the session after reading a websocket frame but have not reached the end of the message. */ SUSPENDED, /** * We have received a frame with fin==true and have suspended until we are signaled that onMessage method exited. */ COMPLETE, /** * We have read to EOF or someone has called InputStream.close(), any further reads will result in reading -1. */ CLOSED } public MessageInputStream(Session session) { this(session, -1); } public MessageInputStream(Session session, int timeoutMs) { this.timeoutMs = timeoutMs; this.session = session; this.bufferPool = (session instanceof WebSocketSession) ? ((WebSocketSession)session).getBufferPool() : new NullByteBufferPool(); } @Override public void appendFrame(ByteBuffer framePayload, boolean fin) throws IOException { if (LOG.isDebugEnabled()) LOG.debug("Appending {} chunk: {}", fin ? "final" : "non-final", BufferUtil.toDetailString(framePayload)); // Avoid entering synchronized block if there is nothing to do. boolean bufferIsEmpty = BufferUtil.isEmpty(framePayload); if (bufferIsEmpty && !fin) return; try { synchronized (this) { if (!bufferIsEmpty) { switch (state) { case CLOSED: return; case RESUMED: suspendToken = session.suspend(); state = State.SUSPENDED; break; default: throw new IllegalStateException("Incorrect State: " + state.name()); } // Put the payload into the queue, by copying it. // Copying is necessary because the payload will // be processed after this method returns. ByteBuffer copy = acquire(framePayload.remaining(), framePayload.isDirect()); BufferUtil.clearToFill(copy); copy.put(framePayload); BufferUtil.flipToFlush(copy, 0); buffers.put(copy); } if (fin) { buffers.add(EOF); state = State.COMPLETE; } } } catch (InterruptedException e) { throw new IOException(e); } } @Override public void close() { synchronized (this) { if (state == State.CLOSED) return; boolean remainingContent = (state != State.COMPLETE) || (!buffers.isEmpty() && buffers.peek() != EOF) || (activeBuffer != null && activeBuffer.hasRemaining()); if (remainingContent) LOG.warn("MessageInputStream closed without fully consuming content {}", session); // Release any buffers taken from the pool. if (activeBuffer != null && activeBuffer != EOF) bufferPool.release(activeBuffer); for (ByteBuffer buffer : buffers) { if (buffer != null && buffer != EOF) bufferPool.release(buffer); } activeBuffer = null; buffers.clear(); state = State.CLOSED; buffers.add(EOF); } } public void handlerComplete() { // Close the InputStream. close(); // May need to resume to resume and read to the next message. SuspendToken resume; synchronized (this) { resume = suspendToken; suspendToken = null; } if (resume != null) resume.resume(); } @Override public int read() throws IOException { byte[] bytes = new byte[1]; while (true) { int read = read(bytes, 0, 1); if (read < 0) return -1; if (read == 0) continue; return bytes[0] & 0xFF; } } @Override public int read(byte[] b, int off, int len) throws IOException { try { if (state == State.CLOSED) { if (LOG.isDebugEnabled()) LOG.debug("Stream closed"); return -1; } // grab a fresh buffer while (activeBuffer == null) { if (LOG.isDebugEnabled()) LOG.debug("Waiting {} ms to read", timeoutMs); if (timeoutMs < 0) { // Wait forever until a buffer is available. activeBuffer = buffers.take(); } else { // Wait at most for the given timeout. activeBuffer = buffers.poll(timeoutMs, TimeUnit.MILLISECONDS); if (activeBuffer == null) throw new IOException(String.format("Read timeout: %,dms expired", timeoutMs)); } if (activeBuffer == EOF) { if (LOG.isDebugEnabled()) LOG.debug("Reached EOF"); close(); return -1; } } ByteBuffer buffer = BufferUtil.toBuffer(b, off, len); BufferUtil.clearToFill(buffer); int written = BufferUtil.put(activeBuffer, buffer); BufferUtil.flipToFlush(buffer, 0); // If we have no more content we may need to resume to get more data. if (!activeBuffer.hasRemaining()) { SuspendToken resume = null; synchronized (this) { // Release buffer back to pool. if (activeBuffer != null && activeBuffer != EOF) bufferPool.release(activeBuffer); activeBuffer = null; switch (state) { case CLOSED: return -1; case COMPLETE: // If we are complete we have read the last frame but // don't want to resume reading until onMessage() exits. break; case SUSPENDED: resume = suspendToken; suspendToken = null; state = State.RESUMED; break; case RESUMED: throw new IllegalStateException("Incorrect State: " + state.name()); } } // Get more content to read. if (resume != null) resume.resume(); } return written; } catch (InterruptedException x) { if (LOG.isDebugEnabled()) LOG.debug("Interrupted while waiting to read", x); close(); return -1; } } @Override public void messageComplete() { // We handle this case in appendFrame with fin==true. } @Override public void reset() throws IOException { throw new IOException("reset() not supported"); } @Override public void mark(int readlimit) { // Not supported. } @Override public boolean markSupported() { return false; } private ByteBuffer acquire(int capacity, boolean direct) { return bufferPool.acquire(capacity, direct); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy