001/*
002 * Copyright 2002-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.util;
018
019import java.net.DatagramSocket;
020import java.net.InetAddress;
021import java.net.ServerSocket;
022import java.util.Random;
023import java.util.SortedSet;
024import java.util.TreeSet;
025import javax.net.ServerSocketFactory;
026
027/**
028 * Simple utility methods for working with network sockets — for example,
029 * for finding available ports on {@code localhost}.
030 *
031 * <p>Within this class, a TCP port refers to a port for a {@link ServerSocket};
032 * whereas, a UDP port refers to a port for a {@link DatagramSocket}.
033 *
034 * @author Sam Brannen
035 * @author Ben Hale
036 * @author Arjen Poutsma
037 * @author Gunnar Hillert
038 * @author Gary Russell
039 * @since 4.0
040 */
041public class SocketUtils {
042
043        /**
044         * The default minimum value for port ranges used when finding an available
045         * socket port.
046         */
047        public static final int PORT_RANGE_MIN = 1024;
048
049        /**
050         * The default maximum value for port ranges used when finding an available
051         * socket port.
052         */
053        public static final int PORT_RANGE_MAX = 65535;
054
055
056        private static final Random random = new Random(System.currentTimeMillis());
057
058
059        /**
060         * Although {@code SocketUtils} consists solely of static utility methods,
061         * this constructor is intentionally {@code public}.
062         * <h4>Rationale</h4>
063         * <p>Static methods from this class may be invoked from within XML
064         * configuration files using the Spring Expression Language (SpEL) and the
065         * following syntax.
066         * <pre><code>&lt;bean id="bean1" ... p:port="#{T(org.springframework.util.SocketUtils).findAvailableTcpPort(12000)}" /&gt;</code></pre>
067         * If this constructor were {@code private}, you would be required to supply
068         * the fully qualified class name to SpEL's {@code T()} function for each usage.
069         * Thus, the fact that this constructor is {@code public} allows you to reduce
070         * boilerplate configuration with SpEL as can be seen in the following example.
071         * <pre><code>&lt;bean id="socketUtils" class="org.springframework.util.SocketUtils" /&gt;
072         * &lt;bean id="bean1" ... p:port="#{socketUtils.findAvailableTcpPort(12000)}" /&gt;
073         * &lt;bean id="bean2" ... p:port="#{socketUtils.findAvailableTcpPort(30000)}" /&gt;</code></pre>
074         */
075        public SocketUtils() {
076                /* no-op */
077        }
078
079
080        /**
081         * Find an available TCP port randomly selected from the range
082         * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}].
083         * @return an available TCP port number
084         * @throws IllegalStateException if no available port could be found
085         */
086        public static int findAvailableTcpPort() {
087                return findAvailableTcpPort(PORT_RANGE_MIN);
088        }
089
090        /**
091         * Find an available TCP port randomly selected from the range
092         * [{@code minPort}, {@value #PORT_RANGE_MAX}].
093         * @param minPort the minimum port number
094         * @return an available TCP port number
095         * @throws IllegalStateException if no available port could be found
096         */
097        public static int findAvailableTcpPort(int minPort) {
098                return findAvailableTcpPort(minPort, PORT_RANGE_MAX);
099        }
100
101        /**
102         * Find an available TCP port randomly selected from the range
103         * [{@code minPort}, {@code maxPort}].
104         * @param minPort the minimum port number
105         * @param maxPort the maximum port number
106         * @return an available TCP port number
107         * @throws IllegalStateException if no available port could be found
108         */
109        public static int findAvailableTcpPort(int minPort, int maxPort) {
110                return SocketType.TCP.findAvailablePort(minPort, maxPort);
111        }
112
113        /**
114         * Find the requested number of available TCP ports, each randomly selected
115         * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}].
116         * @param numRequested the number of available ports to find
117         * @return a sorted set of available TCP port numbers
118         * @throws IllegalStateException if the requested number of available ports could not be found
119         */
120        public static SortedSet<Integer> findAvailableTcpPorts(int numRequested) {
121                return findAvailableTcpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX);
122        }
123
124        /**
125         * Find the requested number of available TCP ports, each randomly selected
126         * from the range [{@code minPort}, {@code maxPort}].
127         * @param numRequested the number of available ports to find
128         * @param minPort the minimum port number
129         * @param maxPort the maximum port number
130         * @return a sorted set of available TCP port numbers
131         * @throws IllegalStateException if the requested number of available ports could not be found
132         */
133        public static SortedSet<Integer> findAvailableTcpPorts(int numRequested, int minPort, int maxPort) {
134                return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort);
135        }
136
137        /**
138         * Find an available UDP port randomly selected from the range
139         * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}].
140         * @return an available UDP port number
141         * @throws IllegalStateException if no available port could be found
142         */
143        public static int findAvailableUdpPort() {
144                return findAvailableUdpPort(PORT_RANGE_MIN);
145        }
146
147        /**
148         * Find an available UDP port randomly selected from the range
149         * [{@code minPort}, {@value #PORT_RANGE_MAX}].
150         * @param minPort the minimum port number
151         * @return an available UDP port number
152         * @throws IllegalStateException if no available port could be found
153         */
154        public static int findAvailableUdpPort(int minPort) {
155                return findAvailableUdpPort(minPort, PORT_RANGE_MAX);
156        }
157
158        /**
159         * Find an available UDP port randomly selected from the range
160         * [{@code minPort}, {@code maxPort}].
161         * @param minPort the minimum port number
162         * @param maxPort the maximum port number
163         * @return an available UDP port number
164         * @throws IllegalStateException if no available port could be found
165         */
166        public static int findAvailableUdpPort(int minPort, int maxPort) {
167                return SocketType.UDP.findAvailablePort(minPort, maxPort);
168        }
169
170        /**
171         * Find the requested number of available UDP ports, each randomly selected
172         * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}].
173         * @param numRequested the number of available ports to find
174         * @return a sorted set of available UDP port numbers
175         * @throws IllegalStateException if the requested number of available ports could not be found
176         */
177        public static SortedSet<Integer> findAvailableUdpPorts(int numRequested) {
178                return findAvailableUdpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX);
179        }
180
181        /**
182         * Find the requested number of available UDP ports, each randomly selected
183         * from the range [{@code minPort}, {@code maxPort}].
184         * @param numRequested the number of available ports to find
185         * @param minPort the minimum port number
186         * @param maxPort the maximum port number
187         * @return a sorted set of available UDP port numbers
188         * @throws IllegalStateException if the requested number of available ports could not be found
189         */
190        public static SortedSet<Integer> findAvailableUdpPorts(int numRequested, int minPort, int maxPort) {
191                return SocketType.UDP.findAvailablePorts(numRequested, minPort, maxPort);
192        }
193
194
195        private enum SocketType {
196
197                TCP {
198                        @Override
199                        protected boolean isPortAvailable(int port) {
200                                try {
201                                        ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(
202                                                        port, 1, InetAddress.getByName("localhost"));
203                                        serverSocket.close();
204                                        return true;
205                                }
206                                catch (Exception ex) {
207                                        return false;
208                                }
209                        }
210                },
211
212                UDP {
213                        @Override
214                        protected boolean isPortAvailable(int port) {
215                                try {
216                                        DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost"));
217                                        socket.close();
218                                        return true;
219                                }
220                                catch (Exception ex) {
221                                        return false;
222                                }
223                        }
224                };
225
226                /**
227                 * Determine if the specified port for this {@code SocketType} is
228                 * currently available on {@code localhost}.
229                 */
230                protected abstract boolean isPortAvailable(int port);
231
232                /**
233                 * Find a pseudo-random port number within the range
234                 * [{@code minPort}, {@code maxPort}].
235                 * @param minPort the minimum port number
236                 * @param maxPort the maximum port number
237                 * @return a random port number within the specified range
238                 */
239                private int findRandomPort(int minPort, int maxPort) {
240                        int portRange = maxPort - minPort;
241                        return minPort + random.nextInt(portRange + 1);
242                }
243
244                /**
245                 * Find an available port for this {@code SocketType}, randomly selected
246                 * from the range [{@code minPort}, {@code maxPort}].
247                 * @param minPort the minimum port number
248                 * @param maxPort the maximum port number
249                 * @return an available port number for this socket type
250                 * @throws IllegalStateException if no available port could be found
251                 */
252                int findAvailablePort(int minPort, int maxPort) {
253                        Assert.isTrue(minPort > 0, "'minPort' must be greater than 0");
254                        Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equals 'minPort'");
255                        Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX);
256
257                        int portRange = maxPort - minPort;
258                        int candidatePort;
259                        int searchCounter = 0;
260                        do {
261                                if (++searchCounter > portRange) {
262                                        throw new IllegalStateException(String.format(
263                                                        "Could not find an available %s port in the range [%d, %d] after %d attempts",
264                                                        name(), minPort, maxPort, searchCounter));
265                                }
266                                candidatePort = findRandomPort(minPort, maxPort);
267                        }
268                        while (!isPortAvailable(candidatePort));
269
270                        return candidatePort;
271                }
272
273                /**
274                 * Find the requested number of available ports for this {@code SocketType},
275                 * each randomly selected from the range [{@code minPort}, {@code maxPort}].
276                 * @param numRequested the number of available ports to find
277                 * @param minPort the minimum port number
278                 * @param maxPort the maximum port number
279                 * @return a sorted set of available port numbers for this socket type
280                 * @throws IllegalStateException if the requested number of available ports could not be found
281                 */
282                SortedSet<Integer> findAvailablePorts(int numRequested, int minPort, int maxPort) {
283                        Assert.isTrue(minPort > 0, "'minPort' must be greater than 0");
284                        Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'");
285                        Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX);
286                        Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0");
287                        Assert.isTrue((maxPort - minPort) >= numRequested,
288                                        "'numRequested' must not be greater than 'maxPort' - 'minPort'");
289
290                        SortedSet<Integer> availablePorts = new TreeSet<Integer>();
291                        int attemptCount = 0;
292                        while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) {
293                                availablePorts.add(findAvailablePort(minPort, maxPort));
294                        }
295
296                        if (availablePorts.size() != numRequested) {
297                                throw new IllegalStateException(String.format(
298                                                "Could not find %d available %s ports in the range [%d, %d]",
299                                                numRequested, name(), minPort, maxPort));
300                        }
301
302                        return availablePorts;
303                }
304        }
305
306}