/* * 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.Map; 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.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.slf4j.Logger; import org.slf4j.LoggerFactory; public class StandardStream implements IStream { private static final Logger logger = LoggerFactory.getLogger(Stream.class); private final Map<String, Object> attributes = new ConcurrentHashMap<>(); private final SynStreamFrame frame; private final ISession session; private final AtomicInteger windowSize; private volatile StreamFrameListener listener; private volatile boolean opened; private volatile boolean halfClosed; private volatile boolean closed; public StandardStream(SynStreamFrame frame, ISession session, int windowSize) { this.frame = frame; this.session = session; this.windowSize = new AtomicInteger(windowSize); this.halfClosed = frame.isClose(); } @Override public int getId() { return frame.getStreamId(); } @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; } public boolean isHalfClosed() { return halfClosed; } @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) { if (close) { if (isHalfClosed()) closed = true; else halfClosed = true; } } @Override public void process(ControlFrame frame) { switch (frame.getType()) { case SYN_STREAM: { opened = true; break; } case SYN_REPLY: { opened = true; SynReplyFrame synReply = (SynReplyFrame)frame; updateCloseState(synReply.isClose()); ReplyInfo replyInfo = new ReplyInfo(synReply.getHeaders(), synReply.isClose()); notifyOnReply(replyInfo); break; } case HEADERS: { HeadersFrame headers = (HeadersFrame)frame; updateCloseState(headers.isClose()); 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: { // TODO: break; } default: { throw new IllegalStateException(); } } session.flush(); } @Override public void process(DataFrame frame, ByteBuffer data) { if (!opened) { session.rst(new RstInfo(getId(), StreamStatus.PROTOCOL_ERROR)); return; } updateCloseState(frame.isClose()); 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<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) { updateCloseState(replyInfo.isClose()); 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) { // 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) { updateCloseState(headersInfo.isClose()); HeadersFrame frame = new HeadersFrame(session.getVersion(), headersInfo.getFlags(), getId(), headersInfo.getHeaders()); session.control(this, frame, timeout, unit, handler, null); } @Override public boolean isClosed() { return closed; } @Override public String toString() { return String.format("stream=%d v%d closed=%s", getId(), session.getVersion(), isClosed() ? "true" : isHalfClosed() ? "half" : "false"); } }