Browse Source
Includes basic abstractions and an RxNetty support to start. Issue: SPR-14527pull/1239/merge
20 changed files with 1380 additions and 5 deletions
@ -0,0 +1,213 @@
@@ -0,0 +1,213 @@
|
||||
/* |
||||
* Copyright 2002-2014 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; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* Representation of WebSocket "close" status codes and reasons. Status codes |
||||
* in the 1xxx range are pre-defined by the protocol. |
||||
* |
||||
* <p>See <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1"> |
||||
* RFC 6455, Section 7.4.1 "Defined Status Codes"</a>. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public final class CloseStatus { |
||||
|
||||
/** |
||||
* "1000 indicates a normal closure, meaning that the purpose for which the connection |
||||
* was established has been fulfilled." |
||||
*/ |
||||
public static final CloseStatus NORMAL = new CloseStatus(1000); |
||||
|
||||
/** |
||||
* "1001 indicates that an endpoint is "going away", such as a server going down or a |
||||
* browser having navigated away from a page." |
||||
*/ |
||||
public static final CloseStatus GOING_AWAY = new CloseStatus(1001); |
||||
|
||||
/** |
||||
* "1002 indicates that an endpoint is terminating the connection due to a protocol |
||||
* error." |
||||
*/ |
||||
public static final CloseStatus PROTOCOL_ERROR = new CloseStatus(1002); |
||||
|
||||
/** |
||||
* "1003 indicates that an endpoint is terminating the connection because it has |
||||
* received a type of data it cannot accept (e.g., an endpoint that understands only |
||||
* text data MAY send this if it receives a binary message)." |
||||
*/ |
||||
public static final CloseStatus NOT_ACCEPTABLE = new CloseStatus(1003); |
||||
|
||||
// 10004: Reserved.
|
||||
// The specific meaning might be defined in the future.
|
||||
|
||||
/** |
||||
* "1005 is a reserved value and MUST NOT be set as a status code in a Close control |
||||
* frame by an endpoint. It is designated for use in applications expecting a status |
||||
* code to indicate that no status code was actually present." |
||||
*/ |
||||
public static final CloseStatus NO_STATUS_CODE = new CloseStatus(1005); |
||||
|
||||
/** |
||||
* "1006 is a reserved value and MUST NOT be set as a status code in a Close control |
||||
* frame by an endpoint. It is designated for use in applications expecting a status |
||||
* code to indicate that the connection was closed abnormally, e.g., without sending |
||||
* or receiving a Close control frame." |
||||
*/ |
||||
public static final CloseStatus NO_CLOSE_FRAME = new CloseStatus(1006); |
||||
|
||||
/** |
||||
* "1007 indicates that an endpoint is terminating the connection because it has |
||||
* received data within a message that was not consistent with the type of the message |
||||
* (e.g., non-UTF-8 [RFC3629] data within a text message)." |
||||
*/ |
||||
public static final CloseStatus BAD_DATA = new CloseStatus(1007); |
||||
|
||||
/** |
||||
* "1008 indicates that an endpoint is terminating the connection because it has |
||||
* received a message that violates its policy. This is a generic status code that can |
||||
* be returned when there is no other more suitable status code (e.g., 1003 or 1009) |
||||
* or if there is a need to hide specific details about the policy." |
||||
*/ |
||||
public static final CloseStatus POLICY_VIOLATION = new CloseStatus(1008); |
||||
|
||||
/** |
||||
* "1009 indicates that an endpoint is terminating the connection because it has |
||||
* received a message that is too big for it to process." |
||||
*/ |
||||
public static final CloseStatus TOO_BIG_TO_PROCESS = new CloseStatus(1009); |
||||
|
||||
/** |
||||
* "1010 indicates that an endpoint (client) is terminating the connection because it |
||||
* has expected the server to negotiate one or more extension, but the server didn't |
||||
* return them in the response message of the WebSocket handshake. The list of |
||||
* extensions that are needed SHOULD appear in the /reason/ part of the Close frame. |
||||
* Note that this status code is not used by the server, because it can fail the |
||||
* WebSocket handshake instead." |
||||
*/ |
||||
public static final CloseStatus REQUIRED_EXTENSION = new CloseStatus(1010); |
||||
|
||||
/** |
||||
* "1011 indicates that a server is terminating the connection because it encountered |
||||
* an unexpected condition that prevented it from fulfilling the request." |
||||
*/ |
||||
public static final CloseStatus SERVER_ERROR = new CloseStatus(1011); |
||||
|
||||
/** |
||||
* "1012 indicates that the service is restarted. A client may reconnect, and if it |
||||
* chooses to do, should reconnect using a randomized delay of 5 - 30s." |
||||
*/ |
||||
public static final CloseStatus SERVICE_RESTARTED = new CloseStatus(1012); |
||||
|
||||
/** |
||||
* "1013 indicates that the service is experiencing overload. A client should only |
||||
* connect to a different IP (when there are multiple for the target) or reconnect to |
||||
* the same IP upon user action." |
||||
*/ |
||||
public static final CloseStatus SERVICE_OVERLOAD = new CloseStatus(1013); |
||||
|
||||
/** |
||||
* "1015 is a reserved value and MUST NOT be set as a status code in a Close control |
||||
* frame by an endpoint. It is designated for use in applications expecting a status |
||||
* code to indicate that the connection was closed due to a failure to perform a TLS |
||||
* handshake (e.g., the server certificate can't be verified)." |
||||
*/ |
||||
public static final CloseStatus TLS_HANDSHAKE_FAILURE = new CloseStatus(1015); |
||||
|
||||
|
||||
private final int code; |
||||
|
||||
private final String reason; |
||||
|
||||
|
||||
/** |
||||
* Create a new {@link CloseStatus} instance. |
||||
* @param code the status code |
||||
*/ |
||||
public CloseStatus(int code) { |
||||
this(code, null); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link CloseStatus} instance. |
||||
* @param code the status code |
||||
* @param reason the reason |
||||
*/ |
||||
public CloseStatus(int code, String reason) { |
||||
Assert.isTrue((code >= 1000 && code < 5000), "Invalid status code"); |
||||
this.code = code; |
||||
this.reason = reason; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the status code. |
||||
*/ |
||||
public int getCode() { |
||||
return this.code; |
||||
} |
||||
|
||||
/** |
||||
* Return the reason, or {@code null} if none. |
||||
*/ |
||||
public String getReason() { |
||||
return this.reason; |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link CloseStatus} from this one with the specified reason. |
||||
* @param reason the reason |
||||
* @return a new {@link CloseStatus} instance |
||||
*/ |
||||
public CloseStatus withReason(String reason) { |
||||
Assert.hasText(reason, "Reason must not be empty"); |
||||
return new CloseStatus(this.code, reason); |
||||
} |
||||
|
||||
|
||||
public boolean equalsCode(CloseStatus other) { |
||||
return (this.code == other.code); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object other) { |
||||
if (this == other) { |
||||
return true; |
||||
} |
||||
if (!(other instanceof CloseStatus)) { |
||||
return false; |
||||
} |
||||
CloseStatus otherStatus = (CloseStatus) other; |
||||
return (this.code == otherStatus.code && |
||||
ObjectUtils.nullSafeEquals(this.reason, otherStatus.reason)); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return this.code * 29 + ObjectUtils.nullSafeHashCode(this.reason); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "CloseStatus[code=" + this.code + ", reason=" + this.reason + "]"; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright 2002-2016 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; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
/** |
||||
* Handler for a WebSocket-style session interaction. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public interface WebSocketHandler { |
||||
|
||||
/** |
||||
* Return the list of sub-protocols supported by this handler. |
||||
* <p>By default an empty list is returned. |
||||
*/ |
||||
default List<String> getSubProtocols() { |
||||
return Collections.emptyList(); |
||||
} |
||||
|
||||
/** |
||||
* Handle the given WebSocket session. |
||||
* @param session the session |
||||
* @return signals completion for session handling |
||||
*/ |
||||
Mono<Void> handle(WebSocketSession session); |
||||
|
||||
} |
||||
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
/* |
||||
* Copyright 2002-2016 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; |
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* Representation of a WebSocket message. |
||||
* Use one of the static factory methods in this class to create a message. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public class WebSocketMessage { |
||||
|
||||
private final Type type; |
||||
|
||||
private final DataBuffer payload; |
||||
|
||||
|
||||
/** |
||||
* Private constructor. See static factory methods. |
||||
*/ |
||||
private WebSocketMessage(Type type, DataBuffer payload) { |
||||
Assert.notNull(type, "'type' must not be null"); |
||||
Assert.notNull(payload, "'payload' must not be null"); |
||||
this.type = type; |
||||
this.payload = payload; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the message type (text, binary, etc). |
||||
*/ |
||||
public Type getType() { |
||||
return this.type; |
||||
} |
||||
|
||||
/** |
||||
* Return the message payload. |
||||
*/ |
||||
public DataBuffer getPayload() { |
||||
return this.payload; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean equals(Object other) { |
||||
if (this == other) { |
||||
return true; |
||||
} |
||||
if (!(other instanceof WebSocketMessage)) { |
||||
return false; |
||||
} |
||||
WebSocketMessage otherMessage = (WebSocketMessage) other; |
||||
return (this.type.equals(otherMessage.type) && |
||||
ObjectUtils.nullSafeEquals(this.payload, otherMessage.payload)); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return this.type.hashCode() * 29 + this.payload.hashCode(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Factory method to create a text WebSocket message. |
||||
*/ |
||||
public static WebSocketMessage text(DataBuffer payload) { |
||||
return create(Type.TEXT, payload); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a binary WebSocket message. |
||||
*/ |
||||
public static WebSocketMessage binary(DataBuffer payload) { |
||||
return create(Type.BINARY, payload); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a ping WebSocket message. |
||||
*/ |
||||
public static WebSocketMessage ping(DataBuffer payload) { |
||||
return create(Type.PING, payload); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a pong WebSocket message. |
||||
*/ |
||||
public static WebSocketMessage pong(DataBuffer payload) { |
||||
return create(Type.PONG, payload); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a WebSocket message of the given type. |
||||
*/ |
||||
public static WebSocketMessage create(Type type, DataBuffer payload) { |
||||
return new WebSocketMessage(type, payload); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* WebSocket message types. |
||||
*/ |
||||
public enum Type { TEXT, BINARY, PING, PONG } |
||||
|
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* Copyright 2002-2016 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; |
||||
|
||||
import java.net.URI; |
||||
|
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
|
||||
/** |
||||
* Representation for a WebSocket session. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public interface WebSocketSession { |
||||
|
||||
/** |
||||
* Return the id for the session. |
||||
*/ |
||||
String getId(); |
||||
|
||||
/** |
||||
* Return the WebSocket endpoint URI. |
||||
*/ |
||||
URI getUri(); |
||||
|
||||
/** |
||||
* Get the flux of incoming messages. |
||||
* <p><strong>Note:</strong> the caller of this method is responsible for |
||||
* releasing the DataBuffer payload of each message after consuming it |
||||
* on runtimes where a {@code PooledByteBuffer} is used such as Netty. |
||||
* @see org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer) |
||||
*/ |
||||
Flux<WebSocketMessage> receive(); |
||||
|
||||
/** |
||||
* Write the given messages to the WebSocket connection. |
||||
* @param messages the messages to write |
||||
*/ |
||||
Mono<Void> send(Publisher<WebSocketMessage> messages); |
||||
|
||||
/** |
||||
* Close the WebSocket session with {@link CloseStatus#NORMAL}. |
||||
*/ |
||||
default Mono<Void> close() { |
||||
return close(CloseStatus.NORMAL); |
||||
} |
||||
|
||||
/** |
||||
* Close the WebSocket session with the given status. |
||||
* @param status the close status |
||||
*/ |
||||
Mono<Void> close(CloseStatus status); |
||||
|
||||
} |
||||
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.net.URI; |
||||
|
||||
import io.reactivex.netty.protocol.http.ws.WebSocketConnection; |
||||
import reactor.core.publisher.Mono; |
||||
import rx.Observable; |
||||
import rx.RxReactiveStreams; |
||||
|
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
|
||||
/** |
||||
* RxNetty {@code WebSocketHandler} implementation adapting and delegating to a |
||||
* Spring {@link WebSocketHandler}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public class RxNettyWebSocketHandlerAdapter |
||||
implements io.reactivex.netty.protocol.http.ws.server.WebSocketHandler { |
||||
|
||||
private final URI uri; |
||||
|
||||
private final NettyDataBufferFactory bufferFactory; |
||||
|
||||
private final WebSocketHandler handler; |
||||
|
||||
|
||||
public RxNettyWebSocketHandlerAdapter(ServerHttpRequest request, ServerHttpResponse response, |
||||
WebSocketHandler handler) { |
||||
|
||||
Assert.notNull("'request' is required"); |
||||
Assert.notNull("'response' is required"); |
||||
Assert.notNull("'handler' handler is required"); |
||||
|
||||
this.uri = request.getURI(); |
||||
this.bufferFactory = (NettyDataBufferFactory) response.bufferFactory(); |
||||
this.handler = handler; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public Observable<Void> handle(WebSocketConnection connection) { |
||||
Mono<Void> result = this.handler.handle(createSession(connection)); |
||||
return RxReactiveStreams.toObservable(result); |
||||
} |
||||
|
||||
private RxNettyWebSocketSession createSession(WebSocketConnection conn) { |
||||
return new RxNettyWebSocketSession(conn, this.uri, this.bufferFactory); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.net.URI; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import io.netty.buffer.ByteBuf; |
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame; |
||||
import io.reactivex.netty.protocol.http.ws.WebSocketConnection; |
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
import rx.Observable; |
||||
import rx.RxReactiveStreams; |
||||
|
||||
import org.springframework.core.io.buffer.NettyDataBuffer; |
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.web.reactive.socket.CloseStatus; |
||||
import org.springframework.web.reactive.socket.WebSocketMessage; |
||||
|
||||
/** |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public class RxNettyWebSocketSession extends WebSocketSessionSupport<WebSocketConnection> { |
||||
|
||||
private static final Map<Class<?>, WebSocketMessage.Type> MESSAGE_TYPES; |
||||
|
||||
static { |
||||
MESSAGE_TYPES = new HashMap<>(4); |
||||
MESSAGE_TYPES.put(TextWebSocketFrame.class, WebSocketMessage.Type.TEXT); |
||||
MESSAGE_TYPES.put(BinaryWebSocketFrame.class, WebSocketMessage.Type.BINARY); |
||||
MESSAGE_TYPES.put(PingWebSocketFrame.class, WebSocketMessage.Type.PING); |
||||
MESSAGE_TYPES.put(PongWebSocketFrame.class, WebSocketMessage.Type.PONG); |
||||
} |
||||
|
||||
|
||||
private final String id; |
||||
|
||||
private final URI uri; |
||||
|
||||
private final NettyDataBufferFactory bufferFactory; |
||||
|
||||
|
||||
public RxNettyWebSocketSession(WebSocketConnection conn, URI uri, NettyDataBufferFactory factory) { |
||||
super(conn); |
||||
Assert.notNull(uri, "'uri' is required."); |
||||
Assert.notNull(uri, "'bufferFactory' is required."); |
||||
this.id = ObjectUtils.getIdentityHexString(getDelegate()); |
||||
this.uri = uri; |
||||
this.bufferFactory = factory; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public String getId() { |
||||
return this.id; |
||||
} |
||||
|
||||
@Override |
||||
public URI getUri() { |
||||
return this.uri; |
||||
} |
||||
|
||||
@Override |
||||
public Flux<WebSocketMessage> receive() { |
||||
return Flux.from(RxReactiveStreams.toPublisher(getDelegate().getInput())) |
||||
.filter(frame -> !(frame instanceof CloseWebSocketFrame)) |
||||
.window() |
||||
.concatMap(flux -> flux.takeUntil(WebSocketFrame::isFinalFragment).buffer()) |
||||
.map(this::toMessage); |
||||
} |
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent") |
||||
private WebSocketMessage toMessage(List<WebSocketFrame> frames) { |
||||
Class<?> frameType = frames.get(0).getClass(); |
||||
if (frames.size() == 1) { |
||||
NettyDataBuffer buffer = this.bufferFactory.wrap(frames.get(0).content()); |
||||
return WebSocketMessage.create(MESSAGE_TYPES.get(frameType), buffer); |
||||
} |
||||
return frames.stream() |
||||
.map(socketFrame -> bufferFactory.wrap(socketFrame.content())) |
||||
.reduce(NettyDataBuffer::write) |
||||
.map(buffer -> WebSocketMessage.create(MESSAGE_TYPES.get(frameType), buffer)) |
||||
.get(); |
||||
} |
||||
|
||||
@Override |
||||
public Mono<Void> send(Publisher<WebSocketMessage> messages) { |
||||
Observable<WebSocketFrame> frames = RxReactiveStreams.toObservable(messages).map(this::toFrame); |
||||
Observable<Void> completion = getDelegate().write(frames); |
||||
return Mono.from(RxReactiveStreams.toPublisher(completion)); |
||||
} |
||||
|
||||
private WebSocketFrame toFrame(WebSocketMessage message) { |
||||
ByteBuf byteBuf = NettyDataBufferFactory.toByteBuf(message.getPayload()); |
||||
if (WebSocketMessage.Type.TEXT.equals(message.getType())) { |
||||
return new TextWebSocketFrame(byteBuf); |
||||
} |
||||
else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { |
||||
return new BinaryWebSocketFrame(byteBuf); |
||||
} |
||||
else if (WebSocketMessage.Type.PING.equals(message.getType())) { |
||||
return new PingWebSocketFrame(byteBuf); |
||||
} |
||||
else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { |
||||
return new PongWebSocketFrame(byteBuf); |
||||
} |
||||
else { |
||||
throw new IllegalArgumentException("Unexpected message type: " + message.getType()); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected Mono<Void> closeInternal(CloseStatus status) { |
||||
return Mono.from(RxReactiveStreams.toPublisher(getDelegate().close())); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright 2002-2016 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 org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.reactive.socket.CloseStatus; |
||||
import org.springframework.web.reactive.socket.WebSocketSession; |
||||
|
||||
/** |
||||
* Base class for {@link WebSocketSession} implementations wrapping and |
||||
* delegating to the native WebSocket session (or connection) of the underlying |
||||
* WebSocket runtime. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public abstract class WebSocketSessionSupport<T> implements WebSocketSession { |
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass()); |
||||
|
||||
|
||||
private final T delegate; |
||||
|
||||
|
||||
/** |
||||
* Create a new instance and associate the given attributes with it. |
||||
* @param delegate the underlying WebSocket connection |
||||
*/ |
||||
protected WebSocketSessionSupport(T delegate) { |
||||
Assert.notNull(delegate, "'delegate' session is required."); |
||||
this.delegate = delegate; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the native session of the underlying runtime. |
||||
*/ |
||||
public T getDelegate() { |
||||
return this.delegate; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public final Mono<Void> close(CloseStatus status) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Closing " + this); |
||||
} |
||||
return closeInternal(status); |
||||
} |
||||
|
||||
protected abstract Mono<Void> closeInternal(CloseStatus status); |
||||
|
||||
|
||||
@Override |
||||
public String toString() { |
||||
return getClass().getSimpleName() + "[id=" + getId() + ", uri=" + getUri() + "]"; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
/** |
||||
* Classes adapting Spring's Reactive WebSocket API to and from WebSocket runtimes. |
||||
*/ |
||||
package org.springframework.web.reactive.socket.adapter; |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
/** |
||||
* Abstractions and support classes for WebSocket interactions. |
||||
*/ |
||||
package org.springframework.web.reactive.socket; |
||||
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* A strategy for upgrading an HTTP request to a WebSocket interaction depending |
||||
* on the underlying HTTP runtime. |
||||
* |
||||
* <p>Typically there is one such strategy for every {@link ServerHttpRequest} |
||||
* and {@link ServerHttpResponse} implementation type except in the case of |
||||
* Servlet containers for which there is no standard API to upgrade a request. |
||||
* JSR-356 does have programmatic endpoint registration but that is only |
||||
* intended for use on startup and not per request. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public interface RequestUpgradeStrategy { |
||||
|
||||
/** |
||||
* Upgrade the request to a WebSocket interaction and adapt the given |
||||
* Spring {@link WebSocketHandler} to the underlying runtime WebSocket API. |
||||
* @param exchange the current exchange |
||||
* @param webSocketHandler handler for WebSocket session |
||||
* @return a completion Mono for the WebSocket session handling |
||||
*/ |
||||
Mono<Void> upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler); |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* A service to delegate WebSocket-related HTTP requests to. |
||||
* |
||||
* <p>For a straight-up WebSocket endpoint this means handling the initial |
||||
* handshake request but for a SockJS endpoint this means handling all HTTP |
||||
* requests defined in the SockJS protocol. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
* @see HandshakeWebSocketService |
||||
*/ |
||||
public interface WebSocketService { |
||||
|
||||
/** |
||||
* Handle the HTTP request and use the given {@link WebSocketHandler}. |
||||
* @param exchange the current exchange |
||||
* @param webSocketHandler handler for WebSocket session |
||||
* @return a completion Mono for the WebSocket session handling |
||||
*/ |
||||
Mono<Void> handleRequest(ServerWebExchange exchange, WebSocketHandler webSocketHandler); |
||||
|
||||
} |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
/** |
||||
* Server support for WebSocket interactions. |
||||
*/ |
||||
package org.springframework.web.reactive.socket.server; |
||||
@ -0,0 +1,155 @@
@@ -0,0 +1,155 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server.support; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; |
||||
import org.springframework.web.reactive.socket.server.WebSocketService; |
||||
import org.springframework.web.server.MethodNotAllowedException; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* A {@code WebSocketService} implementation that handles a WebSocket handshake |
||||
* and upgrades to a WebSocket interaction through the configured or |
||||
* auto-detected {@link RequestUpgradeStrategy}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public class HandshakeWebSocketService implements WebSocketService { |
||||
|
||||
private static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; |
||||
|
||||
|
||||
private static final boolean rxNettyPresent = ClassUtils.isPresent( |
||||
"io.reactivex.netty.protocol.http.ws.WebSocketConnection", |
||||
HandshakeWebSocketService.class.getClassLoader()); |
||||
|
||||
|
||||
protected static final Log logger = LogFactory.getLog(HandshakeWebSocketService.class); |
||||
|
||||
|
||||
private final RequestUpgradeStrategy upgradeStrategy; |
||||
|
||||
|
||||
/** |
||||
* Default constructor automatic, classpath detection based discovery of the |
||||
* {@link RequestUpgradeStrategy} to use. |
||||
*/ |
||||
public HandshakeWebSocketService() { |
||||
this(initUpgradeStrategy()); |
||||
} |
||||
|
||||
/** |
||||
* Alternative constructor with the {@link RequestUpgradeStrategy} to use. |
||||
* @param upgradeStrategy the strategy to use |
||||
*/ |
||||
public HandshakeWebSocketService(RequestUpgradeStrategy upgradeStrategy) { |
||||
Assert.notNull(upgradeStrategy, "'upgradeStrategy' is required"); |
||||
this.upgradeStrategy = upgradeStrategy; |
||||
} |
||||
|
||||
private static RequestUpgradeStrategy initUpgradeStrategy() { |
||||
String className; |
||||
if (rxNettyPresent) { |
||||
className = "RxNettyRequestUpgradeStrategy"; |
||||
} |
||||
else { |
||||
throw new IllegalStateException("No suitable default RequestUpgradeStrategy found"); |
||||
} |
||||
|
||||
try { |
||||
className = HandshakeWebSocketService.class.getPackage().getName() + "." + className; |
||||
Class<?> clazz = ClassUtils.forName(className, HandshakeWebSocketService.class.getClassLoader()); |
||||
return (RequestUpgradeStrategy) ReflectionUtils.accessibleConstructor(clazz).newInstance(); |
||||
} |
||||
catch (Throwable ex) { |
||||
throw new IllegalStateException( |
||||
"Failed to instantiate RequestUpgradeStrategy: " + className, ex); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the {@link RequestUpgradeStrategy} for WebSocket requests. |
||||
*/ |
||||
public RequestUpgradeStrategy getUpgradeStrategy() { |
||||
return this.upgradeStrategy; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public Mono<Void> handleRequest(ServerWebExchange exchange, WebSocketHandler webSocketHandler) { |
||||
|
||||
ServerHttpRequest request = exchange.getRequest(); |
||||
ServerHttpResponse response = exchange.getResponse(); |
||||
|
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("Processing " + request.getMethod() + " " + request.getURI()); |
||||
} |
||||
|
||||
if (HttpMethod.GET != request.getMethod()) { |
||||
return Mono.error(new MethodNotAllowedException( |
||||
request.getMethod().name(), Collections.singleton("GET"))); |
||||
} |
||||
|
||||
if (!isWebSocketUpgrade(request)) { |
||||
response.setStatusCode(HttpStatus.BAD_REQUEST); |
||||
return response.setComplete(); |
||||
} |
||||
|
||||
return getUpgradeStrategy().upgrade(exchange, webSocketHandler); |
||||
} |
||||
|
||||
private boolean isWebSocketUpgrade(ServerHttpRequest request) { |
||||
if (!"WebSocket".equalsIgnoreCase(request.getHeaders().getUpgrade())) { |
||||
if (logger.isErrorEnabled()) { |
||||
logger.error("Invalid 'Upgrade' header: " + request.getHeaders()); |
||||
} |
||||
return false; |
||||
} |
||||
List<String> connectionValue = request.getHeaders().getConnection(); |
||||
if (!connectionValue.contains("Upgrade") && !connectionValue.contains("upgrade")) { |
||||
if (logger.isErrorEnabled()) { |
||||
logger.error("Invalid 'Connection' header: " + request.getHeaders()); |
||||
} |
||||
return false; |
||||
} |
||||
String key = request.getHeaders().getFirst(SEC_WEBSOCKET_KEY); |
||||
if (key == null) { |
||||
if (logger.isErrorEnabled()) { |
||||
logger.error("Missing \"Sec-WebSocket-Key\" header"); |
||||
} |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server.support; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
import org.springframework.web.reactive.HandlerAdapter; |
||||
import org.springframework.web.reactive.HandlerResult; |
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
import org.springframework.web.reactive.socket.server.WebSocketService; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* {@code HandlerAdapter} that allows using a {@link WebSocketHandler} contract |
||||
* with the generic {@link DispatcherHandler} mapping URLs directly to such |
||||
* handlers. Requests are handled through the configured {@link WebSocketService}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public class WebSocketHandlerAdapter implements HandlerAdapter { |
||||
|
||||
private final WebSocketService webSocketService; |
||||
|
||||
|
||||
/** |
||||
* Default constructor that creates and uses a |
||||
* {@link HandshakeWebSocketService} for a straight-up WebSocket interaction, |
||||
* i.e. treating incoming requests as WebSocket handshake requests. |
||||
*/ |
||||
public WebSocketHandlerAdapter() { |
||||
this(new HandshakeWebSocketService()); |
||||
} |
||||
|
||||
/** |
||||
* Alternative constructor with the {@link WebSocketService} to use. |
||||
*/ |
||||
public WebSocketHandlerAdapter(WebSocketService webSocketService) { |
||||
Assert.notNull(webSocketService, "'webSocketService' is required"); |
||||
this.webSocketService = webSocketService; |
||||
} |
||||
|
||||
|
||||
public WebSocketService getWebSocketService() { |
||||
return this.webSocketService; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean supports(Object handler) { |
||||
return WebSocketHandler.class.isAssignableFrom(handler.getClass()); |
||||
} |
||||
|
||||
@Override |
||||
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) { |
||||
WebSocketHandler webSocketHandler = (WebSocketHandler) handler; |
||||
return getWebSocketService().handleRequest(exchange, webSocketHandler).then(Mono.empty()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
/** |
||||
* Server-side support classes for WebSocket requests. |
||||
*/ |
||||
package org.springframework.web.reactive.socket.server.support; |
||||
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server.upgrade; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
import rx.Observable; |
||||
import rx.RxReactiveStreams; |
||||
|
||||
import org.springframework.http.server.reactive.RxNettyServerHttpRequest; |
||||
import org.springframework.http.server.reactive.RxNettyServerHttpResponse; |
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
import org.springframework.web.reactive.socket.adapter.RxNettyWebSocketHandlerAdapter; |
||||
import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* A {@link RequestUpgradeStrategy} for use with RxNetty. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public class RxNettyRequestUpgradeStrategy implements RequestUpgradeStrategy { |
||||
|
||||
@Override |
||||
public Mono<Void> upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler) { |
||||
|
||||
RxNettyServerHttpRequest request = (RxNettyServerHttpRequest) exchange.getRequest(); |
||||
RxNettyServerHttpResponse response = (RxNettyServerHttpResponse) exchange.getResponse(); |
||||
|
||||
RxNettyWebSocketHandlerAdapter rxNettyHandler = |
||||
new RxNettyWebSocketHandlerAdapter(request, response, webSocketHandler); |
||||
|
||||
Observable<Void> completion = response.getRxNettyResponse() |
||||
.acceptWebSocketUpgrade(rxNettyHandler) |
||||
.subprotocol(getSubProtocols(webSocketHandler)); |
||||
|
||||
return Mono.from(RxReactiveStreams.toPublisher(completion)); |
||||
} |
||||
|
||||
private static String[] getSubProtocols(WebSocketHandler webSocketHandler) { |
||||
List<String> subProtocols = webSocketHandler.getSubProtocols(); |
||||
return subProtocols.toArray(new String[subProtocols.size()]); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
/** |
||||
* Holds implementations of |
||||
* {@link org.springframework.web.reactive.socket.server.RequestUpgradeStrategy}. |
||||
*/ |
||||
package org.springframework.web.reactive.socket.server.upgrade; |
||||
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server; |
||||
|
||||
import org.junit.After; |
||||
import org.junit.Before; |
||||
import org.junit.runner.RunWith; |
||||
import org.junit.runners.Parameterized; |
||||
import org.junit.runners.Parameterized.Parameter; |
||||
import org.junit.runners.Parameterized.Parameters; |
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.http.server.reactive.HttpHandler; |
||||
import org.springframework.http.server.reactive.bootstrap.HttpServer; |
||||
import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer; |
||||
import org.springframework.util.SocketUtils; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; |
||||
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; |
||||
import org.springframework.web.reactive.socket.server.upgrade.RxNettyRequestUpgradeStrategy; |
||||
|
||||
/** |
||||
* Base class for WebSocket integration tests involving a server-side |
||||
* {@code WebSocketHandler}. Sub-classes to return a Spring configuration class
|
||||
* via {@link #getWebConfigClass()} containing a SimpleUrlHandlerMapping with |
||||
* pattern-to-WebSocketHandler mappings. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@RunWith(Parameterized.class) |
||||
@SuppressWarnings({"unused", "WeakerAccess"}) |
||||
public abstract class AbstractWebSocketHandlerIntegrationTests { |
||||
|
||||
protected int port; |
||||
|
||||
@Parameter(0) |
||||
public HttpServer server; |
||||
|
||||
@Parameter(1) |
||||
public Class<?> handlerAdapterConfigClass; |
||||
|
||||
|
||||
@Parameters |
||||
public static Object[][] arguments() { |
||||
return new Object[][] { |
||||
{new RxNettyHttpServer(), RxNettyConfig.class} |
||||
}; |
||||
} |
||||
|
||||
|
||||
@Before |
||||
public void setup() throws Exception { |
||||
this.port = SocketUtils.findAvailableTcpPort(); |
||||
this.server.setPort(this.port); |
||||
this.server.setHandler(createHttpHandler()); |
||||
this.server.afterPropertiesSet(); |
||||
this.server.start(); |
||||
} |
||||
|
||||
private HttpHandler createHttpHandler() { |
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); |
||||
context.register(DispatcherConfig.class, this.handlerAdapterConfigClass); |
||||
context.register(getWebConfigClass()); |
||||
context.refresh(); |
||||
return DispatcherHandler.toHttpHandler(context); |
||||
} |
||||
|
||||
protected abstract Class<?> getWebConfigClass(); |
||||
|
||||
@After |
||||
public void tearDown() throws Exception { |
||||
this.server.stop(); |
||||
} |
||||
|
||||
|
||||
@Configuration |
||||
static class DispatcherConfig { |
||||
|
||||
@Bean |
||||
public DispatcherHandler webHandler() { |
||||
return new DispatcherHandler(); |
||||
} |
||||
} |
||||
|
||||
static abstract class AbstractHandlerAdapterConfig { |
||||
|
||||
@Bean |
||||
public WebSocketHandlerAdapter handlerAdapter() { |
||||
RequestUpgradeStrategy strategy = createUpgradeStrategy(); |
||||
WebSocketService service = new HandshakeWebSocketService(strategy); |
||||
return new WebSocketHandlerAdapter(service); |
||||
} |
||||
|
||||
protected abstract RequestUpgradeStrategy createUpgradeStrategy(); |
||||
|
||||
} |
||||
|
||||
@Configuration |
||||
static class RxNettyConfig extends AbstractHandlerAdapterConfig { |
||||
|
||||
@Override |
||||
protected RequestUpgradeStrategy createUpgradeStrategy() { |
||||
return new RxNettyRequestUpgradeStrategy(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
|
||||
/* |
||||
* Copyright 2002-2016 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.server; |
||||
|
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; |
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame; |
||||
import io.reactivex.netty.protocol.http.client.HttpClient; |
||||
import io.reactivex.netty.protocol.http.ws.client.WebSocketResponse; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
import rx.Observable; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.web.reactive.HandlerMapping; |
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; |
||||
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||
import org.springframework.web.reactive.socket.WebSocketSession; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
/** |
||||
* Basic WebSocket integration |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@SuppressWarnings({"unused", "WeakerAccess"}) |
||||
public class BasicWebSocketHandlerIntegrationTests extends AbstractWebSocketHandlerIntegrationTests { |
||||
|
||||
|
||||
@Override |
||||
protected Class<?> getWebConfigClass() { |
||||
return WebConfig.class; |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void echo() throws Exception { |
||||
Observable<String> messages = Observable.range(1, 10).map(i -> "Interval " + i); |
||||
List<String> actual = HttpClient.newClient("localhost", this.port) |
||||
.createGet("/echo") |
||||
.requestWebSocketUpgrade() |
||||
.flatMap(WebSocketResponse::getWebSocketConnection) |
||||
.flatMap(conn -> conn.write(messages |
||||
.map(TextWebSocketFrame::new) |
||||
.cast(WebSocketFrame.class) |
||||
.concatWith(Observable.just(new CloseWebSocketFrame()))) |
||||
.cast(WebSocketFrame.class) |
||||
.mergeWith(conn.getInput()) |
||||
) |
||||
.take(10) |
||||
.map(frame -> frame.content().toString(StandardCharsets.UTF_8)) |
||||
.toList().toBlocking().first(); |
||||
List<String> expected = messages.toList().toBlocking().first(); |
||||
assertEquals(expected, actual); |
||||
} |
||||
|
||||
|
||||
@Configuration |
||||
static class WebConfig { |
||||
|
||||
@Bean |
||||
public HandlerMapping handlerMapping() { |
||||
|
||||
Map<String, WebSocketHandler> map = new HashMap<>(); |
||||
map.put("/echo", new EchoWebSocketHandler()); |
||||
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); |
||||
mapping.setUrlMap(map); |
||||
return mapping; |
||||
} |
||||
|
||||
} |
||||
|
||||
private static class EchoWebSocketHandler implements WebSocketHandler { |
||||
|
||||
@Override |
||||
public Mono<Void> handle(WebSocketSession session) { |
||||
return session.send(session.receive()); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue