Surely you won't have enough traffic to justify async

I find Gopher really cool. I think it’s a really nice way to organize information into trees and hierarchies, and as we all know programmers can’t resist trees. So recently I took an interest in Gopher and started writing my own server.

Gopher, like HTTP, is a network protocol for retrieving information over the internet. One crucial difference is, it hasn’t been commercialized by adtech companies. This is probably because it doesn’t provide many opportunities for tracking, and it doesn’t have a significantly large user base.

But recently it’s been gaining traction; so we should provide a decent landscape for new gophers, full of oxidised servers. Since I started using Gopher more often, it’s beneficial for me if there’s more content out there. So I’m writing this blog post to walk you through how to write your own server. We’ll be doing this in Rust.

Before we jump into the details of the protocol, let’s set up a server that responds with “Hello world”. This will provide a skeleton that we can fill with Gopher-related code later.

Handling connections

fn handle_client ( stream : TcpStream ) -> io :: Result < () > { write! ( stream , "Hello world!" ) ? ; Ok (()) } fn main () -> io :: Result < () > { let listener = TcpListener :: bind ( format! ( "0.0.0.0:70" ) ? ; for stream in listener .incoming () { thread :: spawn ( move || handle_client ( stream ? )); } Ok (()) }

In this skeleton, pretty much all of our code is going to be in the handle_client function. If we look at the RFC for Gopher; we can see that after establishing a connection, the client sends the selector for the resource they are interested in. Like /ProgrammingLanguages/Python. Let’s read one line from the socket and look at which selector they want.

Gopher protocol

let mut line = String :: new (); BufReader :: new ( stream .try_clone () ? ) .read_line ( & mut line ) ? ; let line = line .trim ();

At this point, a traditional server would check the filesystem for the selector and a fancy web framework would go through the registered routes and possibly check some regular expressions. But for our toy server, a simple match statement will be more than enough.

let mut menu = GopherMenu :: with_write ( & stream ); match line { "/" | "" => { menu .info ( "Amazing home page of amazingness" ) ? ; } x => { menu .info ( "Page not found" ) ? ; } } menu .end () ? ;

In the code above, GopherMenu comes from a crate called gophermap. It’s a crate that can parse and generate Gopher menus.

For relative links, we need to know the server address. Let’s put that in a constant and write a small helper.

const HOST : & str = "gkbrk.com" ; let menu_link = | text : & str , selector : & str | { menu .write_entry ( ItemType :: Directory , text , selector , HOST , 70 ) }; match line { "/" | "" => { menu .info ( "Hi!" ) ? ; menu .info ( "Try going to page 1" ) ? ; menu_link ( "Page One" , "/page1" ) ? ; menu_link ( "Go to unknown link" , "/test" ) ? ; } "/page1" => { menu .info ( "Yay! You found the secret page" ) ? ; menu_link ( "Home page" , "/" ) ? ; } x => { menu .info ( & format! ( "Unknown link: {}" , x )) ? ; menu_link ( "Home page" , "/" ) ? ; } }; menu .end () ? ;

Now we can link between our pages and start building proper pages. Hopefully this was a good start. If anyone reading this sets up their own Gopherspace, please let me know by leaving a comment or sending me an email.