/* * Copyright 2001-2017 the original author or authors. * * 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 org.springframework.integration.ip.udp; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.SocketException; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.ip.AbstractInternetProtocolSendingMessageHandler; import org.springframework.messaging.Message; import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.MessagingException; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * A {@link org.springframework.messaging.MessageHandler} implementation that maps a Message into * a UDP datagram packet and sends that to the specified host and port. * * Messages can be basic, with no support for reliability, can be prefixed * by a length so the receiving end can detect truncation, and can require * a UDP acknowledgment to confirm delivery. * * @author Gary Russell * @author Marcin Pilaczynski * @author Artem Bilan * @since 2.0 */ public class UnicastSendingMessageHandler extends AbstractInternetProtocolSendingMessageHandler implements Runnable { private final DatagramPacketMessageMapper mapper = new DatagramPacketMessageMapper(); private final Expression destinationExpression; private volatile DatagramSocket socket; /** * If true adds headers to instruct receiving adapter to return an ack. */ private volatile boolean waitForAck = false; private volatile boolean acknowledge = false; private volatile String ackHost; private volatile int ackPort; private volatile int ackTimeout = 5000; private volatile int ackCounter = 1; private volatile Map<String, CountDownLatch> ackControl = Collections .synchronizedMap(new HashMap<String, CountDownLatch>()); private volatile int soReceiveBufferSize = -1; private volatile String localAddress; private volatile CountDownLatch ackLatch; private volatile boolean ackThreadRunning; private volatile Executor taskExecutor; private volatile boolean taskExecutorSet; private Expression socketExpression; private EvaluationContext evaluationContext; /** * Basic constructor; no reliability; no acknowledgment. * @param host Destination host. * @param port Destination port. */ public UnicastSendingMessageHandler(String host, int port) { super(host, port); this.mapper.setLengthCheck(false); this.mapper.setAcknowledge(false); this.destinationExpression = null; } /** * Construct UnicastSendingMessageHandler based on the destination SpEL expression to * determine the target destination at runtime against requestMessage. * @param destinationExpression the SpEL expression to evaluate the target destination * at runtime. Must evaluate to {@link String}, {@link URI} or {@link SocketAddress}. * @since 4.3 */ public UnicastSendingMessageHandler(String destinationExpression) { super("", 0); Assert.hasText(destinationExpression, "'destinationExpression' cannot be null or empty"); this.mapper.setLengthCheck(false); this.mapper.setAcknowledge(false); this.destinationExpression = EXPRESSION_PARSER.parseExpression(destinationExpression); } /** * Construct UnicastSendingMessageHandler based on the destination SpEL expression to * determine the target destination at runtime against requestMessage. * @param destinationExpression the SpEL expression to evaluate the target destination * at runtime. Must evaluate to {@link String}, {@link URI} or {@link SocketAddress}. * @since 4.3 */ public UnicastSendingMessageHandler(Expression destinationExpression) { super("", 0); Assert.notNull(destinationExpression, "'destinationExpression' cannot be null"); this.mapper.setLengthCheck(false); this.mapper.setAcknowledge(false); this.destinationExpression = destinationExpression; } /** * Can used to add a length to each packet which can be checked at the destination. * @param host Destination Host. * @param port Destination Port. * @param lengthCheck If true, packets will contain a length. */ public UnicastSendingMessageHandler(String host, int port, boolean lengthCheck) { super(host, port); this.mapper.setLengthCheck(lengthCheck); this.mapper.setAcknowledge(false); this.destinationExpression = null; } /** * Add an acknowledgment request to packets. * @param host Destination Host. * @param port Destination Port. * @param acknowledge If true, packets will request acknowledgment. * @param ackHost The host to which acks should be sent. Required if ack true. * @param ackPort The port to which acks should be sent. * @param ackTimeout How long we will wait (milliseconds) for the ack. */ public UnicastSendingMessageHandler(String host, int port, boolean acknowledge, String ackHost, int ackPort, int ackTimeout) { super(host, port); this.destinationExpression = null; setReliabilityAttributes(false, acknowledge, ackHost, ackPort, ackTimeout); } /** * Add a length and/or acknowledgment request to packets. * @param host Destination Host. * @param port Destination Port. * @param lengthCheck If true, packets will contain a length. * @param acknowledge If true, packets will request acknowledgment. * @param ackHost The host to which acks should be sent. Required if ack true. * @param ackPort The port to which acks should be sent. * @param ackTimeout How long we will wait (milliseconds) for the ack. */ public UnicastSendingMessageHandler(String host, int port, boolean lengthCheck, boolean acknowledge, String ackHost, int ackPort, int ackTimeout) { super(host, port); this.destinationExpression = null; setReliabilityAttributes(lengthCheck, acknowledge, ackHost, ackPort, ackTimeout); } protected final void setReliabilityAttributes(boolean lengthCheck, boolean acknowledge, String ackHost, int ackPort, int ackTimeout) { this.mapper.setLengthCheck(lengthCheck); this.waitForAck = acknowledge; this.mapper.setAcknowledge(acknowledge); this.mapper.setAckAddress(ackHost + ":" + ackPort); this.ackHost = ackHost; this.ackPort = ackPort; if (ackTimeout > 0) { this.ackTimeout = ackTimeout; } this.acknowledge = acknowledge; if (this.acknowledge) { Assert.state(StringUtils.hasText(ackHost), "'ackHost' must not be empty"); } } /** * @param lengthCheck if true, a four byte binary length header is added to the * packet, allowing the receiver to check for data truncation. * @since 5.0 */ public void setLengthCheck(boolean lengthCheck) { this.mapper.setLengthCheck(lengthCheck); } @Override public void doStart() { if (this.acknowledge) { if (this.taskExecutor == null) { Executor executor = Executors .newSingleThreadExecutor(new ThreadFactory() { private final AtomicInteger n = new AtomicInteger(); @Override public Thread newThread(Runnable runner) { Thread thread = new Thread(runner); thread.setName("UDP-Ack-Handler-" + n.getAndIncrement()); thread.setDaemon(true); return thread; } }); this.taskExecutor = executor; } startAckThread(); } } @Override protected void doStop() { this.closeSocketIfNeeded(); if (!this.taskExecutorSet && this.taskExecutor != null) { ((ExecutorService) this.taskExecutor).shutdown(); this.taskExecutor = null; } } @Override public void handleMessageInternal(Message<?> message) throws MessageHandlingException, MessageDeliveryException { if (this.acknowledge) { Assert.state(this.isRunning(), "When 'acknowledge' is enabled, adapter must be running"); startAckThread(); } CountDownLatch countdownLatch = null; String messageId = message.getHeaders().getId().toString(); try { boolean waitForAck = this.waitForAck; if (waitForAck) { countdownLatch = new CountDownLatch(this.ackCounter); this.ackControl.put(messageId, countdownLatch); } convertAndSend(message); if (waitForAck) { try { if (!countdownLatch.await(this.ackTimeout, TimeUnit.MILLISECONDS)) { throw new MessagingException(message, "Failed to receive UDP Ack in " + this.ackTimeout + " millis"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } catch (MessagingException e) { throw e; } catch (Exception e) { try { this.socket.close(); } catch (Exception e1) { } this.socket = null; throw new MessageHandlingException(message, "failed to send UDP packet", e); } finally { if (countdownLatch != null) { this.ackControl.remove(messageId); } } } public void startAckThread() { if (!this.ackThreadRunning) { synchronized (this) { if (!this.ackThreadRunning) { try { getSocket(); } catch (IOException e) { logger.error("Error creating socket", e); } this.ackLatch = new CountDownLatch(1); this.taskExecutor.execute(this); try { this.ackLatch.await(10000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } } protected void convertAndSend(Message<?> message) throws Exception { DatagramSocket socket; if (this.socketExpression != null) { socket = this.socketExpression.getValue(this.evaluationContext, message, DatagramSocket.class); } else { socket = getSocket(); } SocketAddress destinationAddress; if (this.destinationExpression != null) { Object destination = this.destinationExpression.getValue(this.evaluationContext, message); if (destination instanceof String) { destination = new URI((String) destination); } if (destination instanceof URI) { URI uri = (URI) destination; destination = new InetSocketAddress(uri.getHost(), uri.getPort()); } if (destination instanceof SocketAddress) { destinationAddress = (SocketAddress) destination; } else { throw new IllegalStateException("'destinationExpression' must evaluate to String, URI " + "or SocketAddress. Gotten [" + destination + "]."); } } else { destinationAddress = getDestinationAddress(); } DatagramPacket packet = this.mapper.fromMessage(message); packet.setSocketAddress(destinationAddress); socket.send(packet); if (logger.isDebugEnabled()) { logger.debug("Sent packet for message " + message + " to " + packet.getSocketAddress()); } } protected void setSocket(DatagramSocket socket) { this.socket = socket; } protected DatagramSocket getTheSocket() { return this.socket; } protected synchronized DatagramSocket getSocket() throws IOException { if (this.socket == null) { if (this.acknowledge) { if (this.localAddress == null) { this.socket = this.ackPort == 0 ? new DatagramSocket() : new DatagramSocket(this.ackPort); } else { InetAddress whichNic = InetAddress.getByName(this.localAddress); this.socket = new DatagramSocket(new InetSocketAddress(whichNic, this.ackPort)); } if (this.soReceiveBufferSize > 0) { this.socket.setReceiveBufferSize(this.soReceiveBufferSize); } if (logger.isDebugEnabled()) { logger.debug("Listening for acks on port: " + getAckPort()); } updateAckAddress(); } else { this.socket = new DatagramSocket(); } setSocketAttributes(this.socket); } return this.socket; } protected void updateAckAddress() { this.mapper.setAckAddress(this.ackHost + ":" + getAckPort()); } /** * @see java.net.Socket#setReceiveBufferSize(int) * @see DatagramSocket#setReceiveBufferSize(int) */ @Override public void setSoReceiveBufferSize(int size) { this.soReceiveBufferSize = size; } @Override public void setLocalAddress(String localAddress) { this.localAddress = localAddress; } public void setTaskExecutor(Executor taskExecutor) { Assert.notNull(taskExecutor, "'taskExecutor' cannot be null"); this.taskExecutor = taskExecutor; this.taskExecutorSet = true; } /** * @param ackCounter the ackCounter to set */ public void setAckCounter(int ackCounter) { this.ackCounter = ackCounter; } /** * @param socketExpression the socket expression to determine the target socket at runtime. * @since 4.3 */ public void setSocketExpression(Expression socketExpression) { this.socketExpression = socketExpression; } /** * @param socketExpression the socket SpEL expression to determine the target socket at runtime. * @since 4.3 */ public void setSocketExpressionString(String socketExpression) { this.socketExpression = EXPRESSION_PARSER.parseExpression(socketExpression); } @Override public String getComponentType() { return "ip:udp-outbound-channel-adapter"; } /** * @return the acknowledge */ public boolean isAcknowledge() { return this.acknowledge; } /** * @return the ackPort */ public int getAckPort() { DatagramSocket socket = this.socket; if (this.ackPort == 0 && socket != null) { return socket.getLocalPort(); } else { return this.ackPort; } } /** * @return the soReceiveBufferSize */ public int getSoReceiveBufferSize() { return this.soReceiveBufferSize; } @Override protected void onInit() throws Exception { super.onInit(); this.mapper.setBeanFactory(getBeanFactory()); this.evaluationContext = IntegrationContextUtils.getEvaluationContext(getBeanFactory()); if (this.socketExpression != null) { Assert.state(!this.acknowledge, "'acknowledge' must be false when using a socket expression"); } } protected void setSocketAttributes(DatagramSocket socket) throws SocketException { if (this.getSoTimeout() >= 0) { socket.setSoTimeout(this.getSoTimeout()); } if (this.getSoSendBufferSize() > 0) { socket.setSendBufferSize(this.getSoSendBufferSize()); } } /** * Process acknowledgments, if requested. */ @Override public void run() { try { this.ackThreadRunning = true; this.ackLatch.countDown(); DatagramPacket ackPack = new DatagramPacket(new byte[100], 100); while (true) { this.getSocket().receive(ackPack); String id = new String(ackPack.getData(), ackPack.getOffset(), ackPack.getLength()); if (logger.isDebugEnabled()) { logger.debug("Received ack for " + id + " from " + ackPack.getAddress().getHostAddress()); } CountDownLatch latch = this.ackControl.get(id); if (latch != null) { latch.countDown(); } } } catch (IOException e) { if (this.socket != null && !this.socket.isClosed()) { logger.error("Error on UDP Acknowledge thread:" + e.getMessage()); } } finally { this.ackThreadRunning = false; } } /** * If exposed as an MBean, can be used to restart the ack thread if a fatal * (bind) error occurred, without bouncing the JVM. */ public void restartAckThread() { this.taskExecutor.execute(this); } private void closeSocketIfNeeded() { if (this.socket != null) { this.socket.close(); this.socket = null; } } }