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:

  1. Client connects.
  2. Client/server exchange requests and responses.
  3. Client disconnects.
  4. 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.