Our First Client/Server

Whew.

We’ve now looked at establishing connections and a whole lot of information about exchanging data. Up until now we’ve mostly been working with very small, self-contained bits of example code. It’s time to put everything we’ve seen together into a network server and client.

The Server

For our server we’re going to write the next NoSQL solution that no one has heard of. It’s just going to be a network layer on top of a Ruby hash. It’s aptly named CloudHash.

Here’s the full implementation of a simple CloudHash server:

require 'socket'

module CloudHash
  class Server
    def initialize(port)
      # Create the underlying server socket.
      @server = TCPServer.new(port)
      puts "Listening on port #{@server.local_address.ip_port}"
      @storage = {}
    end

    def start
      # The familiar accept loop.
      Socket.accept_loop(@server) do |connection|
        handle(connection)
        connection.close
      end
    end

    def handle(connection)
      # Read from the connection until EOF.
      request = connection.read

      # Write back the result of the hash operation.
      connection.write process(request)
    end

    # Supported commands:
    # SET key value
    # GET key
    def process(request)
      command, key, value = request.split

      case command.upcase
      when 'GET'
        @storage[key]

      when 'SET'
        @storage[key] = value
      end
    end
  end
end

server = CloudHash::Server.new(4481)
server.start

The Client

And here’s a simple client:

require 'socket'

module CloudHash
  class Client
    class << self
      attr_accessor :host, :port
    end

    def self.get(key)
      request "GET #{key}"
    end

    def self.set(key, value)
      request "SET #{key} #{value}"
    end

    def self.request(string)
      # Create a new connection for each operation.
      @client = TCPSocket.new(host, port)
      @client.write(string)

      # Send EOF after writing the request.
      @client.close_write

      # Read until EOF to get the response.
      @client.read
    end
  end
end

CloudHash::Client.host = 'localhost'
CloudHash::Client.port = 4481

puts CloudHash::Client.set 'prez', 'obama'
puts CloudHash::Client.get 'prez'
puts CloudHash::Client.get 'vp'

Put It All Together

Let’s stitch it all together!

Boot the server:

$ ruby code/cloud_hash/server.rb

Remember that the data structure is a Hash. Running the client will run the following operations:

$ tail -4 code/cloud_hash/client.rb
puts CloudHash::Client.set 'prez', 'obama'
puts CloudHash::Client.get 'prez'
puts CloudHash::Client.get 'vp'

$ ruby code/cloud_hash/client.rb

Thoughts

So what have we done here? We’ve wrapped a Ruby hash with a network API, but not even the whole Hash API, just the getter/setter. A good chunk of the code is boilerplate networking stuff, so it should be easy to see how you could extend this example to expose more of the Hash API.

I commented the code so you can get an idea of why things are done the way they are, I made sure to stick to concepts that we’ve already seen, such as establishing connections, EOF, etc.

But overall, CloudHash is kind of a kludge. In the last few chapters we’ve gone over the basics of establishing connections and exchanging data. Both of those things were applied here. What’s missing from this example is best practices about architecture patterns, design, and some advanced features we haven’t seen yet.

For example, notice that the client has to initiate a new connection for each request it sends? If you wanted to send a bunch of requests all in a row,, each would require its own connection. The current server design requires this. It processes one command from the client socket and then closes it.

There’s no reason why this needs to be the case. Establishing connections incurs overhead and it’s certainly possible for CloudHash to handle multiple requests on the same connection.

This can be remedied in a few different ways. The client/server could communicate using a simple message protocol that doesn’t require sending EOF to delimit messages. This change would allow for multiple requests on a single connection but the server will still process each client connection in sequence. If one client is sending a lot of requests or leaving their connection open for a long time, then other clients will not be able to interact with the server.

We can resolve this by building some form of concurrency into the server. The rest of the book builds on the basic knowledge you have thus far and focuses on helping you write efficient, understandable, and sane network programs. CloudHash, as it stands, does not provide a good example of how socket programming should be done.