by Sevki
19 Apr 2020
[pdf and ps]

It seems that it is a right of passage for anyone that touches Plan9 to write a9P library. In addition to being damn good fun, you'll also learn a lot about the 9P protocol.

9P, is a RPC protocol for manipulating a filesystem. It has a set of RPC calls defined that are mapped to Plan9 syscall like open, create as well as some calls that are rpc specific for negotiating version of the protocol.

If you want to learn more about 9P or want to implement your own lib, go and read Writing a 9P server from scratch.


There are a lot of 9P implementations in Go, none of them really fit the use case I had so I'll just go trough them and try to explain where they fell short and where they excelled.

As mentioned before there is nothing inherently wrong with using bytes to represent 9P messages, however doing so ties the message to the specific implementation of 9P which, more often than not, will be 9P2000. If you want a server that supports multiple versions of 9P and can also marshal and unmarshal messages to different formats, keeping them as []byte slices isn't the best way to go.

For instance Tversion message is described as;

size[4] Tversion tag[2] msize[4] version[s]

On a block of memory a version message looks like

structs struct1 19 0 0 0 100 85 170 0 32 0 0 6 0 57 80 50 48 48 48 size[4] (19) size[4] (19) size[4] (19)->struct1:sz Tversion (100) Tversion (100) Tversion (100)->struct1:tversion tag[2] (43605) tag[2] (43605) tag[2] (43605)->struct1:tag msize[4] (8192) msize[4] (8192) msize[4] (8192)->struct1:msize version[s] (9P2000) version[s] (9P2000) version[s] (9P2000)->struct1:version version-size[2] (6) version-size[2] (6) version-size[2] (6)->struct1:versionsize

A go struct wtih the relevant information would look like

type VersionReq struct {
	MaxSize  uint32
	Version  string

Tversion can be inferred from the struct type and size can (and should) be calculated when marshaling the message. tag doesn't need to be stored anywhere other than the response messages tag field.

Therefore 9P is the interface and 9P2000, 9P2000.u and 9P2000.L are specific implementations of that interface. The major driving force behind this implementation is being able to use different protocols.

http go brrrrrrrr

What about 9P over HTTP?

Hear me out! If we were just connecting two sockets with a dumb pipe 9P2000 would be an efficient way of encoding messages, unfourtunately for 9P2000 our pipes aren't dumb at all. The current HTTP/3 draft spans layer 7 (HTTP), layer 6 (TLS), layer 5 (HTTP streams), layer 4 (UDP) therefore just comparing the 9P to HTTP it self wouldn't give us a wholistic picture of the trade-offs. For instance when early adaoptors started implementing quic, they've realized even though the packet sizes are smaller and the number of round trips required are fewer, HTTP/2 over TCP out performs HTTP/3 over UDP. This is due to the fact that TCP implementations both in hardware and software is optimized to squeeze the every little bit of perf.

The ammount of work that has gone in to making HTTP as fast as possible is simply insurmountable. At every single level of OSI model, there are armies of people working on making it faster, wheter it be faster crypto (ecdsa vs rsa), message off-loading, hpack or strams HTTP is coming after your benchmarks.

In addition to all the work that is being done on the performance side of things, HTTP is well understood. This means that while an arbirtrary application might be blocked on your network, it's very unlikely HTTP will be. It also means that pretty much any HTTP proxy will be able to forward your requests to it's relevan't destination without actually reading the entire message.

The performance benefits, dynamic and static header tables, UDP, TCP improvements make HTTP/2 and very soon HTTP/3 very realistic transport mechanisms.

In part II I'll explore how to make 9P work over HTTP.