org.eclipse.californium.elements.tcp.TcpServerConnector.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.californium.elements.tcp.TcpServerConnector.java

Source

/*******************************************************************************
 * Copyright (c) 2016, 2017 Amazon Web Services and others.
 * <p>
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 * <p>
 * The Eclipse Public License is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.html.
 * <p>
 * Contributors:
 * Joe Magerramov (Amazon Web Services) - CoAP over TCP support.
 * Achim Kraus (Bosch Software Innovations GmbH) - adjust port when bound.
 * Achim Kraus (Bosch Software Innovations GmbH) - use CloseOnErrorHandler.
 * Achim Kraus (Bosch Software Innovations GmbH) - add correlation context.
 * Achim Kraus (Bosch Software Innovations GmbH) - dummy CorrelationContextMatcher
 *                                                 (implemented afterwards)
 * Achim Kraus (Bosch Software Innovations GmbH) - add TcpCorrelationContextMatcher
 *                                                 implementation
 * Achim Kraus (Bosch Software Innovations GmbH) - add onSent() and onError(). 
 *                                                 issue #305
 * Achim Kraus (Bosch Software Innovations GmbH) - introduce protocol,
 *                                                 remove scheme
 * Bosch Software Innovations GmbH - migrate to SLF4J
 * Achim Kraus (Bosch Software Innovations GmbH) - move SO_KEEPALIVE to child options.
 ******************************************************************************/
package org.eclipse.californium.elements.tcp;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.GenericFutureListener;

import org.eclipse.californium.elements.Connector;
import org.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.elements.EndpointContextMatcher;
import org.eclipse.californium.elements.exception.EndpointMismatchException;
import org.eclipse.californium.elements.exception.EndpointUnconnectedException;
import org.eclipse.californium.elements.exception.MulticastNotSupportedException;
import org.eclipse.californium.elements.RawData;
import org.eclipse.californium.elements.RawDataChannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TCP server connection is used by CoapEndpoint when instantiated by the CoapServer. Per RFC the server can both
 * send and receive messages, but cannot initiated new outgoing connections.
 */
public class TcpServerConnector implements Connector {

    protected final Logger LOGGER = LoggerFactory.getLogger(getClass().getName());

    private final int numberOfThreads;
    private final int connectionIdleTimeoutSeconds;
    private final InetSocketAddress localAddress;
    private final TcpContextUtil contextUtil;
    private final ConcurrentMap<SocketAddress, Channel> activeChannels = new ConcurrentHashMap<>();

    /**
     * Endpoint context matcher for outgoing messages.
     * 
     * @see #setEndpointContextMatcher(EndpointContextMatcher)
     * @see #getEndpointContextMatcher()
     */
    private volatile EndpointContextMatcher endpointContextMatcher;
    private volatile InetSocketAddress effectiveLocalAddress;

    private RawDataChannel rawDataChannel;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    public TcpServerConnector(InetSocketAddress localAddress, int numberOfThreads, int idleTimeout) {
        this(localAddress, numberOfThreads, idleTimeout, new TcpContextUtil());
    }

    protected TcpServerConnector(InetSocketAddress localAddress, int numberOfThreads, int idleTimeout,
            TcpContextUtil contextUtil) {
        this.numberOfThreads = numberOfThreads;
        this.connectionIdleTimeoutSeconds = idleTimeout;
        this.localAddress = localAddress;
        this.contextUtil = contextUtil;
        this.effectiveLocalAddress = localAddress;
    }

    @Override
    public synchronized void start() throws IOException {
        if (rawDataChannel == null) {
            throw new IllegalStateException("Cannot start without message handler.");
        }
        if (bossGroup != null) {
            throw new IllegalStateException("Connector already started");
        }
        if (workerGroup != null) {
            throw new IllegalStateException("Connector already started");
        }

        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup(numberOfThreads);

        ServerBootstrap bootstrap = new ServerBootstrap();
        // server socket 
        bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelRegistry()).option(ChannelOption.SO_BACKLOG, 100)
                .option(ChannelOption.AUTO_READ, true).childOption(ChannelOption.SO_KEEPALIVE, true);

        // Start the server.
        ChannelFuture channelFuture = bootstrap.bind(localAddress).syncUninterruptibly();

