The Petrichor Protocol Primitives
=================================
This is an idea I have for a basic set of primitives that let you build
protocols quickly and easily. It is similar to ZeroMQ except:
1. It's transparent and you can actually understand it.
2. It could be implemented in any language, not just C++.
3. It doesn't blow up your server with any of 492 asserts.
4. You can put it on the internet without worrying about it.
5. It can transmit actual data structures or raw binary data.
6. You can put it in an SSL socket or any other crypto protocol.
7. It will work with any socket system, events, or threads.
Getting The Code
================
You can get the code at https://gitorious.org/petrichor-protocol/petrichor-protocol
which contains a simple Python proof-of-concept to see what people think about the idea.
Here's what's in this first version:
* Look at the client.py and server.py to see an example of an advanced echo server.
* Look at petrichor.py to see what it takes to make it work.
* Look at tnetstrings.py and http://tnetstrings.org/ to see the data you can send.
Planned Features
================
* Clean up the implementation and make it a real python package.
* Implement the Dedupe, SSL, and Compression helpers.
* Create samples for different existing protocol semantics.
* Write versions for Ruby and C.
* Create a literate document teaching you how to implement Petrichor yourself as the spec.
* Get a decent website that isn't a PRE tag.
The Entire Protocol So Far
==========================
Petrichor is simply sequences of paired TNetstrings as HEADER,BODY.
The main API is:
def read_chunk(conn):
payload = read_payload(conn)
if payload:
data, _ = tnetstrings.parse(payload)
return data
else:
return None
def recv(conn):
header = read_chunk(conn)
if header == None or type(header) != dict:
return None, None
data = read_chunk(conn)
return header, data
def send(conn, header, body):
data = tnetstrings.dump(body)
conn.write(tnetstrings.dump(header) + data)
conn.flush()
Then there's a few helper functions for common stuff:
def connect(host, port, header, body="", timeout=20):
sock = socket.create_connection((host, port), timeout)
sock.setblocking(1)
conn = sock.makefile()
send(conn, header, body)
conn.flush()
header, body = recv(conn)
return sock, conn, header, body
def connect_allowed(header):
return header.get('allowed', True)
def accept(conn):
return recv(conn)
def peer_allowed(conn, header, msg):
header['allowed'] = True
send(conn, header, msg)
def peer_denied(conn, header, msg):
header['allowed'] = False
send(conn, header, msg)
def request(conn, header, body):
send(conn, header, body)
header, body = recv(conn)
return header, body
Other than that there's just some extra gear for reading TNetstrings
off the wire, which will probably go into the tnetstrings library.
Sample Client For The Lazy
==========================
The following is an echo client with probably more features than
an echo client should have:
import petrichor
import sys
from uuid import uuid4
VERSION = "0.1"
HOST, PORT, USER = sys.argv[1], int(sys.argv[2]), sys.argv[3]
IDENT = {"user": USER, "ident": uuid4().hex, 'version': VERSION}
sock, conn, header, body = petrichor.connect(HOST, PORT, IDENT)
if petrichor.connect_allowed(header):
print "CONNECTED", body
else:
print "YOU WERE DENIED", body
sys.exit(1)
while header != None:
msg = raw_input("> ")
header, reply = petrichor.request(conn, {}, msg)
print reply
sock.close()
Sample Server For The Lazy
==========================
Here's the echo server, again with things like identifying connections
and users, which most echo servers wouldn't have:
import SocketServer
import petrichor
from uuid import uuid4
import sys
IDENT = uuid4().hex
VERSION = "0.1"
class PetrichorServer(SocketServer.StreamRequestHandler):
def handle(self):
ident, body = petrichor.accept(self.rfile)
# at this point you'd some kind of auth, we just fake it
reply = {"ident": IDENT, 'version': VERSION}
petrichor.peer_allowed(self.wfile, reply, "WELCOME")
header = {}
while header != None:
# just keep looping until you get a None which is returned on close
header, body = petrichor.recv(self.rfile)
print "RECEIVED", ident['user'], ident['ident'], header, body
if header != None:
petrichor.send(self.wfile, {}, body)
print "ALL DONE"
if __name__ == "__main__":
HOST, PORT = sys.argv[1], int(sys.argv[2])
server = SocketServer.TCPServer((HOST, PORT), PetrichorServer)
print "RUNNING", HOST, PORT
server.serve_forever()