Sacrilege and Zeroconf

(Yes, I will get back to CouchDB soon. It's my blog, I can post what I want. :)) Recently, I've been toying around with Zeroconf, the "look ma, no hands" network service discovery better known as Bonjour on Mac and Avahi on Linux. It actually does a lot of different things, from "headless" network address negotiation to host name resolution, all built on top of (mostly) the same stuff that drives DNS across the web. It's really cool stuff, and also really late for me to be getting into the game. Anyway, the stuff I am most interested in is the "service discovery", or the way two iTunes clients see each other without anyone entering in IP addresses. This is solid user-friendly tech, and this style of thinking should be implemented in more software. So, when I found pyzeroconf, a pure python implementation of the service discovery stack, I was stoked. Until I tried running it, and all of the tests failed no matter what system I ran it on. I was no longer stoked. Thus I began reading, and this is always a dangerous thing. Below is just some fun I've been having. It is a horrific test implementation of multicast UDP service discovery. About the only thing it shares in common with Bonjour is that it uses the same multicast address. I understand that many educated network programmers will see this and run screaming -- don't worry, this is just to get a feel for the way it all works. Actually, any critiques / advice / links would be most welcome, as anyone who comes stumbling along here via the alleys of Google will also learn from it. This should run in any default install of Python. There are some caveats, as this is just something I've thrown together. First: it doesn't actually do anything with the service, it just prints out a list of every compatible service it finds (including itself). Second: I only tried it once in Windows, but while my Ubuntu box and Macbook saw the Windows service, Windows wouldn't see any, even itself, so I have some hacking / learning to do if I want to support the devil. Finally -- and this is where it gets really funny -- I have had a few times where if I leave them running for too long I can't hit the outside internet. At all. So, yeah, not a perfect implementation at all yet, but I'm moving along. :) Eventually, I'll tack on a graphical user interface and, you know, conform to some actual specification as opposed to throwing strings around. Without further ado, my Frankenstein: [sourcecode language="py"] import socket from threading import Thread, Lock import time import sys # Multicast Globals MC_IP = '224.0.0.251' # Oh no he didn't MC_PORT = 5354 # Oh yes he did def message(text): # Just an stdout.write wrapper sys.stdout.write(text) sys.stdout.flush() class Broadcast(Thread): def __init__(self, client): Thread.__init__(self) self.die = False self.client = client self.lock = Lock() def run(self): self.emit('UP') self.last_emit = time.time() while True: if self.die: self.emit('DOWN') break # So the emit thread stays alive time.sleep(3) def emit(self, status='ALIVE'): self.lock.acquire() if status not in ('UP', 'DOWN', 'ALIVE'): raise ValueError('Status "%s" not valid.' % status) message = '_woot._tcp.local.|%s|%s|%s' % (self.client.port, self.client.hostname, status) sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM ) sock.sendto(message, (MC_IP, MC_PORT)) sock.close() self.lock.release() class Browser(Thread): def __init__(self, client): Thread.__init__(self) self.client = client self.die = False def run(self): while True: if self.die: break sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM ) sock.bind((MC_IP, MC_PORT)) data, addr = sock.recvfrom( 1024 ) sock.close() self.parse(data, addr) def parse(self, data, addr): """ DATA STRUCTURE: _woot._tcp.local.|port|hostname|STATUS STATUS can be UP, DOWN, or ALIVE """ if not data.startswith('_woot._tcp.local.'): return data_list = data.split('|') if len(data_list) 1: port = int(sys.argv[1]) client = Client(port) client.run() [/sourcecode] Save it as something (I like client.py personally) and run it just by: [sourcecode language="sh"] python client.py [/sourcecode] Optionally, you can add a port to the end of it, and that is the port the actual client will run on. The system should be smart enough to tell the other systems about it, but if it doesn't work, hey, it's a pre-alpha script file from the internet -- give me a break. :) You should get something like the following if it is by itself: [sourcecode language="sh"] Starting up: josh-laptop-ubuntu:1337 Searching for services. 1 SERVICE(S(): * josh-laptop-ubuntu - 192.168.0.120:1337 | UP [/sourcecode] ...with the SERVICE(S) list printing out to the terminal every few seconds. DO NOT run two of these on the same computer, I haven't done it and I'm not at all certain what would happen. If you want to test multiple ones, use different computers or at least different virtual machines with local addresses. Feel free to shout at me in the comments!