Pattern: Serial
The first network architecture pattern we’ll look at is a Serial model of processing requests. We’ll proceed from the perspective of our FTP server.
Explanation
With a serial architecture all client connections are handled serially. Since there is no concurrency, multiple clients are never served simultaneously.
The flow of this architecture is straightforward:
- Client connects.
- Client/server exchange requests and responses.
- Client disconnects.
- Back to step #1.
Implementation
require 'socket'
require_relative '../command_handler'
module FTP
CRLF = "\r\n"
class Serial
def initialize(port = 21)
@control_socket = TCPServer.new(port)
trap(:INT) { exit }
end
def gets
@client.gets(CRLF)
end
def respond(message)
@client.write(message)
@client.write(CRLF)
end
def run
loop do
@client = @control_socket.accept
respond "220 OHAI"
handler = CommandHandler.new(self)
loop do
request = gets
if request
respond handler.handle(request)
else
@client.close
break
end
end
end
end
end
end
server = FTP::Serial.new(4481)
server.run
Notice that this class is only responsible for networking and concurrency; it hands off the protocol handling to the CommandHandler
methods. It’s a pattern you’ll keep seeing. Let’s take it from the top.
class Serial
def initialize(port = 21)
@control_socket = TCPServer.new(port)
trap(:INT) { exit }
end
def gets
@client.gets(CRLF)
end
def respond(message)
@client.write(message)
@client.write(CRLF)
end
These three methods are the boilerplate of this particular implementation. The initialize
method opens a socket that will, eventually, accept client connections.
The gets
method delegates gets
to the current client connection. Notice that it passes an explicit delimiter in order to stay portable across platforms with different defaults delimiters.
The respond
method writes out a formatted FTP response. The message
is a combination of an Integer response code and a String message. The client knows the response is complete when it receives the combination of the carriage return, \r
, and line feed, \n
, characters.
def run
loop do
@client = @control_socket.accept
respond "220 OHAI"
handler = CommandHandler.new(self)
This is the main run loop for this server. As you can see, all of the logic happens inside a main outer loop.
The only call to accept
inside this loop is the one you see at the top here. It accepts a connection from the @control_socket
initialized in initialize
. The 220 response is a protocol implementation detail. FTP requires us to say ‘hi’ after accepting a new client connection.
The last bit here is the initialization of a CommandHandler
for this connection. This class encapsulates the current state (current working directory) of the server on a per-connection basis. We’ll feed the incoming requests to the handler
object and get back the proper responses.
This bit of code is the concurrency blocker in this pattern. Because the server does not continue to accept connections while it’s processing this one, there can be no concurrency. This difference will become more apparent as we look at how other patterns handle this.
loop do
request = gets
if request
respond handler.handle(request)
else
@client.close
break
end
end
This rounds out the serial implementation of our FTP server.
It enters an inner loop where it gets
requests from the client socket passing in the explicit separator. It then passes those requests to the handler
which crafts the proper response for the client.
Given that this is a fully functioning FTP server (albeit, it only supports a subset of FTP), we can actually run the server and hook it up with a standard FTP client to see it in action. If you stick the full implementation in a file and run it like so:
$ ruby code/ftp/arch/serial.rb
Then run a standard ftp client:
$ ftp -a -v 127.0.0.1 4481
cd /var/log
pwd
get kernel.log
Considerations
It’s hard to nail down the pros and cons for each pattern because it depends entirely on your needs. I’ll do my best to explain where each pattern excels and what tradeoffs it makes.
The greatest advantage that a serial architecture offers is simplicity. There’s no locks, no shared state, no way to confuse one connection with another. This also goes for resource usage: one instance handling one connection won’t consume as many resources as many instances or many connections.
The obvious disadvantage is that there’s no concurrency. Pending connections aren’t processed even when the current connection is idle. Similarly, if a connection is using a slow link or pausing between sending requests the server remains blocked until that connection is closed.
This serial implementation is really just a baseline for the more interesting patterns that follow.