Package: zeroconnect
zeroconnectUse zeroconf to automatically connect devices via TCP on a LAN.
I can hardly believe this doesn't exist already, but after searching for an hour, in despair I resign myself to write my own, and patch the glaring hole in existence.Table of ContentsInstallationUsageLicenseTipsBonus!Installationpip install zeroconnectUsageOne or more servers, and one or more clients, run connected to the same LAN.  (Wifi or ethernet.)Most basicService:fromzeroconnectimportZeroConnectdefrxMessageConnection(messageSock,nodeId,serviceId):print(f"got message connection from{nodeId}")data=messageSock.recvMsg()print(data)messageSock.sendMsg(b"Hello from server")ZeroConnect().advertise(rxMessageConnection,"YOUR_SERVICE_ID_HERE")Client:fromzeroconnectimportZeroConnectmessageSock=ZeroConnect().connectToFirst("YOUR_SERVICE_ID_HERE")messageSock.sendMsg(b"Hello from client")data=messageSock.recvMsg()print(data)Less basicService:fromzeroconnectimportZeroConnectSERVICE_ID="YOUR_SERVICE_ID_HERE"zc=ZeroConnect("NODE_ID")defrxMessageConnection(messageSock,nodeId,serviceId):print(f"got message connection from{nodeId}")# If you also want to spontaneously send messages, pass the socket to e.g. another thread.whileTrue:data=messageSock.recvMsg()print(data)ifdata==b'enable jimjabber':print(f"ENABLE JIMJABBER")elifdata==b'save msg:':toSave=messageSock.recvMsg()print(f"SAVE MESSAGE{toSave}")elifdata==b'marco':messageSock.sendMsg(b'polo')print(f"PING PONGED")elifdata==None:print(f"Connection closed from{nodeId}")messageSock.close()returnelse:print(f"Unhandled message:{data}")# Use messageSock.sock for e.g. sock.getsockname()# I recommend messageSock.close() after you're done with it - but it'll get closed on zc.close(), at leastzc.advertise(rxMessageConnection,SERVICE_ID)# Implicit mode=SocketMode.Messagestry:input("Press enter to exit...\n\n")finally:zc.close()Client:fromzeroconnectimportZeroConnect,SocketModeSERVICE_ID="YOUR_SERVICE_ID_HERE"zc=ZeroConnect("NODE_ID")# Technically the nodeId is optional; it'll assign you a random UUIDads=zc.scan(SERVICE_ID,time=5)# OR: ads = zc.scan(SERVICE_ID, NODE_ID)# An `Ad` contains a `serviceId` and `nodeId` etc.; see `Ad` for detailsmessageSock=zc.connect(ads[0],mode=SocketMode.Messages)# Send and receive messages; the default mode# OR: messageSock = zc.connectToFirst(SERVICE_ID)# OR: messageSock = zc.connectToFirst(nodeId=NODE_ID)# OR: messageSock = zc.connectToFirst(SERVICE_ID, NODE_ID, timeout=10)messageSock.sendMsg(b"enable jimjabber")messageSock.sendMsg(b"save msg:")messageSock.sendMsg(b"i love you")messageSock.sendMsg(b"marco")print(f"rx:{messageSock.recvMsg()}")# ...zc.close()You can also get raw sockets rather than MessageSockets, if you prefer:Server:fromzeroconnectimportZeroConnect,SocketModeSERVICE_ID="YOUR_SERVICE_ID_HERE"zc=ZeroConnect("NODE_ID")defrxRawConnection(sock,nodeId,serviceId):print(f"got raw connection from{nodeId}")data=sock.recv(1024)print(data)sock.sendall(b"Hello from server\n")# sock is a plain socket; use accordinglyzc.advertise(rxRawConnection,SERVICE_ID,mode=SocketMode.Raw)try:input("Press enter to exit...\n\n")finally:zc.close()Client:fromzeroconnectimportZeroConnect,SocketModeSERVICE_ID="YOUR_SERVICE_ID_HERE"zc=ZeroConnect("NODE_ID")# Technically the nodeId is optional; it'll assign you a random UUIDads=zc.scan(SERVICE_ID,time=5)# OR: ads = zc.scan(SERVICE_ID, NODE_ID)# An `Ad` contains a `serviceId` and `nodeId` etc.; see `Ad` for detailssock=zc.connect(ads[0],mode=SocketMode.Raw)# Get the raw streams# OR: sock = zc.connectToFirst(SERVICE_ID, mode=SocketMode.Raw)# OR: sock = zc.connectToFirst(nodeId=NODE_ID, mode=SocketMode.Raw)# OR: sock = zc.connectToFirst(SERVICE_ID, NODE_ID, mode=SocketMode.Raw, timeout=10)sock.sendall(b"Hello from client\n")data=sock.recv(1024)print(f"rx:{data}")# ...zc.close()There's a few other functions you might find useful.  Look at the source code.
Here; I'll paste the declaration of all the publicZeroConnectmethods here.def__init__(self,localId=None):defadvertise(self,callback,serviceId,port=0,host="0.0.0.0",mode=SocketMode.Messages):defscan(self,serviceId=None,nodeId=None,time=30):defscanGen(self,serviceId=None,nodeId=None,time=30):defconnectToFirst(self,serviceId=None,nodeId=None,localServiceId="",mode=SocketMode.Messages,timeout=30):defconnect(self,ad,localServiceId="",mode=SocketMode.Messages):defbroadcast(self,message,serviceId=None,nodeId=None):defgetConnections(self):defclose(self):TipsBe careful not to have two nodes recv from each other at the same time, or they'll deadlock.
However, you CAN have them send at the same time (at least according to my tests).ZeroConnectis intended to be manipulated via its methods, but it probably won't immediately explode if you
read the data in the fields.Note that some computers/networks block zeroconf, or external connection attempts, etc.Callingbroadcastwill automatically clean up dead connections.If you close your socket immediately after sending a message, the data may not finish sending.  Not my fault; blame socket.broadcastuses MessageSockets, so if you're using a raw socket, be aware the message will be prefixed with a header, currently
an 8 byte unsigned long representing the length of the subsequent message.  SeeMessageSocket.See logging.py to see logging settings, or do like so:fromzeroconnect.loggingimport*setLogLevel(-1)# 4+ for everything current, -1 for nothing except uncaught exceptions# It also contains some presets; ERROR/WARN/INFO/VERBOSE/DEBUG atm.# Also, you can move all the logging to stderr with `setLogType(2)`.Bonus!Also includes zcat, like ncat/nc/netcat.  Use as follows:RX:python-mzeroconnect.zcat-lSERVICE_ID[NODE_ID]>FILETX:catFILE|python-mzeroconnect.zcatSERVICE_ID[NODE_ID]Licensezeroconnectis distributed under the terms of theMITlicense.TODOssl
lower timeouts?
connect to all, forever?
connection callback
maybe some automated tests?
.advertiseSingle to get one connection?  for quick stuff?