Starting point for reactive WebSocket support

Includes basic abstractions and an RxNetty support to start.

Issue: SPR-14527
This commit is contained in:
Rossen Stoyanchev
2016-11-16 13:55:04 -05:00
parent 8662b7773c
commit 637b6387ea
20 changed files with 1380 additions and 5 deletions

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}
}