Network Architecture Patterns
Where the previous chapters covered the basics and the ‘need-to-knows’, this set of chapters turns to best practices and real-world examples. I consider the book up until this point to be like reference documentation: you can refer back to it and remind yourself how to use certain features or solve certain problems, but only if you know what you’re looking for.
If you were tasked with building an FTP server in Ruby, just knowing the first half of this book would certainly help, but it wouldn’t lead you to creating great software.
Though you know the building blocks you haven’t yet seen common ways to structure networked applications. How should concurrency be handled? How should errors be handled? What’s the best way to handle slow clients? How can you make most efficient use of resources?
These are the kinds of questions that this section of the book aims to answer. We’ll begin by looking at 6 network architectures patterns and apply these to an example project.
The Muse
Rather than just use diagrams and talk in the abstract, I want to really have an example project that we can implement and re-implement using different patterns. This should really drive home the differences between the patterns.
For this we’ll be writing a server that speaks a subset of FTP. Why a subset? Because I want the focus of this section to be on the architecture pattern, not the protocol implementation. Why FTP? Because then we can test it without having to write our own clients. Lots of FTP clients already exist.
For the unfamiliar, FTP is the File Transfer Protocol. It defines a text-based protocol, typically spoken over TCP, for transferring files between two computers.
As you’ll see, it feels a bit like browsing a filesystem. FTP makes use of simultaneous TCP sockets. One ‘control’ socket is used for sending FTP commands and their arguments between server and client. Each time that a file transfer is to be made, a new TCP socket is used. It’s a nice hack that allows for FTP commands to continue to be processed on the control socket while a transfer is in progress.
Here’s the protocol implementation of our FTP server. It provides a CommandHandler
class that encapsulates the handling of individual commands on a per-connection basis. This is important. Individual connections on the same server may have different working directories, and this class honours that.
module FTP
class CommandHandler
CRLF = "\r\n"
attr_reader :connection
def initialize(connection)
@connection = connection
end
def pwd
@pwd || Dir.pwd
end
def handle(data)
cmd = data[0..3].strip.upcase
options = data[4..-1].strip
case cmd
when 'USER'
# Accept any username anonymously
"230 Logged in anonymously"
when 'SYST'
# what's your name?
"215 UNIX Working With FTP"
when 'CWD'
if File.directory?(options)
@pwd = options
"250 directory changed to #{pwd}"
else
"550 directory not found"
end
when 'PWD'
"257 \"#{pwd}\" is the current directory"
when 'PORT'
parts = options.split(',')
ip_address = parts[0..3].join('.')
port = Integer(parts[4]) * 256 + Integer(parts[5])
@data_socket = TCPSocket.new(ip_address, port)
"200 Active connection established (#{port})"
when 'RETR'
file = File.open(File.join(pwd, options), 'r')
connection.respond "125 Data transfer starting #{file.size} bytes"
bytes = IO.copy_stream(file, @data_socket)
@data_socket.close
"226 Closing data connection, sent #{bytes} bytes"
when 'LIST'
connection.respond "125 Opening data connection for file list"
result = Dir.entries(pwd).join(CRLF)
@data_socket.write(result)
@data_socket.close
"226 Closing data connection, sent #{result.size} bytes"
when 'QUIT'
"221 Ciao"
else
"502 Don't know how to respond to #{cmd}"
end
end
end
end
This protocol implementation doesn’t say much about networking or concurrency; that’s the part we get to play with in the following chapters.