        if (channelFuture.isSuccess() && 0 == localAddress.getPort()) {
            // replace port with the assigned one
            InetSocketAddress listenAddress = (InetSocketAddress) channelFuture.channel().localAddress();
            effectiveLocalAddress = new InetSocketAddress(localAddress.getAddress(), listenAddress.getPort());
        }
    }

    @Override
    public synchronized void stop() {
        if (null != bossGroup) {
            bossGroup.shutdownGracefully(0, 500, TimeUnit.MILLISECONDS).syncUninterruptibly();
            bossGroup = null;
        }
        if (null != workerGroup) {
            workerGroup.shutdownGracefully(0, 500, TimeUnit.MILLISECONDS).syncUninterruptibly();
            workerGroup = null;
        }
        effectiveLocalAddress = localAddress;
    }

    @Override
    public void destroy() {
        stop();
    }

    @Override
    public void send(final RawData msg) {
        if (msg == null) {
            throw new NullPointerException("Message must not be null");
        }
        if (msg.isMulticast()) {
            LOGGER.warn("TcpConnector drops {} bytes to multicast {}:{}", msg.getSize(), msg.getAddress(),
                    msg.getPort());
            msg.onError(new MulticastNotSupportedException("TCP doesn't support multicast!"));
            return;
        }
        if (bossGroup == null) {
            msg.onError(new IllegalStateException("TCP server connector not running!"));
            return;
        }
        Channel channel = activeChannels.get(msg.getInetSocketAddress());
        if (channel == null) {
            // TODO: Is it worth allowing opening a new connection when in server mode?
            LOGGER.debug("Attempting to send message to an address without an active connection {}",
                    msg.getAddress());
            msg.onError(new EndpointUnconnectedException());
            return;
        }
        EndpointContext context = contextUtil.buildEndpointContext(channel);
        final EndpointContextMatcher endpointMatcher = getEndpointContextMatcher();
        /* check, if the message should be sent with the established connection */
        if (null != endpointMatcher && !endpointMatcher.isToBeSent(msg.getEndpointContext(), context)) {
            LOGGER.warn("TcpConnector drops {} bytes to {}:{}", msg.getSize(), msg.getAddress(), msg.getPort());
            msg.onError(new EndpointMismatchException());
            return;
        }

        msg.onContextEstablished(context);
        ChannelFuture channelFuture = channel.writeAndFlush(Unpooled.wrappedBuffer(msg.getBytes()));
        channelFuture.addListener(new GenericFutureListener<ChannelFuture>() {

            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    msg.onSent();
                } else if (future.isCancelled()) {
                    msg.onError(new CancellationException());
                } else {
                    msg.onError(future.cause());
                }
            }
        });
    }

    @Override
    public void setRawDataReceiver(RawDataChannel messageHandler) {
        if (rawDataChannel != null) {
            throw new IllegalStateException("RawDataChannel already set");
        }

        this.rawDataChannel = messageHandler;
    }

    @Override
    public void setEndpointContextMatcher(EndpointContextMatcher matcher) {
        endpointContextMatcher = matcher;
    }

    private EndpointContextMatcher getEndpointContextMatcher() {
        return endpointContextMatcher;
    }

    @Override
    public InetSocketAddress getAddress() {
        return effectiveLocalAddress;
    }

    /**
     * Called when a new channel is created, Allows subclasses to add their own handlers first, like an SSL handler.
     */
    protected void onNewChannelCreated(Channel ch) {

    }

    @Override
    public String getProtocol() {
        return "TCP";
    }

    @Override
    public String toString() {
        return getProtocol() + "-" + getAddress();
    }

    private class ChannelRegistry extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            onNewChannelCreated(ch);

            // Handler order:
            // 0. Register/unregister new channel: all messages can only be sent over open connections.
            // 1. Generate Idle events
            // 2. Close idle channels.
            // 3. Stream-to-message decoder
            // 4. Hand-off decoded messages to CoAP stack
            // 5. Close connections on errors.
            ch.pipeline().addLast(new ChannelTracker());
            ch.pipeline().addLast(new IdleStateHandler(0, 0, connectionIdleTimeoutSeconds));
            ch.pipeline().addLast(new CloseOnIdleHandler());
            ch.pipeline().addLast(new DatagramFramer(contextUtil));
            ch.pipeline().addLast(new DispatchHandler(rawDataChannel));
            ch.pipeline().addLast(new CloseOnErrorHandler());
        }
    }

    /** Tracks active channels to send messages over them. TCPServer connector does not establish new connections. */
    private class ChannelTracker extends ChannelInboundHandlerAdapter {

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            activeChannels.put(ctx.channel().remoteAddress(), ctx.channel());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            activeChannels.remove(ctx.channel().remoteAddress());
        }
    }
}