//package com.ryanm.util.net;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
/**
* Performs broadcast and multicast peer detection. How well this works depends
* on your network configuration
*
* @author ryanm
*/
public class PeerDiscovery {
private static final byte QUERY_PACKET = 80;
private static final byte RESPONSE_PACKET = 81;
/**
* The group identifier. Determines the set of peers that are able to discover
* each other
*/
public final int group;
/**
* The port number that we operate on
*/
public final int port;
/**
* Data returned with discovery
*/
public int peerData;
private final DatagramSocket bcastSocket;
private final InetSocketAddress broadcastAddress;
private boolean shouldStop = false;
private List<Peer> responseList = null;
/**
* Used to detect and ignore this peers response to it's own query. When we
* send a response packet, we set this to the destination. When we receive a
* response, if this matches the source, we know that we're talking to
* ourselves and we can ignore the response.
*/
private InetAddress lastResponseDestination = null;
/**
* Redefine this to be notified of exceptions on the listen thread. Default
* behaviour is to print to stdout. Can be left as null for no-op
*/
public ExceptionHandler rxExceptionHandler = new ExceptionHandler();
private Thread bcastListen = new Thread(PeerDiscovery.class.getSimpleName() + " broadcast listen thread") {
@Override
public void run() {
try {
byte[] buffy = new byte[5];
DatagramPacket rx = new DatagramPacket(buffy, buffy.length);
while (!shouldStop) {
try {
buffy[0] = 0;
bcastSocket.receive(rx);
int recData = decode(buffy, 1);
if (buffy[0] == QUERY_PACKET && recData == group) {
byte[] data = new byte[5];
data[0] = RESPONSE_PACKET;
encode(peerData, data, 1);
DatagramPacket tx = new DatagramPacket(data, data.length, rx.getAddress(), port);
lastResponseDestination = rx.getAddress();
bcastSocket.send(tx);
} else if (buffy[0] == RESPONSE_PACKET) {
if (responseList != null && !rx.getAddress().equals(lastResponseDestination)) {
synchronized (responseList) {
responseList.add(new Peer(rx.getAddress(), recData));
}
}
}
} catch (SocketException se) {
// someone may have called disconnect()
}
}
bcastSocket.disconnect();
bcastSocket.close();
} catch (Exception e) {
if (rxExceptionHandler != null) {
rxExceptionHandler.handle(e);
}
}
};
};
/**
* Constructs a UDP broadcast-based peer
*
* @param group
* The identifier shared by the peers that will be discovered.
* @param port
* a valid port, i.e.: in the range 1025 to 65535 inclusive
* @throws IOException
*/
public PeerDiscovery(int group, int port) throws IOException {
this.group = group;
this.port = port;
// http://stackoverflow.com/questions/30360797/udp-broadcast-client-in-java
bcastSocket = new DatagramSocket(port);
broadcastAddress = new InetSocketAddress("255.255.255.255", port);
bcastListen.setDaemon(true);
bcastListen.start();
}
/**
* Signals this {@link PeerDiscovery} to shut down. This call will block until
* everything's timed out and closed etc.
*/
public void disconnect() {
shouldStop = true;
bcastSocket.close();
bcastSocket.disconnect();
try {
bcastListen.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Queries the network and finds the addresses of other peers in the same
* group
*
* @param timeout
* How long to wait for responses, in milliseconds. Call will block
* for this long, although you can {@link Thread#interrupt()} to cut
* the wait short
* @param peerType
* The type flag of the peers to look for
* @return The addresses of other peers in the group
* @throws IOException
* If something goes wrong when sending the query packet
*/
public Peer[] getPeers(int timeout, byte peerType) throws IOException {
responseList = new ArrayList<Peer>();
// send query byte, appended with the group id
byte[] data = new byte[5];
data[0] = QUERY_PACKET;
encode(group, data, 1);
DatagramPacket tx = new DatagramPacket(data, data.length, broadcastAddress);
bcastSocket.send(tx);
// wait for the listen thread to do its thing
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
}
Peer[] peers;
synchronized (responseList) {
peers = responseList.toArray(new Peer[responseList.size()]);
}
responseList = null;
return peers;
}
/**
* Record of a peer
*
* @author ryanm
*/
public class Peer {
/**
* The ip of the peer
*/
public final InetAddress ip;
/**
* The data of the peer
*/
public final int data;
private Peer(InetAddress ip, int data) {
this.ip = ip;
this.data = data;
}
@Override
public String toString() {
return ip.getHostAddress() + " " + data;
}
}
/**
* Handles an exception.
*
* @author ryanm
*/
public class ExceptionHandler {
/**
* Called whenever an exception is thrown from the listen thread. The listen
* thread should now be dead
*
* @param e
*/
public void handle(Exception e) {
e.printStackTrace();
}
}
/**
* @param args
*/
public static void main(String[] args) {
try {
int group = 6969;
PeerDiscovery mp = new PeerDiscovery(group, 6969);
boolean stop = false;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (!stop) {
System.out.println("enter \"q\" to quit, or anything else to query peers");
String s = br.readLine();
if (s.equals("q")) {
System.out.print("Closing down...");
mp.disconnect();
System.out.println(" done");
stop = true;
} else {
System.out.println("Querying");
Peer[] peers = mp.getPeers(100, (byte) 0);
System.out.println(peers.length + " peers found");
for (Peer p : peers) {
System.out.println("\t" + p);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static int decode(byte[] b, int index) {
int i = 0;
i |= b[index] << 24;
i |= b[index + 1] << 16;
i |= b[index + 2] << 8;
i |= b[index + 3];
return i;
}
private static void encode(int i, byte[] b, int index) {
b[index] = (byte) (i >> 24 & 0xff);
b[index + 1] = (byte) (i >> 16 & 0xff);
b[index + 2] = (byte) (i >> 8 & 0xff);
b[index + 3] = (byte) (i & 0xff);
}
}