/* * Copyright (c) 2012 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.eclipse.jetty.spdy; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.Handler; import org.eclipse.jetty.spdy.api.HeadersInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StreamStatus; import org.eclipse.jetty.spdy.api.SynInfo; import org.eclipse.jetty.spdy.frames.ControlFrame; import org.eclipse.jetty.spdy.frames.DataFrame; import org.eclipse.jetty.spdy.frames.HeadersFrame; import org.eclipse.jetty.spdy.frames.SynReplyFrame; import org.eclipse.jetty.spdy.frames.SynStreamFrame; import org.eclipse.jetty.spdy.frames.WindowUpdateFrame; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; public class StandardStream implements IStream { private static final Logger logger = Log.getLogger(Stream.class); private final Map<String, Object> attributes = new ConcurrentHashMap<>(); private final IStream associatedStream; private final SynStreamFrame frame; private final ISession session; private final AtomicInteger windowSize; private final Set<Stream> pushedStreams = Collections.newSetFromMap(new ConcurrentHashMap<Stream, Boolean>()); private volatile StreamFrameListener listener; private volatile OpenState openState = OpenState.SYN_SENT; private volatile CloseState closeState = CloseState.OPENED; private volatile boolean reset = false; public StandardStream(SynStreamFrame frame, ISession session, int windowSize, IStream associatedStream) { this.frame = frame; this.session = session; this.windowSize = new AtomicInteger(windowSize); this.associatedStream = associatedStream; } @Override public int getId() { return frame.getStreamId(); } @Override public IStream getAssociatedStream() { return associatedStream; } @Override public Set<Stream> getPushedStreams() { return pushedStreams; } @Override public void associate(IStream stream) { pushedStreams.add(stream); } @Override public void disassociate(IStream stream) { pushedStreams.remove(stream); } @Override public byte getPriority() { return frame.getPriority(); } @Override public int getWindowSize() { return windowSize.get(); } @Override public void updateWindowSize(int delta) { int size = windowSize.addAndGet(delta); logger.debug("Updated window size by {}, new window size {}",delta,size); } @Override public Session getSession() { return session; } @Override public Object getAttribute(String key) { return attributes.get(key); } @Override public void setAttribute(String key, Object value) { attributes.put(key,value); } @Override public Object removeAttribute(String key) { return attributes.remove(key); } @Override public void setStreamFrameListener(StreamFrameListener listener) { this.listener = listener; } @Override public void updateCloseState(boolean close, boolean local) { if (close) { switch (closeState) { case OPENED: { closeState = local?CloseState.LOCALLY_CLOSED:CloseState.REMOTELY_CLOSED; break; } case LOCALLY_CLOSED: { if (local) throw new IllegalStateException(); else closeState = CloseState.CLOSED; break; } case REMOTELY_CLOSED: { if (local) closeState = CloseState.CLOSED; else throw new IllegalStateException(); break; } default: { throw new IllegalStateException(); } } } } @Override public void process(ControlFrame frame) { switch (frame.getType()) { case SYN_STREAM: { openState = OpenState.SYN_RECV; break; } case SYN_REPLY: { openState = OpenState.REPLY_RECV; SynReplyFrame synReply = (SynReplyFrame)frame; updateCloseState(synReply.isClose(),false); ReplyInfo replyInfo = new ReplyInfo(synReply.getHeaders(),synReply.isClose()); notifyOnReply(replyInfo); break; } case HEADERS: { HeadersFrame headers = (HeadersFrame)frame; updateCloseState(headers.isClose(),false); HeadersInfo headersInfo = new HeadersInfo(headers.getHeaders(),headers.isClose(),headers.isResetCompression()); notifyOnHeaders(headersInfo); break; } case WINDOW_UPDATE: { WindowUpdateFrame windowUpdate = (WindowUpdateFrame)frame; updateWindowSize(windowUpdate.getWindowDelta()); break; } case RST_STREAM: { reset = true; break; } default: { throw new IllegalStateException(); } } session.flush(); } @Override public void process(DataFrame frame, ByteBuffer data) { // TODO: in v3 we need to send a rst instead of just ignoring // ignore data frame if this stream is remotelyClosed already if (isHalfClosed() && !isLocallyClosed()) { logger.debug("Ignoring received dataFrame as this stream is remotely closed: " + frame); return; } if (!canReceive()) { logger.debug("Can't receive. Sending rst: " + frame); session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); return; } updateCloseState(frame.isClose(),false); ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(data,frame.isClose(),frame.isCompress()) { @Override public void consume(int delta) { super.consume(delta); // This is the algorithm for flow control. // This method may be called multiple times with delta=1, but we only send a window // update when the whole dataInfo has been consumed. // Other policies may be to send window updates when consumed() is greater than // a certain threshold, etc. but for now the policy is not pluggable for simplicity. // Note that the frequency of window updates depends on the read buffer, that // should not be too smaller than the window size to avoid frequent window updates. // Therefore, a pluggable policy should be able to modify the read buffer capacity. if (consumed() == length() && !isClosed()) windowUpdate(length()); } }; notifyOnData(dataInfo); session.flush(); } private void windowUpdate(int delta) { if (delta > 0) { WindowUpdateFrame windowUpdateFrame = new WindowUpdateFrame(session.getVersion(),getId(),delta); session.control(this,windowUpdateFrame,0,TimeUnit.MILLISECONDS,null,null); } } private void notifyOnReply(ReplyInfo replyInfo) { final StreamFrameListener listener = this.listener; try { if (listener != null) { logger.debug("Invoking reply callback with {} on listener {}",replyInfo,listener); listener.onReply(this,replyInfo); } } catch (Exception x) { logger.info("Exception while notifying listener " + listener,x); } } private void notifyOnHeaders(HeadersInfo headersInfo) { final StreamFrameListener listener = this.listener; try { if (listener != null) { logger.debug("Invoking headers callback with {} on listener {}",frame,listener); listener.onHeaders(this,headersInfo); } } catch (Exception x) { logger.info("Exception while notifying listener " + listener,x); } } private void notifyOnData(DataInfo dataInfo) { final StreamFrameListener listener = this.listener; try { if (listener != null) { logger.debug("Invoking data callback with {} on listener {}",dataInfo,listener); listener.onData(this,dataInfo); logger.debug("Invoked data callback with {} on listener {}",dataInfo,listener); } } catch (Exception x) { logger.info("Exception while notifying listener " + listener,x); } } @Override public Future<Stream> syn(SynInfo synInfo) { Promise<Stream> result = new Promise<>(); syn(synInfo,0,TimeUnit.MILLISECONDS,result); return result; } @Override public void syn(SynInfo synInfo, long timeout, TimeUnit unit, Handler<Stream> handler) { if (isClosed() || isReset()) { handler.failed(new StreamException(getId(),StreamStatus.STREAM_ALREADY_CLOSED)); return; } PushSynInfo pushSynInfo = new PushSynInfo(getId(),synInfo); session.syn(pushSynInfo,null,timeout,unit,handler); } @Override public Future<Void> reply(ReplyInfo replyInfo) { Promise<Void> result = new Promise<>(); reply(replyInfo,0,TimeUnit.MILLISECONDS,result); return result; } @Override public void reply(ReplyInfo replyInfo, long timeout, TimeUnit unit, Handler<Void> handler) { openState = OpenState.REPLY_SENT; updateCloseState(replyInfo.isClose(),true); SynReplyFrame frame = new SynReplyFrame(session.getVersion(),replyInfo.getFlags(),getId(),replyInfo.getHeaders()); session.control(this,frame,timeout,unit,handler,null); } @Override public Future<Void> data(DataInfo dataInfo) { Promise<Void> result = new Promise<>(); data(dataInfo,0,TimeUnit.MILLISECONDS,result); return result; } @Override public void data(DataInfo dataInfo, long timeout, TimeUnit unit, Handler<Void> handler) { if (!canSend()) { session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a DATA frame before a SYN_REPLY frame"); } if (isLocallyClosed()) { session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a DATA frame on a closed stream"); } // Cannot update the close state here, because the data that we send may // be flow controlled, so we need the stream to update the window size. session.data(this,dataInfo,timeout,unit,handler,null); } @Override public Future<Void> headers(HeadersInfo headersInfo) { Promise<Void> result = new Promise<>(); headers(headersInfo,0,TimeUnit.MILLISECONDS,result); return result; } @Override public void headers(HeadersInfo headersInfo, long timeout, TimeUnit unit, Handler<Void> handler) { if (!canSend()) { session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a HEADERS frame before a SYN_REPLY frame"); } if (isLocallyClosed()) { session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a HEADERS frame on a closed stream"); } updateCloseState(headersInfo.isClose(),true); HeadersFrame frame = new HeadersFrame(session.getVersion(),headersInfo.getFlags(),getId(),headersInfo.getHeaders()); session.control(this,frame,timeout,unit,handler,null); } @Override public boolean isUnidirectional() { if (associatedStream != null) return true; else return false; } @Override public boolean isReset() { return reset; } @Override public boolean isHalfClosed() { CloseState closeState = this.closeState; return closeState == CloseState.LOCALLY_CLOSED || closeState == CloseState.REMOTELY_CLOSED || closeState == CloseState.CLOSED; } @Override public boolean isClosed() { return closeState == CloseState.CLOSED; } private boolean isLocallyClosed() { CloseState closeState = this.closeState; return closeState == CloseState.LOCALLY_CLOSED || closeState == CloseState.CLOSED; } @Override public String toString() { return String.format("stream=%d v%d %s",getId(),session.getVersion(),closeState); } private boolean canSend() { OpenState openState = this.openState; return openState == OpenState.SYN_SENT || openState == OpenState.REPLY_RECV || openState == OpenState.REPLY_SENT; } private boolean canReceive() { OpenState openState = this.openState; return openState == OpenState.SYN_RECV || openState == OpenState.REPLY_RECV || openState == OpenState.REPLY_SENT; } private enum OpenState { SYN_SENT, SYN_RECV, REPLY_SENT, REPLY_RECV } private enum CloseState { OPENED, LOCALLY_CLOSED, REMOTELY_CLOSED, CLOSED } }