/*
* This file is part of mlDHT.
*
* mlDHT 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.
*
* mlDHT 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 mlDHT. If not, see <http://www.gnu.org/licenses/>.
*/
package lbms.plugins.mldht.kad;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
import lbms.plugins.mldht.kad.DHT.DHTtype;
import lbms.plugins.mldht.kad.Node.RoutingTable;
import lbms.plugins.mldht.kad.utils.PackUtil;
/**
* @author Damokles
*
*/
public class KClosestNodesSearch {
private Key targetKey;
private List<KBucketEntry> entries;
private int max_entries;
private DHT owner;
private Comparator<KBucketEntry> comp;
public Predicate<KBucketEntry> filter = KBucketEntry::eligibleForNodesList;
/**
* Constructor sets the key to compare with
* @param key The key to compare with
* @param max_entries The maximum number of entries can be in the map
* @return
*/
public KClosestNodesSearch (Key key, int max_entries, DHT owner) {
this.targetKey = key;
this.owner = owner;
this.max_entries = max_entries;
this.comp = new KBucketEntry.DistanceOrder(key);
entries = new ArrayList<KBucketEntry>(max_entries + DHTConstants.MAX_ENTRIES_PER_BUCKET);
}
/**
* @return the Target key of the search
*/
public Key getSearchTarget () {
return targetKey;
}
/**
* @return the number of entries
*/
public int getNumEntries () {
return entries.size();
}
public void fill()
{
fill(false);
}
/**
consider the following routing table:
0000000...
0000001...
000001...
00001...
0001...
001...
01...
1...
now consider the following target key:
1001101111011100000000011101011001111100001100000010111010111110101000100010101011101001101111010011011110000111010010001100001101011110100000010000011001101000
the first bucket we will want to pick values from is 1...
the second bucket with the next-higher xor distance actually is 0001...
This requires a non-contiguous search
*
*/
private void insertBucket(KBucket bucket) {
bucket.entriesStream().filter(filter).forEach(entries::add);
}
private void shave() {
int overshoot = entries.size() - max_entries;
if(overshoot <= 0)
return;
List<KBucketEntry> tail = entries.subList(Math.max(0, entries.size() - DHTConstants.MAX_ENTRIES_PER_BUCKET), entries.size());
tail.sort(comp);
entries.subList(entries.size() - overshoot, entries.size()).clear();
}
public void fill(boolean includeOurself) {
RoutingTable table = owner.getNode().table();
final int initialIdx = table.indexForId(targetKey);
int currentIdx = initialIdx;
Node.RoutingTableEntry current = table.get(initialIdx);
while(true){
// System.out.println(current.prefix);
insertBucket(current.getBucket());
if(entries.size() >= max_entries)
break;
Prefix bucketPrefix = current.prefix;
Prefix targetToBucketDistance = new Prefix(targetKey.distance(bucketPrefix), bucketPrefix.depth); // translate into xor distance, trim trailing bits
Key incrementedDistance = targetToBucketDistance.add(Key.setBit(targetToBucketDistance.depth)); // increment distance by least significant *prefix* bit
Key nextBucketTarget = targetKey.distance(incrementedDistance); // translate back to natural distance
//System.out.println("dist " + targetToBucketDistance.toBinString());
//System.out.println("newDist " + incrementedDistance.toBinString());
//System.out.println("target " + nextBucketTarget.toBinString());
// guess neighbor bucket that might be next in target order
int dir = Integer.signum(nextBucketTarget.compareTo(current.prefix));
int idx;
current = null;
idx = currentIdx + dir;
if(0 <= idx && idx < table.size())
current = table.get(idx);
// do binary search if guess turned out incorrect
if(current == null || !current.prefix.isPrefixOf(nextBucketTarget)) {
idx = table.indexForId(nextBucketTarget);
current = table.get(idx);
}
currentIdx = idx;
// quit if there are insufficient routing table entries to reach the desired size
if(currentIdx == initialIdx)
break;
}
shave();
RPCServer srv = owner.getServerManager().getRandomActiveServer(true);
if(includeOurself && srv != null && srv.getPublicAddress() != null && entries.size() < max_entries)
{
InetSocketAddress sockAddr = new InetSocketAddress(srv.getPublicAddress(), srv.getPort());
entries.add(new KBucketEntry(sockAddr, srv.getDerivedID()));
}
}
public boolean isFull () {
return entries.size() >= max_entries;
}
/**
* Packs the results in a byte array.
*
* @return the encoded results.
*/
public byte[] pack () {
if(entries.size() == 0)
return null;
int entryLength = owner.getType().NODES_ENTRY_LENGTH;
byte[] buffer = new byte[entries.size() * entryLength];
int max_items = buffer.length / 26;
int j = 0;
for (KBucketEntry e : entries) {
if (j >= max_items) {
break;
}
PackUtil.PackBucketEntry(e, buffer, j * entryLength, owner.getType());
j++;
}
return buffer;
}
public NodeList asNodeList() {
return new NodeList() {
@Override
public int packedSize() {
return entries.size() * owner.getType().NODES_ENTRY_LENGTH;
}
@Override
public Stream<KBucketEntry> entries() {
return entries.stream();
}
@Override
public AddressType type() {
return owner.getType() == DHTtype.IPV4_DHT ? AddressType.V4 : AddressType.V6;
}
};
}
/**
* @return a unmodifiable List of the entries
*/
public List<KBucketEntry> getEntries () {
return Collections.unmodifiableList(entries);
}
}