Polish Reactor Netty TCP client support

This commit is contained in:
Rossen Stoyanchev 2016-11-30 17:46:29 -05:00
parent 870f61fd8e
commit 85c93f5d67
7 changed files with 163 additions and 173 deletions

View File

@ -29,11 +29,10 @@ import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient;
import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFuture;
/** /**
* A STOMP over TCP client that uses * A STOMP over TCP client that uses {@link ReactorNettyTcpClient}.
* {@link ReactorNettyTcpClient}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 4.2 * @since 5.0
*/ */
public class ReactorNettyTcpStompClient extends StompClientSupport { public class ReactorNettyTcpStompClient extends StompClientSupport {
@ -99,25 +98,21 @@ public class ReactorNettyTcpStompClient extends StompClientSupport {
* Create a new {@link ReactorNettyTcpClient} with Stomp specific configuration for * Create a new {@link ReactorNettyTcpClient} with Stomp specific configuration for
* encoding, decoding and hand-off. * encoding, decoding and hand-off.
* *
* @param relayHost target host * @param host target host
* @param relayPort target port * @param port target port
* @param decoder {@link StompDecoder} to use * @param decoder {@link StompDecoder} to use
* @return a new {@link TcpOperations} * @return a new {@link TcpOperations}
*/ */
protected static TcpOperations<byte[]> create(String relayHost, protected static TcpOperations<byte[]> create(String host, int port, StompDecoder decoder) {
int relayPort, return new ReactorNettyTcpClient<>(host, port,
StompDecoder decoder) { new ReactorNettyTcpClient.MessageHandlerConfiguration<>(
return new ReactorNettyTcpClient<>(relayHost, new DecodingFunction(decoder),
relayPort,
new ReactorNettyTcpClient.MessageHandlerConfiguration<>(new DecodingFunction(
decoder),
new EncodingConsumer(new StompEncoder()), new EncodingConsumer(new StompEncoder()),
128, 128,
Schedulers.newParallel("StompClient"))); Schedulers.newParallel("StompClient")));
} }
private static final class EncodingConsumer private static final class EncodingConsumer implements BiConsumer<ByteBuf, Message<byte[]>> {
implements BiConsumer<ByteBuf, Message<byte[]>> {
private final StompEncoder encoder; private final StompEncoder encoder;
@ -127,12 +122,11 @@ public class ReactorNettyTcpStompClient extends StompClientSupport {
@Override @Override
public void accept(ByteBuf byteBuf, Message<byte[]> message) { public void accept(ByteBuf byteBuf, Message<byte[]> message) {
byteBuf.writeBytes(encoder.encode(message)); byteBuf.writeBytes(this.encoder.encode(message));
} }
} }
private static final class DecodingFunction private static final class DecodingFunction implements Function<ByteBuf, List<Message<byte[]>>> {
implements Function<ByteBuf, List<Message<byte[]>>> {
private final StompDecoder decoder; private final StompDecoder decoder;

View File

@ -17,7 +17,6 @@
package org.springframework.messaging.tcp.reactor; package org.springframework.messaging.tcp.reactor;
import java.time.Duration; import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -33,51 +32,53 @@ import org.springframework.util.concurrent.ListenableFutureCallbackRegistry;
import org.springframework.util.concurrent.SuccessCallback; import org.springframework.util.concurrent.SuccessCallback;
/** /**
* Adapts a reactor {@link Mono} to {@link ListenableFuture} optionally converting * Adapts {@link Mono} to {@link ListenableFuture} optionally converting the
* the result Object type {@code <S>} to the expected target type {@code <T>}. * result Object type {@code <S>} to the expected target type {@code <T>}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 4.0 * @since 5.0
* @param <S> the type of object expected from the {@link Mono} * @param <S> the type of object expected from the {@link Mono}
* @param <T> the type of object expected from the {@link ListenableFuture} * @param <T> the type of object expected from the {@link ListenableFuture}
*/ */
abstract class AbstractMonoToListenableFutureAdapter<S, T> abstract class AbstractMonoToListenableFutureAdapter<S, T> implements ListenableFuture<T> {
implements ListenableFuture<T> {
private final MonoProcessor<S> promise; private final MonoProcessor<S> monoProcessor;
private final ListenableFutureCallbackRegistry<T> registry = new ListenableFutureCallbackRegistry<>(); private final ListenableFutureCallbackRegistry<T> registry = new ListenableFutureCallbackRegistry<>();
protected AbstractMonoToListenableFutureAdapter(Mono<S> promise) {
Assert.notNull(promise, "Mono must not be null"); protected AbstractMonoToListenableFutureAdapter(Mono<S> mono) {
this.promise = promise.doOnSuccess(result -> { Assert.notNull(mono, "'mono' must not be null");
T adapted; this.monoProcessor = mono
try { .doOnSuccess(result -> {
adapted = adapt(result); T adapted;
} try {
catch (Throwable ex) { adapted = adapt(result);
registry.failure(ex); }
return; catch (Throwable ex) {
} registry.failure(ex);
registry.success(adapted); return;
}) }
.doOnError(registry::failure) registry.success(adapted);
.subscribe(); })
.doOnError(this.registry::failure)
.subscribe();
} }
@Override @Override
public T get() throws InterruptedException { public T get() throws InterruptedException {
S result = this.promise.block(); S result = this.monoProcessor.block();
return adapt(result); return adapt(result);
} }
@Override @Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { public T get(long timeout, TimeUnit unit)
Objects.requireNonNull(unit, "unit"); throws InterruptedException, ExecutionException, TimeoutException {
S result = this.promise.block(Duration.ofMillis(TimeUnit.MILLISECONDS.convert(
timeout, Assert.notNull(unit);
unit))); Duration duration = Duration.ofMillis(TimeUnit.MILLISECONDS.convert(timeout, unit));
S result = this.monoProcessor.block(duration);
return adapt(result); return adapt(result);
} }
@ -86,18 +87,18 @@ abstract class AbstractMonoToListenableFutureAdapter<S, T>
if (isCancelled()) { if (isCancelled()) {
return false; return false;
} }
this.promise.cancel(); this.monoProcessor.cancel();
return true; return true;
} }
@Override @Override
public boolean isCancelled() { public boolean isCancelled() {
return this.promise.isCancelled(); return this.monoProcessor.isCancelled();
} }
@Override @Override
public boolean isDone() { public boolean isDone() {
return this.promise.isTerminated(); return this.monoProcessor.isTerminated();
} }
@Override @Override
@ -111,7 +112,6 @@ abstract class AbstractMonoToListenableFutureAdapter<S, T>
this.registry.addFailureCallback(failureCallback); this.registry.addFailureCallback(failureCallback);
} }
protected abstract T adapt(S result); protected abstract T adapt(S result);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,15 +19,14 @@ package org.springframework.messaging.tcp.reactor;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/** /**
* A Mono-to-ListenableFutureAdapter where the source and the target from * A Mono-to-ListenableFuture adapter where the source and the target from
* the Promise and the ListenableFuture respectively are of the same type. * the Promise and the ListenableFuture respectively are of the same type.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Stephane Maldini * @author Stephane Maldini
* @since 4.0 * @since 5.0
*/ */
class MonoToListenableFutureAdapter<T> extends class MonoToListenableFutureAdapter<T> extends AbstractMonoToListenableFutureAdapter<T, T> {
AbstractMonoToListenableFutureAdapter<T, T> {
public MonoToListenableFutureAdapter(Mono<T> mono) { public MonoToListenableFutureAdapter(Mono<T> mono) {

View File

@ -17,7 +17,6 @@
package org.springframework.messaging.tcp.reactor; package org.springframework.messaging.tcp.reactor;
import java.util.Collection; import java.util.Collection;
import java.util.Objects;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -58,16 +57,19 @@ import org.springframework.util.concurrent.ListenableFuture;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Stephane Maldini * @author Stephane Maldini
* @since 4.2 * @since 5.0
*/ */
public class ReactorNettyTcpClient<P> implements TcpOperations<P> { public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
private final TcpClient tcpClient; private final TcpClient tcpClient;
private final MessageHandlerConfiguration<P> configuration; private final MessageHandlerConfiguration<P> configuration;
private final ChannelGroup group;
private final ChannelGroup group;
private volatile boolean stopping; private volatile boolean stopping;
/** /**
* A constructor that creates a {@link TcpClient TcpClient} factory relying on * A constructor that creates a {@link TcpClient TcpClient} factory relying on
* Reactor Netty TCP threads. The number of Netty threads can be tweaked with * Reactor Netty TCP threads. The number of Netty threads can be tweaked with
@ -80,120 +82,116 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
* @param port the port to connect to * @param port the port to connect to
* @param configuration the client configuration * @param configuration the client configuration
*/ */
public ReactorNettyTcpClient(String host, public ReactorNettyTcpClient(String host, int port, MessageHandlerConfiguration<P> configuration) {
int port, this(opts -> opts.connect(host, port), configuration);
MessageHandlerConfiguration<P> configuration) {
this.configuration = Objects.requireNonNull(configuration, "configuration");
this.group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
this.tcpClient = TcpClient.create(options -> options.connect(host, port)
.channelGroup(group));
} }
/** /**
* A constructor with a configurator {@link Consumer} that will receive default {@link * A constructor with a configurator {@link Consumer} that will receive
* ClientOptions} from {@link TcpClient}. This might be used to add SSL or specific * default {@link ClientOptions} from {@link TcpClient}. This might be used
* network parameters to the generated client configuration. * to add SSL or specific network parameters to the generated client
* configuration.
* *
* @param tcpOptions the {@link Consumer} of {@link ClientOptions} shared to use by * @param tcpOptions callback for configuring shared {@link ClientOptions}
* connected handlers.
* @param configuration the client configuration * @param configuration the client configuration
*/ */
public ReactorNettyTcpClient(Consumer<? super ClientOptions> tcpOptions, public ReactorNettyTcpClient(Consumer<? super ClientOptions> tcpOptions,
MessageHandlerConfiguration<P> configuration) { MessageHandlerConfiguration<P> configuration) {
this.configuration = Objects.requireNonNull(configuration, "configuration");
Assert.notNull(configuration, "'configuration' is required");
this.group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); this.group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
this.tcpClient = this.tcpClient = TcpClient.create(opts -> tcpOptions.accept(opts.channelGroup(group)));
TcpClient.create(opts -> tcpOptions.accept(opts.channelGroup(group))); this.configuration = configuration;
} }
@Override @Override
public ListenableFuture<Void> connect(final TcpConnectionHandler<P> connectionHandler) { public ListenableFuture<Void> connect(final TcpConnectionHandler<P> handler) {
Assert.notNull(connectionHandler, "TcpConnectionHandler must not be null"); Assert.notNull(handler, "'handler' is required");
if (stopping) {
if (this.stopping) {
IllegalStateException ex = new IllegalStateException("Shutting down."); IllegalStateException ex = new IllegalStateException("Shutting down.");
connectionHandler.afterConnectFailure(ex); handler.afterConnectFailure(ex);
return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex)); return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex));
} }
MessageHandler<P> handler = Mono<Void> connectMono = this.tcpClient
new MessageHandler<>(connectionHandler, configuration); .newHandler(new MessageHandler<>(handler, this.configuration))
.doOnError(handler::afterConnectFailure)
.then();
Mono<Void> promise = tcpClient.newHandler(handler) return new MonoToListenableFutureAdapter<>(connectMono);
.doOnError(connectionHandler::afterConnectFailure)
.then();
return new MonoToListenableFutureAdapter<>(promise);
} }
@Override @Override
public ListenableFuture<Void> connect(TcpConnectionHandler<P> connectionHandler, public ListenableFuture<Void> connect(TcpConnectionHandler<P> handler, ReconnectStrategy strategy) {
ReconnectStrategy strategy) { Assert.notNull(handler, "'handler' is required");
Assert.notNull(connectionHandler, "TcpConnectionHandler must not be null"); Assert.notNull(strategy, "'reconnectStrategy' is required");
Assert.notNull(strategy, "ReconnectStrategy must not be null");
if (stopping) { if (this.stopping) {
IllegalStateException ex = new IllegalStateException("Shutting down."); IllegalStateException ex = new IllegalStateException("Shutting down.");
connectionHandler.afterConnectFailure(ex); handler.afterConnectFailure(ex);
return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex)); return new MonoToListenableFutureAdapter<>(Mono.<Void>error(ex));
} }
MessageHandler<P> handler = MonoProcessor<Void> connectMono = MonoProcessor.create();
new MessageHandler<>(connectionHandler, configuration);
MonoProcessor<Void> promise = MonoProcessor.create(); this.tcpClient.newHandler(new MessageHandler<>(handler, this.configuration))
.doOnNext(item -> {
if (!connectMono.isTerminated()) {
connectMono.onComplete();
}
})
.doOnError(ex -> {
if (!connectMono.isTerminated()) {
connectMono.onError(ex);
}
})
.then(NettyContext::onClose)
.retryWhen(new Reconnector<>(strategy))
.repeatWhen(new Reconnector<>(strategy))
.subscribe();
tcpClient.newHandler(handler) return new MonoToListenableFutureAdapter<>(connectMono);
.doOnNext(e -> {
if (!promise.isTerminated()) {
promise.onComplete();
}
})
.doOnError(e -> {
if (!promise.isTerminated()) {
promise.onError(e);
}
})
.then(NettyContext::onClose)
.retryWhen(new Reconnector<>(strategy))
.repeatWhen(new Reconnector<>(strategy))
.subscribe();
return new MonoToListenableFutureAdapter<>(promise);
} }
@Override @Override
public ListenableFuture<Void> shutdown() { public ListenableFuture<Void> shutdown() {
if (stopping) { if (this.stopping) {
return new MonoToListenableFutureAdapter<>(Mono.empty()); return new MonoToListenableFutureAdapter<>(Mono.empty());
} }
stopping = true; this.stopping = true;
Mono<Void> closing = ChannelFutureMono.from(group.close()); Mono<Void> completion = ChannelFutureMono.from(this.group.close());
if (configuration.scheduler != null) { if (this.configuration.scheduler != null) {
closing = completion = completion.doAfterTerminate((x, e) -> configuration.scheduler.shutdown());
closing.doAfterTerminate((x, e) -> configuration.scheduler.shutdown());
} }
return new MonoToListenableFutureAdapter<>(closing); return new MonoToListenableFutureAdapter<>(completion);
} }
/** /**
* A configuration holder * A configuration holder
*/ */
public static final class MessageHandlerConfiguration<P> { public static final class MessageHandlerConfiguration<P> {
private final Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder; private final Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder;
private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
private final int backlog;
private final Scheduler
scheduler;
public MessageHandlerConfiguration(Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder, private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
private final int backlog;
private final Scheduler scheduler;
public MessageHandlerConfiguration(
Function<? super ByteBuf, ? extends Collection<Message<P>>> decoder,
BiConsumer<? super ByteBuf, ? super Message<P>> encoder, BiConsumer<? super ByteBuf, ? super Message<P>> encoder,
int backlog, int backlog, Scheduler scheduler) {
Scheduler scheduler) {
this.decoder = decoder; this.decoder = decoder;
this.encoder = encoder; this.encoder = encoder;
this.backlog = backlog > 0 ? backlog : QueueSupplier.SMALL_BUFFER_SIZE; this.backlog = backlog > 0 ? backlog : QueueSupplier.SMALL_BUFFER_SIZE;
@ -201,34 +199,30 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
} }
} }
private static final class MessageHandler<P> implements BiFunction<NettyInbound, NettyOutbound, Publisher<Void>> { private static final class MessageHandler<P>
implements BiFunction<NettyInbound, NettyOutbound, Publisher<Void>> {
private final TcpConnectionHandler<P> connectionHandler; private final TcpConnectionHandler<P> connectionHandler;
private final MessageHandlerConfiguration<P> configuration; private final MessageHandlerConfiguration<P> configuration;
MessageHandler(TcpConnectionHandler<P> connectionHandler,
MessageHandlerConfiguration<P> configuration) { MessageHandler(TcpConnectionHandler<P> handler, MessageHandlerConfiguration<P> config) {
this.connectionHandler = connectionHandler; this.connectionHandler = handler;
this.configuration = configuration; this.configuration = config;
} }
@Override @Override
public Publisher<Void> apply(NettyInbound in, NettyOutbound out) { public Publisher<Void> apply(NettyInbound in, NettyOutbound out) {
Flux<Collection<Message<P>>> inbound = in.receive() Flux<Collection<Message<P>>> inbound = in.receive().map(configuration.decoder);
.map(configuration.decoder);
DirectProcessor<Void> promise = DirectProcessor.create(); DirectProcessor<Void> closeProcessor = DirectProcessor.create();
TcpConnection<P> tcpConnection = new ReactorNettyTcpConnection<>(in, TcpConnection<P> tcpConnection =
out, new ReactorNettyTcpConnection<>(in, out, configuration.encoder, closeProcessor);
configuration.encoder,
promise);
if (configuration.scheduler != null) { if (configuration.scheduler != null) {
configuration.scheduler.schedule(() -> connectionHandler.afterConnected( configuration.scheduler.schedule(() -> connectionHandler.afterConnected(tcpConnection));
tcpConnection)); inbound = inbound.publishOn(configuration.scheduler, configuration.backlog);
inbound =
inbound.publishOn(configuration.scheduler, configuration.backlog);
} }
else { else {
connectionHandler.afterConnected(tcpConnection); connectionHandler.afterConnected(tcpConnection);
@ -239,15 +233,16 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
connectionHandler::handleFailure, connectionHandler::handleFailure,
connectionHandler::afterConnectionClosed); connectionHandler::afterConnectionClosed);
return promise; return closeProcessor;
} }
} }
static final class Reconnector<T> implements Function<Flux<T>, Publisher<?>> { private static final class Reconnector<T> implements Function<Flux<T>, Publisher<?>> {
private final ReconnectStrategy strategy; private final ReconnectStrategy strategy;
Reconnector(ReconnectStrategy strategy) { Reconnector(ReconnectStrategy strategy) {
this.strategy = strategy; this.strategy = strategy;
} }
@ -255,9 +250,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
@Override @Override
public Publisher<?> apply(Flux<T> flux) { public Publisher<?> apply(Flux<T> flux) {
return flux.scan(1, (p, e) -> p++) return flux.scan(1, (p, e) -> p++)
.doOnCancel(() -> new Exception().printStackTrace()) .flatMap(attempt -> Mono.delayMillis(strategy.getTimeToNextAttempt(attempt)));
.flatMap(attempt -> Mono.delayMillis(strategy.getTimeToNextAttempt(
attempt)));
} }
} }

View File

@ -35,47 +35,52 @@ import org.springframework.util.concurrent.ListenableFuture;
* @param <P> the payload type of messages read or written to the TCP stream. * @param <P> the payload type of messages read or written to the TCP stream.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 4.2 * @since 5.0
*/ */
public class ReactorNettyTcpConnection<P> implements TcpConnection<P> { public class ReactorNettyTcpConnection<P> implements TcpConnection<P> {
private final NettyInbound in; private final NettyInbound inbound;
private final NettyOutbound out;
private final DirectProcessor<Void> close; private final NettyOutbound outbound;
private final DirectProcessor<Void> closeProcessor;
private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder; private final BiConsumer<? super ByteBuf, ? super Message<P>> encoder;
public ReactorNettyTcpConnection(NettyInbound in,
NettyOutbound out, public ReactorNettyTcpConnection(NettyInbound inbound, NettyOutbound outbound,
BiConsumer<? super ByteBuf, ? super Message<P>> encoder, BiConsumer<? super ByteBuf, ? super Message<P>> encoder,
DirectProcessor<Void> close) { DirectProcessor<Void> closeProcessor) {
this.out = out;
this.in = in; this.inbound = inbound;
this.outbound = outbound;
this.encoder = encoder; this.encoder = encoder;
this.close = close; this.closeProcessor = closeProcessor;
} }
@Override @Override
public ListenableFuture<Void> send(Message<P> message) { public ListenableFuture<Void> send(Message<P> message) {
ByteBuf byteBuf = in.channel().alloc().buffer(); ByteBuf byteBuf = this.inbound.channel().alloc().buffer();
encoder.accept(byteBuf, message); this.encoder.accept(byteBuf, message);
return new MonoToListenableFutureAdapter<>(out.send(Mono.just(byteBuf))); return new MonoToListenableFutureAdapter<>(this.outbound.send(Mono.just(byteBuf)));
} }
@Override @Override
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public void onReadInactivity(Runnable runnable, long inactivityDuration) { public void onReadInactivity(Runnable runnable, long inactivityDuration) {
in.onReadIdle(inactivityDuration, runnable); this.inbound.onReadIdle(inactivityDuration, runnable);
} }
@Override @Override
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public void onWriteInactivity(Runnable runnable, long inactivityDuration) { public void onWriteInactivity(Runnable runnable, long inactivityDuration) {
out.onWriteIdle(inactivityDuration, runnable); this.outbound.onWriteIdle(inactivityDuration, runnable);
} }
@Override @Override
public void close() { public void close() {
close.onComplete(); this.closeProcessor.onComplete();
} }
} }

View File

@ -24,10 +24,10 @@ import org.junit.Test;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.InvalidMimeTypeException;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/** /**
* Test fixture for {@link StompDecoder}. * Test fixture for {@link StompDecoder}.
@ -39,6 +39,7 @@ public class StompDecoderTests {
private final StompDecoder decoder = new StompDecoder(); private final StompDecoder decoder = new StompDecoder();
@Test @Test
public void decodeFrameWithCrLfEols() { public void decodeFrameWithCrLfEols() {
Message<byte[]> frame = decode("DISCONNECT\r\n\r\n\0"); Message<byte[]> frame = decode("DISCONNECT\r\n\r\n\0");

View File

@ -34,10 +34,10 @@ public class StompEncoderTests {
private final StompEncoder encoder = new StompEncoder(); private final StompEncoder encoder = new StompEncoder();
@Test @Test
public void encodeFrameWithNoHeadersAndNoBody() { public void encodeFrameWithNoHeadersAndNoBody() {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
assertEquals("DISCONNECT\n\n\0", new String(encoder.encode(frame))); assertEquals("DISCONNECT\n\n\0", new String(encoder.encode(frame)));
@ -48,20 +48,18 @@ public class StompEncoderTests {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setAcceptVersion("1.2"); headers.setAcceptVersion("1.2");
headers.setHost("github.org"); headers.setHost("github.org");
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
String frameString = new String(encoder.encode(frame)); String frameString = new String(encoder.encode(frame));
assertTrue("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(frameString) || "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals( assertTrue(
frameString)); "CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(frameString) ||
"CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(frameString));
} }
@Test @Test
public void encodeFrameWithHeadersThatShouldBeEscaped() { public void encodeFrameWithHeadersThatShouldBeEscaped() {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\");
Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); Message<byte[]> frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0", assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0",
@ -72,8 +70,8 @@ public class StompEncoderTests {
public void encodeFrameWithHeadersBody() { public void encodeFrameWithHeadersBody() {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.addNativeHeader("a", "alpha"); headers.addNativeHeader("a", "alpha");
Message<byte[]> frame = MessageBuilder.createMessage(
Message<byte[]> frame = MessageBuilder.createMessage("Message body".getBytes(), headers.getMessageHeaders()); "Message body".getBytes(), headers.getMessageHeaders());
assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0", assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0",
new String(encoder.encode(frame))); new String(encoder.encode(frame)));
@ -83,8 +81,8 @@ public class StompEncoderTests {
public void encodeFrameWithContentLengthPresent() { public void encodeFrameWithContentLengthPresent() {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setContentLength(12); headers.setContentLength(12);
Message<byte[]> frame = MessageBuilder.createMessage(
Message<byte[]> frame = MessageBuilder.createMessage("Message body".getBytes(), headers.getMessageHeaders()); "Message body".getBytes(), headers.getMessageHeaders());
assertEquals("SEND\ncontent-length:12\n\nMessage body\0", assertEquals("SEND\ncontent-length:12\n\nMessage body\0",
new String(encoder.encode(frame))); new String(encoder.encode(frame)));