/*
* Copyright (c) 2015-2016, Christoph Engelbert (aka noctarius) and
* contributors. All rights reserved.
*
* 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 com.noctarius.tengi.client.impl.transport.http;
import com.noctarius.tengi.client.TransportLayers;
import com.noctarius.tengi.client.impl.ConnectCallback;
import com.noctarius.tengi.client.impl.ServerConnection;
import com.noctarius.tengi.client.impl.transport.AbstractClientConnector;
import com.noctarius.tengi.core.connection.Connection;
import com.noctarius.tengi.core.connection.HandshakeHandler;
import com.noctarius.tengi.core.connection.TransportLayer;
import com.noctarius.tengi.core.exception.ConnectionDestroyedException;
import com.noctarius.tengi.core.model.Message;
import com.noctarius.tengi.spi.buffer.MemoryBuffer;
import com.noctarius.tengi.spi.buffer.impl.MemoryBufferFactory;
import com.noctarius.tengi.spi.connection.AbstractConnection;
import com.noctarius.tengi.spi.connection.impl.TransportConstants;
import com.noctarius.tengi.spi.connection.packets.Handshake;
import com.noctarius.tengi.spi.connection.packets.PollingRequest;
import com.noctarius.tengi.spi.serialization.Protocol;
import com.noctarius.tengi.spi.serialization.Serializer;
import com.noctarius.tengi.spi.serialization.codec.AutoClosableEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import java.net.InetAddress;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.noctarius.tengi.client.impl.ClientUtil.CONNECTION;
import static com.noctarius.tengi.client.impl.ClientUtil.CONNECT_FUTURE;
import static com.noctarius.tengi.client.impl.ClientUtil.connectionAttribute;
public class HttpConnector
extends AbstractClientConnector<HttpRequest> {
private final AtomicBoolean destroyed = new AtomicBoolean(false);
private final AtomicReference<Channel> downstream = new AtomicReference<>(null);
private final Bootstrap bootstrap;
private final InetAddress address;
private final int port;
private final Serializer serializer;
private final Protocol protocol;
private final EventLoopGroup clientGroup;
private final HandshakeHandler handshakeHandler;
private volatile ByteBufAllocator allocator;
public HttpConnector(InetAddress address, int port, Serializer serializer, HandshakeHandler handshakeHandler,
EventLoopGroup clientGroup) {
this.address = address;
this.port = port;
this.serializer = serializer;
this.protocol = serializer.getProtocol();
this.handshakeHandler = handshakeHandler;
this.clientGroup = clientGroup;
this.bootstrap = createBootstrap();
}
@Override
public void connect(ConnectCallback connectCallback) {
Bootstrap bootstrap = createBootstrap();
ConnectCallback wrapper = (connection, throwable) -> {
if (connection != null) {
activateLongPolling(connection);
connectCallback.on(connection);
} else {
connectCallback.on(throwable);
}
};
bootstrap.attr(CONNECT_FUTURE, wrapper);
ChannelFuture channelFuture = bootstrap.connect(address, port);
channelFuture.addListener(connectionListener(wrapper, this::handshakeRequest, this::handleChannelClose));
}
@Override
public HandshakeHandler handshakeHandler() {
return handshakeHandler;
}
@Override
public ByteBufAllocator allocator() {
return allocator;
}
@Override
public void write(HttpRequest message)
throws Exception {
if (destroyed.get()) {
throw new ConnectionDestroyedException("Connection already destroyed");
}
ChannelFuture channelFuture = bootstrap.connect(address, port);
Channel channel = channelFuture.sync().channel();
channel.writeAndFlush(message).sync();
channel.close().sync();
}
@Override
public void destroy(Connection connection)
throws Exception {
if (destroyed.compareAndSet(false, true)) {
Channel channel = downstream.get();
if (channel != null) {
channel.close().sync();
}
if (connection instanceof AbstractConnection) {
((AbstractConnection) connection).notifyClose();
}
}
}
@Override
public String getName() {
return TransportConstants.TRANSPORT_NAME_HTTP;
}
@Override
public boolean isStreaming() {
return false;
}
@Override
public int getDefaultPort() {
return TransportConstants.DEFAULT_PORT_TCP;
}
@Override
public TransportLayer getTransportLayer() {
return TransportLayers.TCP;
}
private HttpConnectionProcessor buildProcessor(Serializer serializer) {
return new HttpConnectionProcessor(serializer, HttpConnector.this);
}
private void handleChannelClose(ChannelFuture channelFuture)
throws Exception {
Channel channel = channelFuture.channel();
ConnectCallback connectCallback = connectionAttribute(channel, CONNECT_FUTURE);
if (connectCallback != null) {
clientGroup.shutdownGracefully();
connectCallback.on(null, null);
}
}
private Connection activateLongPolling(Connection connection) {
if (connection != null) {
ServerConnection serverConnection = (ServerConnection) connection;
Bootstrap bootstrap = this.bootstrap.attr(CONNECTION, serverConnection);
ChannelFuture channelFuture = bootstrap.connect(address, port);
channelFuture.addListener(fireLongPollingRequest(serverConnection));
}
return connection;
}
private ChannelFutureListener fireLongPollingRequest(ServerConnection connection) {
return (channelFuture) -> {
Channel channel = channelFuture.channel();
downstream.set(channel);
channel.closeFuture().addListener(reconnectLongPolling(connection));
PollingRequest pollingRequest = new PollingRequest();
ByteBuf buffer = channel.alloc().directBuffer();
MemoryBuffer memoryBuffer = MemoryBufferFactory.create(buffer);
try (AutoClosableEncoder encoder = serializer.retrieveEncoder(memoryBuffer)) {
encoder.writeBoolean("loggedIn", true);
encoder.writeObject("connectionId", connection.getConnectionId());
encoder.writeObject("pollingRequest", Message.create(pollingRequest));
}
channel.writeAndFlush(buildHttpRequest(buffer, protocol.getMimeType()));
};
}
private ChannelFutureListener reconnectLongPolling(ServerConnection connection) {
return (cf) -> {
if (destroyed.get()) {
return;
}
ChannelFuture channelFuture = bootstrap.connect(address, port);
channelFuture.addListener(fireLongPollingRequest(connection));
};
}
private void handshakeRequest(Channel channel)
throws Exception {
this.allocator = channel.alloc();
// Send Handshake
ByteBuf buffer = Unpooled.buffer();
MemoryBuffer memoryBuffer = MemoryBufferFactory.create(buffer);
try (AutoClosableEncoder encoder = serializer.retrieveEncoder(memoryBuffer)) {
encoder.writeBoolean("loggedIn", false);
encoder.writeObject("handshake", new Handshake());
}
channel.writeAndFlush(buildHttpRequest(buffer, protocol.getMimeType()));
}
private Bootstrap createBootstrap() {
return new Bootstrap().channel(NioSocketChannel.class) //
.group(clientGroup).option(ChannelOption.TCP_NODELAY, true) //
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) //
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("codec", new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(1048576));
pipeline.addLast(buildProcessor(serializer));
}
});
}
static HttpRequest buildHttpRequest(ByteBuf buffer, String mimeType) {
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/channel", buffer);
HttpHeaders headers = request.headers();
headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
headers.set(HttpHeaderNames.CONTENT_TYPE, mimeType);
headers.set(HttpHeaderNames.CONTENT_LENGTH, buffer.writerIndex());
headers.set(HttpHeaderNames.USER_AGENT, TransportConstants.TRANSPORT_NAME_HTTP);
return request;
}
}