added travis file and some more tests
This commit is contained in:
parent
2a4802f56f
commit
ee0c6b9bd5
6
.travis.yml
Normal file
6
.travis.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "2.7"
|
||||||
|
- "pypy"
|
||||||
|
install: pip install . --use-mirrors
|
||||||
|
script: trial kademlia
|
6
Makefile
6
Makefile
@ -1,5 +1,8 @@
|
|||||||
PYDOCTOR=pydoctor
|
PYDOCTOR=pydoctor
|
||||||
|
|
||||||
|
test:
|
||||||
|
trial kademlia
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
$(PYDOCTOR) --make-html --html-output apidoc --add-package kademlia --project-name=kademlia --project-url=http://github.com/bmuller/kademlia --html-use-sorttable --html-use-splitlinks --html-shorten-lists
|
$(PYDOCTOR) --make-html --html-output apidoc --add-package kademlia --project-name=kademlia --project-url=http://github.com/bmuller/kademlia --html-use-sorttable --html-use-splitlinks --html-shorten-lists
|
||||||
|
|
||||||
@ -9,6 +12,3 @@ lint:
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
python setup.py install
|
python setup.py install
|
||||||
|
|
||||||
test:
|
|
||||||
trial kademlia
|
|
||||||
|
@ -1,18 +1,49 @@
|
|||||||
# [Kademlia](http://en.wikipedia.org/wiki/Kademlia) in Python
|
# [Kademlia](http://en.wikipedia.org/wiki/Kademlia) in Python
|
||||||
|
[![Build Status](https://secure.travis-ci.org/bmuller/kademlia.png?branch=master)](https://travis-ci.org/bmuller/kademlia)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
easy_install kademlia
|
pip install kademlia
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
*This assumes you have a working familiarity with Twisted.*
|
*This assumes you have a working familiarity with Twisted.*
|
||||||
|
|
||||||
|
Assuming you want to connect to an existing network (run the standalone server example below if you don't have a network):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
from twisted.python import log
|
||||||
|
from kademlia.network import Server
|
||||||
|
import sys
|
||||||
|
|
||||||
...
|
# log to std out
|
||||||
|
log.startLogging(sys.stdout)
|
||||||
|
|
||||||
|
def quit(result):
|
||||||
|
print "Key result:", result
|
||||||
|
reactor.stop()
|
||||||
|
|
||||||
|
def get(result, server):
|
||||||
|
reactor.stop()
|
||||||
|
#return server.get("a key").addCallback(quit)
|
||||||
|
|
||||||
|
def done(found, server):
|
||||||
|
log.msg("Found nodes: %s" % found)
|
||||||
|
return server.set("a key", "a value").addCallback(get, server)
|
||||||
|
|
||||||
|
server = Server()
|
||||||
|
# next line, or use reactor.listenUDP(5678, server)
|
||||||
|
server.listen(5678)
|
||||||
|
server.bootstrap([('127.0.0.1', 1234)]).addCallback(done, two)
|
||||||
|
|
||||||
|
reactor.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stand-alone Server
|
||||||
|
If all you want to do is run a local server, just start the example server:
|
||||||
|
|
||||||
|
```
|
||||||
|
twistd -noy server.tac
|
||||||
```
|
```
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
from twisted.internet import reactor
|
|
||||||
from twisted.python import log
|
|
||||||
from kademlia.network import Server
|
|
||||||
import sys
|
|
||||||
|
|
||||||
log.startLogging(sys.stdout)
|
|
||||||
one = Server(1234)
|
|
||||||
reactor.run()
|
|
@ -4,7 +4,7 @@ Package for interacting on the network at a high level.
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer, reactor
|
||||||
|
|
||||||
from kademlia.log import Logger
|
from kademlia.log import Logger
|
||||||
from kademlia.protocol import KademliaProtocol
|
from kademlia.protocol import KademliaProtocol
|
||||||
@ -113,7 +113,7 @@ class Server(object):
|
|||||||
to start listening as an active node on the network.
|
to start listening as an active node on the network.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, port, ksize=20, alpha=3):
|
def __init__(self, ksize=20, alpha=3):
|
||||||
"""
|
"""
|
||||||
Create a server instance. This will start listening on the given port.
|
Create a server instance. This will start listening on the given port.
|
||||||
|
|
||||||
@ -125,10 +125,13 @@ class Server(object):
|
|||||||
self.alpha = alpha
|
self.alpha = alpha
|
||||||
self.log = Logger(system=self)
|
self.log = Logger(system=self)
|
||||||
storage = ForgetfulStorage()
|
storage = ForgetfulStorage()
|
||||||
self.node = Node('127.0.0.1', port, digest(random.getrandbits(255)))
|
self.node = Node(None, None, digest(random.getrandbits(255)))
|
||||||
self.protocol = KademliaProtocol(self.node, storage, ksize)
|
self.protocol = KademliaProtocol(self.node.id, storage, ksize)
|
||||||
self.refreshLoop = LoopingCall(self.refreshTable).start(3600)
|
self.refreshLoop = LoopingCall(self.refreshTable).start(3600)
|
||||||
|
|
||||||
|
def listen(self, port):
|
||||||
|
return reactor.listenUDP(port, self.protocol)
|
||||||
|
|
||||||
def refreshTable(self):
|
def refreshTable(self):
|
||||||
"""
|
"""
|
||||||
Refresh buckets that haven't had any lookups in the last hour
|
Refresh buckets that haven't had any lookups in the last hour
|
||||||
|
@ -7,11 +7,11 @@ from kademlia.log import Logger
|
|||||||
|
|
||||||
|
|
||||||
class KademliaProtocol(RPCProtocol):
|
class KademliaProtocol(RPCProtocol):
|
||||||
def __init__(self, node, storage, ksize):
|
def __init__(self, sourceID, storage, ksize):
|
||||||
RPCProtocol.__init__(self, node.port)
|
RPCProtocol.__init__(self)
|
||||||
self.router = RoutingTable(self, ksize)
|
self.router = RoutingTable(self, ksize)
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
self.sourceID = node.id
|
self.sourceID = sourceID
|
||||||
self.log = Logger(system=self)
|
self.log = Logger(system=self)
|
||||||
|
|
||||||
def getRefreshIDs(self):
|
def getRefreshIDs(self):
|
||||||
|
@ -93,13 +93,15 @@ class RoutingTable(object):
|
|||||||
def __init__(self, protocol, ksize):
|
def __init__(self, protocol, ksize):
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.ksize = ksize
|
self.ksize = ksize
|
||||||
self.buckets = [KBucket(0, 2 ** 160, ksize)]
|
self.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.buckets = [KBucket(0, 2 ** 160, self.ksize)]
|
||||||
|
|
||||||
def splitBucket(self, index):
|
def splitBucket(self, index):
|
||||||
one, two = self.buckets[index].split()
|
one, two = self.buckets[index].split()
|
||||||
self.buckets[index] = one
|
self.buckets[index] = one
|
||||||
self.buckets.insert(index + 1, two)
|
self.buckets.insert(index + 1, two)
|
||||||
|
|
||||||
# todo split one/two if needed based on section 4.2
|
# todo split one/two if needed based on section 4.2
|
||||||
|
|
||||||
def getLonelyBuckets(self):
|
def getLonelyBuckets(self):
|
||||||
|
@ -3,7 +3,9 @@ import hashlib
|
|||||||
|
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
|
|
||||||
|
from kademlia.utils import digest
|
||||||
from kademlia.node import Node, NodeHeap
|
from kademlia.node import Node, NodeHeap
|
||||||
|
from kademlia.tests.utils import mknode
|
||||||
|
|
||||||
|
|
||||||
class NodeTest(unittest.TestCase):
|
class NodeTest(unittest.TestCase):
|
||||||
@ -26,3 +28,30 @@ class NodeHeapTest(unittest.TestCase):
|
|||||||
def test_maxSize(self):
|
def test_maxSize(self):
|
||||||
n = NodeHeap(3)
|
n = NodeHeap(3)
|
||||||
self.assertEqual(0, len(n))
|
self.assertEqual(0, len(n))
|
||||||
|
|
||||||
|
for d in range(10):
|
||||||
|
n.push(d, mknode())
|
||||||
|
self.assertEqual(3, len(n))
|
||||||
|
|
||||||
|
self.assertEqual(3, len(list(n)))
|
||||||
|
|
||||||
|
def test_iteration(self):
|
||||||
|
heap = NodeHeap(5)
|
||||||
|
nodes = [mknode(id=digest(x)) for x in range(10)]
|
||||||
|
for index, node in enumerate(nodes):
|
||||||
|
heap.push(index, node)
|
||||||
|
for index, node in enumerate(heap):
|
||||||
|
self.assertEqual(digest(index), node.id)
|
||||||
|
self.assertTrue(index < 5)
|
||||||
|
|
||||||
|
def test_remove(self):
|
||||||
|
heap = NodeHeap(5)
|
||||||
|
nodes = [mknode(id=digest(x)) for x in range(10)]
|
||||||
|
for index, node in enumerate(nodes):
|
||||||
|
heap.push(index, node)
|
||||||
|
|
||||||
|
heap.remove([nodes[0].id, nodes[1].id])
|
||||||
|
self.assertEqual(len(list(heap)), 5)
|
||||||
|
for index, node in enumerate(heap):
|
||||||
|
self.assertEqual(digest(index + 2), node.id)
|
||||||
|
self.assertTrue(index < 5)
|
||||||
|
52
kademlia/tests/test_routing.py
Normal file
52
kademlia/tests/test_routing.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from twisted.trial import unittest
|
||||||
|
|
||||||
|
from kademlia.routing import KBucket, RoutingTable
|
||||||
|
from kademlia.protocol import KademliaProtocol
|
||||||
|
from kademlia.tests.utils import mknode, FakeProtocol
|
||||||
|
|
||||||
|
|
||||||
|
class KBucketTest(unittest.TestCase):
|
||||||
|
def test_split(self):
|
||||||
|
bucket = KBucket(0, 10, 5)
|
||||||
|
bucket.addNode(mknode(intid=5))
|
||||||
|
bucket.addNode(mknode(intid=6))
|
||||||
|
one, two = bucket.split()
|
||||||
|
self.assertEqual(len(one), 1)
|
||||||
|
self.assertEqual(one.range, (0, 5))
|
||||||
|
self.assertEqual(len(two), 1)
|
||||||
|
self.assertEqual(two.range, (6, 10))
|
||||||
|
|
||||||
|
def test_addNode(self):
|
||||||
|
# when full, return false
|
||||||
|
bucket = KBucket(0, 10, 2)
|
||||||
|
self.assertTrue(bucket.addNode(mknode()))
|
||||||
|
self.assertTrue(bucket.addNode(mknode()))
|
||||||
|
self.assertFalse(bucket.addNode(mknode()))
|
||||||
|
self.assertEqual(len(bucket), 2)
|
||||||
|
|
||||||
|
# make sure when a node is double added it's put at the end
|
||||||
|
bucket = KBucket(0, 10, 3)
|
||||||
|
nodes = [mknode(), mknode(), mknode()]
|
||||||
|
for node in nodes:
|
||||||
|
bucket.addNode(node)
|
||||||
|
for index, node in enumerate(bucket.getNodes()):
|
||||||
|
self.assertEqual(node, nodes[index])
|
||||||
|
|
||||||
|
def test_inRange(self):
|
||||||
|
bucket = KBucket(0, 10, 10)
|
||||||
|
self.assertTrue(bucket.hasInRange(mknode(intid=5)))
|
||||||
|
self.assertFalse(bucket.hasInRange(mknode(intid=11)))
|
||||||
|
self.assertTrue(bucket.hasInRange(mknode(intid=10)))
|
||||||
|
self.assertTrue(bucket.hasInRange(mknode(intid=0)))
|
||||||
|
|
||||||
|
|
||||||
|
class RoutingTableTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.id = mknode().id
|
||||||
|
self.protocol = FakeProtocol(self.id)
|
||||||
|
self.router = self.protocol.router
|
||||||
|
|
||||||
|
def test_addContact(self):
|
||||||
|
self.router.addContact(mknode())
|
||||||
|
self.assertTrue(len(self.router.buckets), 1)
|
||||||
|
self.assertTrue(len(self.router.buckets[0].nodes), 1)
|
94
kademlia/tests/utils.py
Normal file
94
kademlia/tests/utils.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
Utility functions for tests.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
from struct import pack
|
||||||
|
|
||||||
|
from kademlia.node import Node
|
||||||
|
from kademlia.routing import RoutingTable
|
||||||
|
|
||||||
|
|
||||||
|
def mknode(ip=None, port=None, id=None, intid=None):
|
||||||
|
"""
|
||||||
|
Make a node. Created a random id if not specified.
|
||||||
|
"""
|
||||||
|
if intid is not None:
|
||||||
|
id = pack('>l', intid)
|
||||||
|
id = id or hashlib.sha1(str(random.getrandbits(255))).digest()
|
||||||
|
return Node(ip, port, id)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeProtocol(object):
|
||||||
|
def __init__(self, sourceID, ksize=20):
|
||||||
|
self.router = RoutingTable(self, ksize)
|
||||||
|
self.storage = {}
|
||||||
|
self.sourceID = sourceID
|
||||||
|
|
||||||
|
def getRefreshIDs(self):
|
||||||
|
"""
|
||||||
|
Get ids to search for to keep old buckets up to date.
|
||||||
|
"""
|
||||||
|
ids = []
|
||||||
|
for bucket in self.router.getLonelyBuckets():
|
||||||
|
ids.append(random.randint(*bucket.range))
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def rpc_ping(self, sender, nodeid):
|
||||||
|
source = Node(sender[0], sender[1], nodeid)
|
||||||
|
self.router.addContact(source)
|
||||||
|
return self.sourceID
|
||||||
|
|
||||||
|
def rpc_store(self, sender, nodeid, key, value):
|
||||||
|
source = Node(sender[0], sender[1], nodeid)
|
||||||
|
self.router.addContact(source)
|
||||||
|
self.log.debug("got a store request from %s, storing value" % str(sender))
|
||||||
|
self.storage[key] = value
|
||||||
|
|
||||||
|
def rpc_find_node(self, sender, nodeid, key):
|
||||||
|
self.log.info("finding neighbors of %i in local table" % long(nodeid.encode('hex'), 16))
|
||||||
|
source = Node(sender[0], sender[1], nodeid)
|
||||||
|
self.router.addContact(source)
|
||||||
|
node = Node(None, None, key)
|
||||||
|
return map(tuple, self.router.findNeighbors(node, exclude=source))
|
||||||
|
|
||||||
|
def rpc_find_value(self, sender, nodeid, key):
|
||||||
|
source = Node(sender[0], sender[1], nodeid)
|
||||||
|
self.router.addContact(source)
|
||||||
|
value = self.storage.get(key, None)
|
||||||
|
if value is None:
|
||||||
|
return self.rpc_find_node(sender, nodeid, key)
|
||||||
|
return { 'value': value }
|
||||||
|
|
||||||
|
def callFindNode(self, nodeToAsk, nodeToFind):
|
||||||
|
address = (nodeToAsk.ip, nodeToAsk.port)
|
||||||
|
d = self.find_node(address, self.sourceID, nodeToFind.id)
|
||||||
|
return d.addCallback(self.handleCallResponse, nodeToAsk)
|
||||||
|
|
||||||
|
def callFindValue(self, nodeToAsk, nodeToFind):
|
||||||
|
address = (nodeToAsk.ip, nodeToAsk.port)
|
||||||
|
d = self.find_value(address, self.sourceID, nodeToFind.id)
|
||||||
|
return d.addCallback(self.handleCallResponse, nodeToAsk)
|
||||||
|
|
||||||
|
def callPing(self, nodeToAsk):
|
||||||
|
address = (nodeToAsk.ip, nodeToAsk.port)
|
||||||
|
d = self.ping(address, self.sourceID)
|
||||||
|
return d.addCallback(self.handleCallResponse, nodeToAsk)
|
||||||
|
|
||||||
|
def callStore(self, nodeToAsk, key, value):
|
||||||
|
address = (nodeToAsk.ip, nodeToAsk.port)
|
||||||
|
d = self.store(address, self.sourceID, key, value)
|
||||||
|
return d.addCallback(self.handleCallResponse, nodeToAsk)
|
||||||
|
|
||||||
|
def handleCallResponse(self, result, node):
|
||||||
|
"""
|
||||||
|
If we get a response, add the node to the routing table. If
|
||||||
|
we get no response, make sure it's removed from the routing table.
|
||||||
|
"""
|
||||||
|
if result[0]:
|
||||||
|
self.log.info("got response from %s, adding to router" % node)
|
||||||
|
self.router.addContact(node)
|
||||||
|
else:
|
||||||
|
self.log.debug("no response from %s, removing from router" % node)
|
||||||
|
self.router.removeContact(node)
|
||||||
|
return result
|
9
server.tac
Normal file
9
server.tac
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from twisted.application import service, internet
|
||||||
|
import sys, os
|
||||||
|
sys.path.append(os.path.dirname(__file__))
|
||||||
|
from kademlia.network import Server
|
||||||
|
|
||||||
|
application = service.Application("kademlia")
|
||||||
|
kserver = Server()
|
||||||
|
server = internet.UDPServer(1234, kserver.protocol)
|
||||||
|
server.setServiceParent(application)
|
2
setup.py
2
setup.py
@ -12,5 +12,5 @@ setup(
|
|||||||
url="http://github.com/bmuller/kademlia",
|
url="http://github.com/bmuller/kademlia",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
requires=["twisted", "rpcudp"],
|
requires=["twisted", "rpcudp"],
|
||||||
install_requires=['twisted>=12.0', "rpcudp>=0.3"]
|
install_requires=['twisted>=13.0', "rpcudp>=1.0"]
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user