• No results found

The Muse The Muse

In document Working With TCP Sockets (Page 124-134)

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 defines some common methods for writing FTP formatted responses and building the control socket. It also provides a

CommandHandler class that encapsulates the handling of individual commands on a per-connection basis. This is important. Individual per-connections on the same server may have different working directories, and this class honours that.

# ./code/ftp/common.rb

when 'SYST'

"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"

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 end

Did you see the respond method? It writes the response out on the connection. FTP uses a

\r\n to signify message boundaries, just like HTTP.

This protocol implementation doesn't say much about networking or concurrency;

that's the part we get to play with in the following chapters.

Chapter 19

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 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 Implementation

# ./code/ftp/arch/serial.rb

Notice that this class is only responsible for networking and concurrency; it hands off the protocol handling to the Common module methods. It's a pattern you'll keep seeing.

Let's take it from the top.

# ./code/ftp/arch/serial.rb

This is the entry point for the 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 Common. 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.

# ./code/ftp/arch/serial.rb

loop do

request = @client.gets(CRLF) if request

respond handler.handle(request) else

@client.close

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:

$ ruby code/ftp/arch/serial.rb

$ ftp -a -v 127.0.0.1 4481 cd /var/log

pwd

get kernel.log

Considerations 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.

Chapter 20

In document Working With TCP Sockets (Page 124-134)

Related documents