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

org.springframework.web.reactive.socket.adapter.AbstractListenerWebSocketSession Maven / Gradle / Ivy

/*
 * Copyright 2002-2017 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.springframework.web.reactive.socket.adapter;

import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.util.concurrent.Queues;

import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.server.reactive.AbstractListenerReadPublisher;
import org.springframework.http.server.reactive.AbstractListenerWriteProcessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.reactive.socket.CloseStatus;
import org.springframework.web.reactive.socket.HandshakeInfo;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketMessage.Type;
import org.springframework.web.reactive.socket.WebSocketSession;

/**
 * Base class for {@link WebSocketSession} implementations that bridge between
 * event-listener WebSocket APIs (e.g. Java WebSocket API JSR-356, Jetty,
 * Undertow) and Reactive Streams.
 *
 * 

Also an implementation of {@code Subscriber<Void>} so it can be used as * the completion subscriber for session handling * * @author Violeta Georgieva * @author Rossen Stoyanchev * @since 5.0 */ public abstract class AbstractListenerWebSocketSession extends AbstractWebSocketSession implements Subscriber { /** * The "back-pressure" buffer size to use if the underlying WebSocket API * does not have flow control for receiving messages. */ private static final int RECEIVE_BUFFER_SIZE = 8192; @Nullable private final MonoProcessor completionMono; private final WebSocketReceivePublisher receivePublisher = new WebSocketReceivePublisher(); @Nullable private volatile WebSocketSendProcessor sendProcessor; private final AtomicBoolean sendCalled = new AtomicBoolean(); /** * Base constructor. * @param delegate the native WebSocket session, channel, or connection * @param id the session id * @param handshakeInfo the handshake info * @param bufferFactory the DataBuffer factor for the current connection */ public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo handshakeInfo, DataBufferFactory bufferFactory) { this(delegate, id, handshakeInfo, bufferFactory, null); } /** * Alternative constructor with completion {@code Mono<Void>} to propagate * the session completion (success or error) (for client-side use). */ public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo handshakeInfo, DataBufferFactory bufferFactory, @Nullable MonoProcessor completionMono) { super(delegate, id, handshakeInfo, bufferFactory); this.completionMono = completionMono; } protected WebSocketSendProcessor getSendProcessor() { WebSocketSendProcessor sendProcessor = this.sendProcessor; Assert.state(sendProcessor != null, "No WebSocketSendProcessor available"); return sendProcessor; } @Override public Flux receive() { return canSuspendReceiving() ? Flux.from(this.receivePublisher) : Flux.from(this.receivePublisher).onBackpressureBuffer(RECEIVE_BUFFER_SIZE); } @Override public Mono send(Publisher messages) { if (this.sendCalled.compareAndSet(false, true)) { WebSocketSendProcessor sendProcessor = new WebSocketSendProcessor(); this.sendProcessor = sendProcessor; return Mono.from(subscriber -> { messages.subscribe(sendProcessor); sendProcessor.subscribe(subscriber); }); } else { return Mono.error(new IllegalStateException("send() has already been called")); } } /** * Whether the underlying WebSocket API has flow control and can suspend and * resume the receiving of messages. *

Note: Sub-classes are encouraged to start out in * suspended mode, if possible, and wait until demand is received. */ protected abstract boolean canSuspendReceiving(); /** * Suspend receiving until received message(s) are processed and more demand * is generated by the downstream Subscriber. *

Note: if the underlying WebSocket API does not provide * flow control for receiving messages, this method should be a no-op * and {@link #canSuspendReceiving()} should return {@code false}. */ protected abstract void suspendReceiving(); /** * Resume receiving new message(s) after demand is generated by the * downstream Subscriber. *

Note: if the underlying WebSocket API does not provide * flow control for receiving messages, this method should be a no-op * and {@link #canSuspendReceiving()} should return {@code false}. */ protected abstract void resumeReceiving(); /** * Send the given WebSocket message. *

Note: Sub-classes are responsible for releasing the * payload data buffer, once fully written, if pooled buffers apply to the * underlying container. */ protected abstract boolean sendMessage(WebSocketMessage message) throws IOException; // WebSocketHandler adapter delegate methods /** Handle a message callback from the WebSocketHandler adapter */ void handleMessage(Type type, WebSocketMessage message) { this.receivePublisher.handleMessage(message); } /** Handle an error callback from the WebSocketHandler adapter */ void handleError(Throwable ex) { this.receivePublisher.onError(ex); WebSocketSendProcessor sendProcessor = this.sendProcessor; if (sendProcessor != null) { sendProcessor.cancel(); sendProcessor.onError(ex); } } /** Handle a close callback from the WebSocketHandler adapter */ void handleClose(CloseStatus reason) { this.receivePublisher.onAllDataRead(); WebSocketSendProcessor sendProcessor = this.sendProcessor; if (sendProcessor != null) { sendProcessor.cancel(); sendProcessor.onComplete(); } } // Subscriber implementation @Override public void onSubscribe(Subscription subscription) { subscription.request(Long.MAX_VALUE); } @Override public void onNext(Void aVoid) { // no op } @Override public void onError(Throwable ex) { if (this.completionMono != null) { this.completionMono.onError(ex); } int code = CloseStatus.SERVER_ERROR.getCode(); close(new CloseStatus(code, ex.getMessage())); } @Override public void onComplete() { if (this.completionMono != null) { this.completionMono.onComplete(); } close(); } private final class WebSocketReceivePublisher extends AbstractListenerReadPublisher { private volatile Queue pendingMessages = Queues.unbounded(Queues.SMALL_BUFFER_SIZE).get(); @Override protected void checkOnDataAvailable() { resumeReceiving(); if (!this.pendingMessages.isEmpty()) { logger.trace("checkOnDataAvailable, processing pending messages"); onDataAvailable(); } else { logger.trace("checkOnDataAvailable, no pending messages"); } } @Override protected void readingPaused() { suspendReceiving(); } @Override @Nullable protected WebSocketMessage read() throws IOException { return (WebSocketMessage) this.pendingMessages.poll(); } void handleMessage(WebSocketMessage message) { if (logger.isTraceEnabled()) { logger.trace("Received message: " + message); } if (!this.pendingMessages.offer(message)) { throw new IllegalStateException("Too many messages received. " + "Please ensure WebSocketSession.receive() is subscribed to."); } onDataAvailable(); } } protected final class WebSocketSendProcessor extends AbstractListenerWriteProcessor { private volatile boolean isReady = true; @Override protected boolean write(WebSocketMessage message) throws IOException { if (logger.isTraceEnabled()) { logger.trace("Sending message " + message); } return sendMessage(message); } @Override protected boolean isDataEmpty(WebSocketMessage message) { return (message.getPayload().readableByteCount() == 0); } @Override protected boolean isWritePossible() { return (this.isReady); } /** * Sub-classes can invoke this before sending a message (false) and * after receiving the async send callback (true) effective translating * async completion callback into simple flow control. */ public void setReadyToSend(boolean ready) { if (ready) { logger.trace("Send succeeded, ready to send again"); } this.isReady = ready; } } }