332 lines
13 KiB
Java
332 lines
13 KiB
Java
/*
|
|
* Copyright 2010 dorkbox, llc
|
|
*
|
|
* 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 dorkbox.network.other.discovery;
|
|
|
|
import java.io.IOException;
|
|
import java.net.Inet6Address;
|
|
import java.net.InetAddress;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.InterfaceAddress;
|
|
import java.net.NetworkInterface;
|
|
import java.net.SocketException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Enumeration;
|
|
import java.util.List;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import dorkbox.network.Client;
|
|
import dorkbox.network.other.discovery.server.BroadcastResponse;
|
|
import dorkbox.network.other.discovery.server.ClientDiscoverHostHandler;
|
|
import dorkbox.network.other.discovery.server.ClientDiscoverHostInitializer;
|
|
import dorkbox.util.OS;
|
|
import io.netty.bootstrap.Bootstrap;
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.buffer.Unpooled;
|
|
import io.netty.channel.Channel;
|
|
import io.netty.channel.ChannelFuture;
|
|
import io.netty.channel.ChannelOption;
|
|
import io.netty.channel.EventLoopGroup;
|
|
import io.netty.channel.nio.NioEventLoopGroup;
|
|
import io.netty.channel.oio.OioEventLoopGroup;
|
|
import io.netty.channel.socket.DatagramPacket;
|
|
import io.netty.channel.socket.nio.NioDatagramChannel;
|
|
import io.netty.channel.socket.oio.OioDatagramChannel;
|
|
|
|
@SuppressWarnings({"unused", "AutoBoxing"})
|
|
public final
|
|
class Broadcast {
|
|
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Client.class.getSimpleName());
|
|
|
|
/**
|
|
* Gets the version number.
|
|
*/
|
|
public static
|
|
String getVersion() {
|
|
return "4.1";
|
|
}
|
|
|
|
/**
|
|
* Broadcasts a UDP message on the LAN to discover any running servers. The address of the first server to respond is returned.
|
|
* <p/>
|
|
* From KryoNet
|
|
*
|
|
* @param udpPort
|
|
* The UDP port of the server.
|
|
* @param discoverTimeoutMillis
|
|
* The number of milliseconds to wait for a response.
|
|
*
|
|
* @return the first server found, or null if no server responded.
|
|
*/
|
|
public static
|
|
BroadcastResponse discoverHost(int udpPort, int discoverTimeoutMillis) throws IOException {
|
|
BroadcastResponse discoverHost = discoverHostAddress(udpPort, discoverTimeoutMillis);
|
|
if (discoverHost != null) {
|
|
return discoverHost;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Broadcasts a UDP message on the LAN to discover any running servers. The address of the first server to respond is returned.
|
|
*
|
|
* @param udpPort
|
|
* The UDP port of the server.
|
|
* @param discoverTimeoutMillis
|
|
* The number of milliseconds to wait for a response.
|
|
*
|
|
* @return the first server found, or null if no server responded.
|
|
*/
|
|
public static
|
|
BroadcastResponse discoverHostAddress(int udpPort, int discoverTimeoutMillis) throws IOException {
|
|
List<BroadcastResponse> servers = discoverHosts0(logger, udpPort, discoverTimeoutMillis, false);
|
|
if (servers.isEmpty()) {
|
|
return null;
|
|
}
|
|
else {
|
|
return servers.get(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Broadcasts a UDP message on the LAN to discover all running servers.
|
|
*
|
|
* @param udpPort
|
|
* The UDP port of the server.
|
|
* @param discoverTimeoutMillis
|
|
* The number of milliseconds to wait for a response.
|
|
*
|
|
* @return the list of found servers (if they responded)
|
|
*/
|
|
public static
|
|
List<BroadcastResponse> discoverHosts(int udpPort, int discoverTimeoutMillis) throws IOException {
|
|
return discoverHosts0(logger, udpPort, discoverTimeoutMillis, true);
|
|
}
|
|
|
|
|
|
static
|
|
List<BroadcastResponse> discoverHosts0(Logger logger, int udpPort, int discoverTimeoutMillis, boolean fetchAllServers) throws IOException {
|
|
// fetch a buffer that contains the serialized object.
|
|
ByteBuf buffer = Unpooled.wrappedBuffer(new byte[]{MagicBytes.broadcastID});
|
|
|
|
List<BroadcastResponse> servers = new ArrayList<BroadcastResponse>();
|
|
|
|
Enumeration<NetworkInterface> networkInterfaces;
|
|
try {
|
|
networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
|
} catch (SocketException e) {
|
|
if (logger != null) {
|
|
logger.error("Host discovery failed.", e);
|
|
}
|
|
throw new IOException("Host discovery failed. No interfaces found.");
|
|
}
|
|
|
|
|
|
scan:
|
|
for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
|
|
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
|
|
InetAddress address = interfaceAddress.getAddress();
|
|
InetAddress broadcast = interfaceAddress.getBroadcast();
|
|
|
|
// don't use IPv6!
|
|
if (address instanceof Inet6Address) {
|
|
if (logger != null) {
|
|
if (logger.isInfoEnabled()) {
|
|
logger.info("Not using IPv6 address: {}", address);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
|
|
try {
|
|
if (logger != null) {
|
|
if (logger.isInfoEnabled()) {
|
|
logger.info("Searching for host on [{}:{}]", address.getHostAddress(), udpPort);
|
|
}
|
|
}
|
|
|
|
EventLoopGroup group;
|
|
Class<? extends Channel> channelClass;
|
|
|
|
if (OS.isAndroid()) {
|
|
// android ONLY supports OIO (not NIO)
|
|
group = new OioEventLoopGroup(1);
|
|
channelClass = OioDatagramChannel.class;
|
|
} else {
|
|
group = new NioEventLoopGroup(1);
|
|
channelClass = NioDatagramChannel.class;
|
|
}
|
|
|
|
|
|
Bootstrap udpBootstrap = new Bootstrap().group(group)
|
|
.channel(channelClass)
|
|
.option(ChannelOption.SO_BROADCAST, true)
|
|
.handler(new ClientDiscoverHostInitializer())
|
|
.localAddress(new InetSocketAddress(address,
|
|
0)); // pick random address. Not listen for broadcast.
|
|
|
|
// we don't care about RECEIVING a broadcast packet, we are only SENDING one.
|
|
ChannelFuture future;
|
|
try {
|
|
future = udpBootstrap.bind();
|
|
future.await();
|
|
} catch (InterruptedException e) {
|
|
if (logger != null) {
|
|
logger.error("Could not bind to random UDP address on the server.", e.getCause());
|
|
}
|
|
throw new IOException("Could not bind to random UDP address on the server.");
|
|
}
|
|
|
|
if (!future.isSuccess()) {
|
|
if (logger != null) {
|
|
logger.error("Could not bind to random UDP address on the server.", future.cause());
|
|
}
|
|
throw new IOException("Could not bind to random UDP address on the server.");
|
|
}
|
|
|
|
Channel channel1 = future.channel();
|
|
|
|
if (broadcast != null) {
|
|
// try the "defined" broadcast first if we have it (not always!)
|
|
channel1.writeAndFlush(new DatagramPacket(buffer, new InetSocketAddress(broadcast, udpPort)));
|
|
|
|
// response is received. If the channel is not closed within 5 seconds, move to the next one.
|
|
if (!channel1.closeFuture().awaitUninterruptibly(discoverTimeoutMillis)) {
|
|
if (logger != null) {
|
|
if (logger.isInfoEnabled()) {
|
|
logger.info("Host discovery timed out.");
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
BroadcastResponse broadcastResponse = channel1.attr(ClientDiscoverHostHandler.STATE).get();
|
|
servers.add(broadcastResponse);
|
|
}
|
|
|
|
|
|
// keep going if we want to fetch all servers. Break if we found one.
|
|
if (!(fetchAllServers || servers.isEmpty())) {
|
|
channel1.close().await();
|
|
group.shutdownGracefully().await();
|
|
break scan;
|
|
}
|
|
}
|
|
|
|
// continue with "common" broadcast addresses.
|
|
// Java 1.5 doesn't support getting the subnet mask, so try them until we find one.
|
|
|
|
byte[] ip = address.getAddress();
|
|
for (int octect = 3; octect >= 0; octect--) {
|
|
ip[octect] = -1; // 255.255.255.0
|
|
|
|
// don't error out on one particular octect
|
|
try {
|
|
InetAddress byAddress = InetAddress.getByAddress(ip);
|
|
channel1.writeAndFlush(new DatagramPacket(buffer, new InetSocketAddress(byAddress, udpPort)));
|
|
|
|
|
|
// response is received. If the channel is not closed within 5 seconds, move to the next one.
|
|
if (!channel1.closeFuture().awaitUninterruptibly(discoverTimeoutMillis)) {
|
|
if (logger != null) {
|
|
if (logger.isInfoEnabled()) {
|
|
logger.info("Host discovery timed out.");
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
BroadcastResponse broadcastResponse = channel1.attr(ClientDiscoverHostHandler.STATE).get();
|
|
servers.add(broadcastResponse);
|
|
|
|
if (!fetchAllServers) {
|
|
break;
|
|
}
|
|
}
|
|
} catch (Exception ignored) {
|
|
}
|
|
}
|
|
|
|
channel1.close().sync();
|
|
group.shutdownGracefully(0, discoverTimeoutMillis, TimeUnit.MILLISECONDS);
|
|
|
|
} catch (Exception ignored) {
|
|
}
|
|
|
|
// keep going if we want to fetch all servers. Break if we found one.
|
|
if (!(fetchAllServers || servers.isEmpty())) {
|
|
break scan;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (logger != null && logger.isInfoEnabled() && !servers.isEmpty()) {
|
|
StringBuilder stringBuilder = new StringBuilder(256);
|
|
|
|
if (fetchAllServers) {
|
|
stringBuilder.append("Discovered servers: (")
|
|
.append(servers.size())
|
|
.append(")");
|
|
|
|
for (BroadcastResponse server : servers) {
|
|
stringBuilder.append("/n")
|
|
.append(server.remoteAddress)
|
|
.append(":");
|
|
|
|
if (server.tcpPort > 0) {
|
|
stringBuilder.append(server.tcpPort);
|
|
|
|
if (server.udpPort > 0) {
|
|
stringBuilder.append(":");
|
|
}
|
|
}
|
|
if (server.udpPort > 0) {
|
|
stringBuilder.append(udpPort);
|
|
}
|
|
}
|
|
logger.info(stringBuilder.toString());
|
|
}
|
|
else {
|
|
BroadcastResponse server = servers.get(0);
|
|
stringBuilder.append(server.remoteAddress)
|
|
.append(":");
|
|
|
|
if (server.tcpPort > 0) {
|
|
stringBuilder.append(server.tcpPort);
|
|
|
|
if (server.udpPort > 0) {
|
|
stringBuilder.append(":");
|
|
}
|
|
}
|
|
if (server.udpPort > 0) {
|
|
stringBuilder.append(udpPort);
|
|
}
|
|
|
|
logger.info("Discovered server [{}]", stringBuilder.toString());
|
|
}
|
|
}
|
|
|
|
return servers;
|
|
}
|
|
|
|
private
|
|
Broadcast() {
|
|
}
|
|
}
|
|
|