Starting point for reactive WebSocket support
Includes basic abstractions and an RxNetty support to start. Issue: SPR-14527
This commit is contained in:
@@ -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 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user