All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cloudhopper.smpp.impl.DefaultSmppServer Maven / Gradle / Ivy

Go to download

Efficient, scalable, and flexible Java implementation of the Short Messaging Peer to Peer Protocol (SMPP)

There is a newer version: 5.1.0-113
Show newest version
package com.cloudhopper.smpp.impl;

/*
 * #%L
 * ch-smpp
 * %%
 * Copyright (C) 2009 - 2015 Cloudhopper by Twitter
 * %%
 * 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.
 * #L%
 */

import com.cloudhopper.smpp.*;
import com.cloudhopper.smpp.channel.*;
import com.cloudhopper.smpp.jmx.DefaultSmppServerMXBean;
import com.cloudhopper.smpp.pdu.BaseBind;
import com.cloudhopper.smpp.pdu.BaseBindResp;
import com.cloudhopper.smpp.tlv.Tlv;
import com.cloudhopper.smpp.transcoder.DefaultPduTranscoder;
import com.cloudhopper.smpp.transcoder.DefaultPduTranscoderContext;
import com.cloudhopper.smpp.transcoder.PduTranscoder;
import com.cloudhopper.smpp.type.SmppChannelException;
import com.cloudhopper.smpp.type.SmppProcessingException;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.oio.OioServerSocketChannel;
import io.netty.handler.timeout.WriteTimeoutHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.util.Timer;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Default implementation of an SmppServer that supports SMPP version 3.3 and 3.4.
 * 
 * @author joelauer (twitter: @jjlauer or http://twitter.com/jjlauer)
 */
public class DefaultSmppServer implements SmppServer, DefaultSmppServerMXBean {
    private static final Logger logger = LoggerFactory.getLogger(DefaultSmppServer.class);

    private final ChannelGroup channels;
    private final SmppServerConnector serverConnector;
    private final SmppServerConfiguration configuration;
    private final SmppServerHandler serverHandler;
    private final PduTranscoder transcoder;
    private final EventLoopGroup bossGroup;
    private final EventLoopGroup workerGroup;
    private ServerBootstrap serverBootstrap;
    private Channel serverChannel; 
    // shared instance of a timer background thread to close unbound channels
    private final Timer bindTimer;
   // shared instance of a session id generator (an atomic long)
    private final AtomicLong sessionIdSequence;
    // shared instance for monitor executors
    private final ScheduledExecutorService monitorExecutor;
    private DefaultSmppServerCounters counters;
    
    /**
     * Creates a new default SmppServer. Window monitoring and automatic
     * expiration of requests will be disabled with no monitorExecutors.
     * A "cachedDaemonThreadPool" will be used for IO worker threads.
     * @param configuration The server configuration to create this server with
     * @param serverHandler The handler implementation for handling bind requests
     *      and creating/destroying sessions.
     */
    public DefaultSmppServer(SmppServerConfiguration configuration, SmppServerHandler serverHandler) {
        this(configuration, serverHandler, null,
                configuration.isNonBlockingSocketsEnabled() ? new NioEventLoopGroup() : new OioEventLoopGroup(),
                configuration.isNonBlockingSocketsEnabled() ? new NioEventLoopGroup() : new OioEventLoopGroup());
    }

    /**
     * Creates a new default SmppServer.
     * @param configuration The server configuration to create this server with
     * @param serverHandler The handler implementation for handling bind requests
     *      and creating/destroying sessions.
     * @param monitorExecutor The scheduled executor that all sessions will share
     *      to monitor themselves and expire requests. If null monitoring will
     *      be disabled.
     * @param bossGroup Specify the EventLoopGroup to accept new connections and
     *      handle accepted connections. The {@link EventLoopGroup} is used to handle
     *      all the events and IO for {@link SocketChannel}.
     * @param workerGroup The {@link EventLoopGroup} is used to handle all the events
     *      and IO for {@link Channel}.
     */
    public DefaultSmppServer(final SmppServerConfiguration configuration, SmppServerHandler serverHandler,
                             ScheduledExecutorService monitorExecutor, EventLoopGroup bossGroup,
                             EventLoopGroup workerGroup) {
        this.configuration = configuration;
        // the same group we'll put every server channel

        //NEW
        //TODO: How do we control the thread pools and executors?
        //      How do we set the max # of threads in the worker pool?
        this.channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
        this.serverHandler = serverHandler;

        // tie the server bootstrap to this server socket channel factory
        this.serverBootstrap = new ServerBootstrap();

        // a factory for creating channels (connections)
        if (configuration.isNonBlockingSocketsEnabled()) {
            this.serverBootstrap.channel(NioServerSocketChannel.class);
        } else {
            this.serverBootstrap.channel(OioServerSocketChannel.class);
        }

        this.bossGroup = bossGroup;
        this.workerGroup = workerGroup;
        this.serverBootstrap.group(this.bossGroup, this.workerGroup);

        // set options for the server socket that are useful
        this.serverBootstrap.option(ChannelOption.SO_REUSEADDR, configuration.isReuseAddress());
        
        // we use the same default pipeline for all new channels - no need for a factory
        this.serverConnector = new SmppServerConnector(channels, this);

        this.serverBootstrap.childHandler(new ChannelInitializer() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(SmppChannelConstants.PIPELINE_SERVER_CONNECTOR_NAME, serverConnector);
            }
        });

        // a shared timer used to make sure new channels are bound within X milliseconds
        this.bindTimer = new Timer(configuration.getName() + "-BindTimer0", true);
        // NOTE: this would permit us to customize the "transcoding" context for a server if needed
        this.transcoder = new DefaultPduTranscoder(new DefaultPduTranscoderContext());
        this.sessionIdSequence = new AtomicLong(0);
        this.monitorExecutor = monitorExecutor;
        this.counters = new DefaultSmppServerCounters();
        if (configuration.isJmxEnabled()) {
            registerMBean();
        }
    }

    private void registerMBean() {
        if (configuration == null) {
            return;
        }
        if (configuration.isJmxEnabled()) {
            // register the this queue manager as an mbean
            try {
                ObjectName name = new ObjectName(configuration.getJmxDomain() + ":name=" + ObjectName.quote(configuration.getName()));
                ManagementFactory.getPlatformMBeanServer().registerMBean(this, name);
            } catch (Exception e) {
                // log the error, but don't throw an exception for this datasource
                logger.error("Unable to register DefaultSmppServerMXBean [{}]", configuration.getName(), e);
            }
        }
    }

    private void unregisterMBean() {
        if (configuration == null) {
            return;
        }
        if (configuration.isJmxEnabled()) {
            // register the this queue manager as an mbean
            try {
                ObjectName name = new ObjectName(configuration.getJmxDomain() + ":name=" + ObjectName.quote(configuration.getName()));
                ManagementFactory.getPlatformMBeanServer().unregisterMBean(name);
            } catch (Exception e) {
                // log the error, but don't throw an exception for this datasource
                logger.error("Unable to unregister DefaultSmppServerMXBean [{}]", configuration.getName(), e);
            }
        }
    }

    public PduTranscoder getTranscoder() {
        return this.transcoder;
    }

    @Override
    public ChannelGroup getChannels() {
        return this.channels;
    }

    public SmppServerConfiguration getConfiguration() {
        return this.configuration;
    }

    @Override
    public DefaultSmppServerCounters getCounters() {
        return this.counters;
    }

    public Timer getBindTimer() {
        return this.bindTimer;
    }

    @Override
    public boolean isStarted() {
        //TODO is isRegistered the same as isBound
        // return (this.serverChannel != null && this.serverChannel.isBound());
        return (this.serverChannel != null && this.serverChannel.isRegistered());
    }

    @Override
    public boolean isStopped() {
        return (this.serverChannel == null);
    }

    @Override
    public boolean isDestroyed() {
        return (this.serverBootstrap == null);
    }

    @Override
    public void start() throws SmppChannelException {
        if (isDestroyed()) {
            throw new SmppChannelException("Unable to start: server is destroyed");
        }
        try {
            ChannelFuture f = this.serverBootstrap.bind(new InetSocketAddress(configuration.getHost(), configuration.getPort()));
            logger.info("{} started at {}:{}", configuration.getName(), configuration.getHost(), configuration.getPort());
            // wait until the connection is made successfully
            boolean timeout = !f.await(configuration.getBindTimeout());
	    // From @trustin: You don't really set a timeout for bind operation.  I would do this instead:
	    //                    this.serverBootstrap.bind(...).sync();
	    //                ChannelFuture.sync() is a very useful operation.  It waits until the future is
	    //                fulfilled and then rethrow the exception if the operation has failed.
	    // Not sure if we can do this, as we actually need to retry on timeout.																       
	    
            if (timeout)
                throw new SmppChannelException("Can't bind to port " + configuration.getPort()
                        + " after " + configuration.getBindTimeout() + " milliseconds");

            if (!f.isSuccess())
                throw new SmppChannelException("Can't bind to port " + configuration.getPort()
                        + " future cause: " + f.cause());

            logger.info("{} started on SMPP port [{}]", configuration.getName(), configuration.getPort());
            serverChannel = f.channel();
        } catch (ChannelException e) {
            throw new SmppChannelException(e.getMessage(), e);
        } catch (InterruptedException e) {
            throw new SmppChannelException(e.getMessage(), e);
        }
    }

    @Override
    public void stop() {
        if (this.channels.size() > 0) {
            logger.info("{} currently has [{}] open child channel(s) that will be closed as part of stop()", configuration.getName(), this.channels.size());
        }
        // close all channels still open within this session "bootstrap"
        this.channels.close().awaitUninterruptibly();
        // clean up all external resources
        if (this.serverChannel != null) {
            try {
                /// this.serverChannel.close().awaitUninterruptibly();
                this.serverChannel.close().sync();
                this.serverChannel = null;
            } catch (InterruptedException e) {
                logger.warn("Thread interrupted closing server channel.", e);
            }
        }
        logger.info("{} stopped at {}:{}", configuration.getName(), configuration.getHost(), configuration.getPort());
    }

    @Override
    public void destroy() {
        this.bindTimer.cancel();
        stop();

        // Shut down all event loops to terminate all threads.
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();

        try {
            // Wait until all threads are terminated.
            bossGroup.terminationFuture().sync();
            workerGroup.terminationFuture().sync();
        } catch (InterruptedException e) {
            logger.warn("Thread interrupted closing executors.", e);
        }

        this.serverBootstrap = null;

        unregisterMBean();
        logger.info("{} destroyed on SMPP port [{}]", configuration.getName(), configuration.getPort());
    }

    protected Long nextSessionId() {
        return this.sessionIdSequence.getAndIncrement();
    }

    protected byte autoNegotiateInterfaceVersion(byte requestedInterfaceVersion) {
        if (!this.configuration.isAutoNegotiateInterfaceVersion()) {
            return requestedInterfaceVersion;
        } else {
            if (requestedInterfaceVersion >= SmppConstants.VERSION_3_4) {
                return SmppConstants.VERSION_3_4;
            } else {
                // downgrade to 3.3
                return SmppConstants.VERSION_3_3;
            }
        }
    }

    protected BaseBindResp createBindResponse(BaseBind bindRequest, int statusCode) {
        BaseBindResp bindResponse = (BaseBindResp)bindRequest.createResponse();
        bindResponse.setCommandStatus(statusCode);
        bindResponse.setSystemId(configuration.getSystemId());

        // if the server supports an SMPP server version >= 3.4 AND the bind request
        // included an interface version >= 3.4, include an optional parameter with configured sc_interface_version TLV
        if (configuration.getInterfaceVersion() >= SmppConstants.VERSION_3_4 && bindRequest.getInterfaceVersion() >= SmppConstants.VERSION_3_4) {
            Tlv scInterfaceVersion = new Tlv(SmppConstants.TAG_SC_INTERFACE_VERSION, new byte[] { configuration.getInterfaceVersion() });
            bindResponse.addOptionalParameter(scInterfaceVersion);
        }

        return bindResponse;
    }

    protected void bindRequested(Long sessionId, SmppSessionConfiguration config, BaseBind bindRequest) throws SmppProcessingException {
        counters.incrementBindRequestedAndGet();
        // delegate request upstream to server handler
        this.serverHandler.sessionBindRequested(sessionId, config, bindRequest);
    }


    protected void createSession(Long sessionId, Channel channel, SmppSessionConfiguration config, BaseBindResp preparedBindResponse) throws SmppProcessingException {
        // NOTE: exactly one PDU (bind request) was read from the channel, we
        // now need to delegate permitting this bind request by calling a method
        // further upstream.  Only after the server-side is completely ready to
        // start processing requests from this session, do we want to actually
        // return the bind response and start reading further requests -- we'll
        // initially block reading from the channel first -- this will be turned
        // back on via the "serverReady()" method call on the session object

        // make sure the channel is not being read/processed (until we flag we're ready later on)

        //TODO
	// @trustin: Channel.config().setAutoRead(false) will suspend read.  To resume, call setAutoRead(true).
	//           However, I would recommend setting autoRead to false in ChannelInitializer.initChannel()
	//           so that absolutely nothing is read since the connection is established.
        channel.config().setAutoRead(false);

        // auto negotiate the interface version in use based on the requested interface version
        byte interfaceVersion = this.autoNegotiateInterfaceVersion(config.getInterfaceVersion());

        // create a new server session associated with this server
        DefaultSmppSession session = new DefaultSmppSession(SmppSession.Type.SERVER, config, channel, this, sessionId, preparedBindResponse, interfaceVersion, monitorExecutor);

        // replace name of thread used for renaming
        SmppSessionThreadRenamer threadRenamer = (SmppSessionThreadRenamer)channel.pipeline().get(SmppChannelConstants.PIPELINE_SESSION_THREAD_RENAMER_NAME);
        threadRenamer.setThreadName(config.getName());

        // add a logging handler after the thread renamer
        SmppSessionLogger loggingHandler = new SmppSessionLogger(DefaultSmppSession.class.getCanonicalName(), config.getLoggingOptions());
        channel.pipeline().addAfter(SmppChannelConstants.PIPELINE_SESSION_THREAD_RENAMER_NAME, SmppChannelConstants.PIPELINE_SESSION_LOGGER_NAME, loggingHandler);

        // add a writeTimeout handler after the logger
        if (config.getWriteTimeout() > 0) {
            WriteTimeoutHandler writeTimeoutHandler = new WriteTimeoutHandler(config.getWriteTimeout(), TimeUnit.MILLISECONDS);
            channel.pipeline().addAfter(SmppChannelConstants.PIPELINE_SESSION_LOGGER_NAME, SmppChannelConstants.PIPELINE_SESSION_WRITE_TIMEOUT_NAME, writeTimeoutHandler);
        }

        // decoder in pipeline is ok (keep it)

        // create a new wrapper around a session to pass the pdu up the chain
        channel.pipeline().remove(SmppChannelConstants.PIPELINE_SESSION_WRAPPER_NAME);
        channel.pipeline().addLast(SmppChannelConstants.PIPELINE_SESSION_WRAPPER_NAME, new SmppSessionWrapper(session));
        
        // check if the # of channels exceeds maxConnections
        if (this.channels.size() > this.configuration.getMaxConnectionSize()) {
            logger.warn("The current connection size [{}] exceeds the configured max connection size [{}]", this.channels.size(), this.configuration.getMaxConnectionSize());
        }
        
        // session created, now pass it upstream
        counters.incrementSessionCreatedAndGet();
        incrementSessionSizeCounters(session);
        this.serverHandler.sessionCreated(sessionId, session, preparedBindResponse);
        
        // register this session as an mbean
        if (configuration.isJmxEnabled()) {
            session.registerMBean(configuration.getJmxDomain() + ":type=" + configuration.getName() + "Sessions,name=" + sessionId);
        }
    }


    protected void destroySession(Long sessionId, DefaultSmppSession session) {
        // session destroyed, now pass it upstream
        counters.incrementSessionDestroyedAndGet();
        decrementSessionSizeCounters(session);
        serverHandler.sessionDestroyed(sessionId, session);
        
        // unregister this session as an mbean
        if (configuration.isJmxEnabled()) {
            session.unregisterMBean(configuration.getJmxDomain() + ":type=" + configuration.getName() + "Sessions,name=" + sessionId);
        }
    }
    
    private void incrementSessionSizeCounters(DefaultSmppSession session) {
        this.counters.incrementSessionSizeAndGet();
        switch (session.getBindType()) {
            case TRANSCEIVER:
                this.counters.incrementTransceiverSessionSizeAndGet();
                break;
            case RECEIVER:
                this.counters.incrementTransmitterSessionSizeAndGet();
                break;
            case TRANSMITTER:
                this.counters.incrementReceiverSessionSizeAndGet();
                break;
        }
    }
    
    private void decrementSessionSizeCounters(DefaultSmppSession session) {
        this.counters.decrementSessionSizeAndGet();
        switch (session.getBindType()) {
            case TRANSCEIVER:
                this.counters.decrementTransceiverSessionSizeAndGet();
                break;
            case RECEIVER:
                this.counters.decrementTransmitterSessionSizeAndGet();
                break;
            case TRANSMITTER:
                this.counters.decrementReceiverSessionSizeAndGet();
                break;
        }
    }

    // mainly for exposing via JMX
    
    @Override
    public void resetCounters() {
        this.counters.reset();
    }
    
    @Override
    public int getSessionSize() {
        return this.counters.getSessionSize();
    }
    
    @Override
    public int getTransceiverSessionSize() {
        return this.counters.getTransceiverSessionSize();
    }
    
    @Override
    public int getTransmitterSessionSize() {
        return this.counters.getTransmitterSessionSize();
    }
    
    @Override
    public int getReceiverSessionSize() {
        return this.counters.getReceiverSessionSize();
    }
    
    @Override
    public int getMaxConnectionSize() {
        return this.configuration.getMaxConnectionSize();
    }

    @Override
    public int getConnectionSize() {
        return this.channels.size();
    }

    @Override
    public long getBindTimeout() {
        return this.configuration.getBindTimeout();
    }

    @Override
    public boolean isNonBlockingSocketsEnabled() {
        return this.configuration.isNonBlockingSocketsEnabled();
    }
    
    @Override
    public boolean isReuseAddress() {
        return this.configuration.isReuseAddress();
    }

    @Override
    public int getChannelConnects() {
        return this.getCounters().getChannelConnects();
    }

    @Override
    public int getChannelDisconnects() {
        return this.getCounters().getChannelDisconnects();
    }

    @Override
    public int getBindTimeouts() {
        return this.getCounters().getBindTimeouts();
    }

    @Override
    public int getBindRequested() {
        return this.getCounters().getBindRequested();
    }

    @Override
    public int getSessionCreated() {
        return this.getCounters().getSessionCreated();
    }

    @Override
    public int getSessionDestroyed() {
        return this.getCounters().getSessionDestroyed();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy