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

net.solarnetwork.io.modbus.tcp.netty.NettyTcpModbusServer Maven / Gradle / Ivy

There is a newer version: 0.17.0
Show newest version
/* ==================================================================
 * NettyTcpModbusServer.java - 30/11/2022 1:44:33 pm
 *
 * Copyright 2022 SolarNetwork.net Dev Team
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307 USA
 * ==================================================================
 */

package net.solarnetwork.io.modbus.tcp.netty;

import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import net.solarnetwork.io.modbus.ModbusMessage;
import net.solarnetwork.io.modbus.netty.msg.SimpleModbusMessageReply;
import net.solarnetwork.io.modbus.tcp.SimpleTransactionIdSupplier;

/**
 * A basic asynchronous Modbus TCP server.
 * 
 * 

* This server listens for Modbus requests, decodes them into * {@link ModbusMessage} instances, and then passes those to the handler * configured via {@link #setMessageHandler(BiConsumer)}. The handler must * provide a response {@link ModbusMessage}, which this server will then encode * and send back to the connected client. *

* * @author matt * @version 1.0 */ public class NettyTcpModbusServer { /** The {@code pendingMessageTtl} property default value. */ public static final long DEFAULT_PENDING_MESSAGE_TTL = TimeUnit.MINUTES.toMillis(2); private static final Logger log = LoggerFactory.getLogger(NettyTcpModbusServer.class); /** A mapping of transaction messages to pair requests/responses. */ private final ConcurrentMap pendingMessages; /** A transaction ID supplier. */ private final IntSupplier transactionIdSupplier; private final int port; private ScheduledFuture cleanupTask; private BiConsumer> messageHandler; private long pendingMessageTtl = DEFAULT_PENDING_MESSAGE_TTL; private boolean wireLogging; private EventLoopGroup bossGroup; private EventLoopGroup workerGroup; private Channel channel; /** * Constructor. * * @param port * the port to listen on * @throws IllegalArgumentException * if any argument is {@literal null} */ public NettyTcpModbusServer(int port) { this(port, new ConcurrentHashMap<>(8, 0.9f, 2), SimpleTransactionIdSupplier.INSTANCE); } /** * Constructor. * * @param port * the port to listen on * @param pendingMessages * a map to use for saving request messages, using transaction IDs * for keys * @param transactionIdSupplier * the transaction ID supplier * @throws IllegalArgumentException * if any argument is {@literal null} */ public NettyTcpModbusServer(int port, ConcurrentMap pendingMessages, IntSupplier transactionIdSupplier) { super(); this.port = port; if ( pendingMessages == null ) { throw new IllegalArgumentException("The pendingMessages argument must not be null."); } this.pendingMessages = pendingMessages; if ( transactionIdSupplier == null ) { throw new IllegalArgumentException("The transactionIdSupplier argument must not be null."); } this.transactionIdSupplier = transactionIdSupplier; } /** * Start the server. * *

* Upon return the server will be bound and ready to accept connections on * the configured port. *

*/ public synchronized void start() throws IOException { if ( this.channel != null ) { return; } try { NioEventLoopGroup bGroup = new NioEventLoopGroup(); this.bossGroup = bGroup; NioEventLoopGroup wGroup = new NioEventLoopGroup(); this.workerGroup = wGroup; // @formatter:off ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bGroup, wGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChildHandlerInitializer()) .option(ChannelOption.SO_REUSEADDR, true) .childOption(ChannelOption.SO_KEEPALIVE, true); // @formatter:on Channel channel = bootstrap.bind(port).sync().channel(); channel.closeFuture().addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { wGroup.shutdownGracefully(); bGroup.shutdownGracefully(); } }); this.channel = channel; if ( cleanupTask == null ) { long period = getPendingMessageTtl() * 2; if ( period > 0 ) { cleanupTask = bGroup.scheduleWithFixedDelay(new PendingMessageExpiredCleaner(), period, period, TimeUnit.MILLISECONDS); } } } catch ( Exception e ) { String msg = String.format("Error starting Modbus server on port %d", port); if ( e instanceof IOException ) { log.warn("{}: {}", msg, e.getMessage()); throw (IOException) e; } else { log.error(msg, e); } throw new RuntimeException(msg, e); } } /** * Stop the server. */ public synchronized void stop() { if ( workerGroup != null ) { workerGroup.shutdownGracefully(); workerGroup = null; } if ( bossGroup != null ) { bossGroup.shutdownGracefully(); bossGroup = null; } if ( cleanupTask != null ) { cleanupTask.cancel(true); cleanupTask = null; } if ( channel != null ) { channel.close().awaitUninterruptibly(); channel = null; } } /** * Initializer for client connections. */ private final class ChildHandlerInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if ( wireLogging ) { pipeline.addLast(new LoggingHandler("net.solarnetwork.io.modbus.server." + port)); } pipeline.addLast(new TcpModbusMessageEncoder(pendingMessages, transactionIdSupplier), new TcpModbusMessageDecoder(false, pendingMessages), new ChildHandler()); } } /** * Handler for client connections. */ private final class ChildHandler extends SimpleChannelInboundHandler { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info("Client connected: {}", ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.info("Client disconnected: {}", ctx.channel()); } @Override protected void channelRead0(ChannelHandlerContext ctx, ModbusMessage msg) throws Exception { log.debug("Request: {}", msg); final BiConsumer> h = getMessageHandler(); if ( h == null ) { return; } h.accept(msg, (r) -> { ctx.channel().writeAndFlush(new SimpleModbusMessageReply(msg, r)); }); } } private final class PendingMessageExpiredCleaner implements Runnable { @Override public void run() { log.debug("Looking for expired pending Modbus messages"); int expiredCount = 0; final long now = System.currentTimeMillis(); try { for ( Iterator itr = pendingMessages.values().iterator(); itr .hasNext(); ) { TcpModbusMessage pending = itr.next(); if ( pending.getTimestamp() + pendingMessageTtl < now ) { log.warn( "Dropping pending Modbus request message that has not had a response provided within {}ms: {}", pendingMessageTtl, pending); itr.remove(); expiredCount++; } } } catch ( Exception e ) { log.warn("Exception cleaning expired pending Modbus requests: {}", e.toString(), e); } finally { if ( expiredCount < 1 ) { log.debug("Finished cleaning expired pending Modbus requests; none expired."); } else { log.info("Finished cleaning expired pending Modbus requests; {} expired.", expiredCount); } } } } /** * Get the port the server is listening to. * * @return the port */ public int getPort() { return port; } /** * Get the message handler. * * @return the handler */ public BiConsumer> getMessageHandler() { return messageHandler; } /** * Set the message handler. * *

* This handler will be passed an inbound message along with another * {@code Consumer} for the reply message. *

* * @param messageHandler * the handler to set */ public void setMessageHandler(BiConsumer> messageHandler) { this.messageHandler = messageHandler; } /** * Get the "wire logging" setting. * * @return {@literal true} to enable wire-level logging of all messages */ public boolean isWireLogging() { return wireLogging; } /** * Set the "wire logging" setting. * * @param wireLogging * {@literal true} to enable wire-level logging of all messages */ public void setWireLogging(boolean wireLogging) { this.wireLogging = wireLogging; } /** * Get the pending Modbus message time-to-live expiration time. * * @return the pendingMessageTtl the pending Modbus message time-to-live, in * milliseconds; defaults to {@link #DEFAULT_PENDING_MESSAGE_TTL} */ public long getPendingMessageTtl() { return pendingMessageTtl; } /** * Set the pending Modbus message time-to-live expiration time. * *

* This timeout represents the minimum amount of time the client will wait * for a Modbus message response, before it qualifies for removal from the * pending message queue. *

* * @param pendingMessageTtl * the pending Modbus message time-to-live, in milliseconds */ public void setPendingMessageTtl(long pendingMessageTtl) { this.pendingMessageTtl = pendingMessageTtl; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy