Client Lifecycle

I mentioned that there are two critical roles that make up a network connection. The server takes the listening role, listening for and processing incoming connections. The client, on the other hand, takes the role of initiating those connections with the server. In other words, it knows the location of a particular server and creates an outbound connection to it.

As I’m sure is obvious, no server is complete without a client.

The client lifecycle is a bit shorter than that of the server. It looks something like this:

  1. create
  2. bind
  3. connect
  4. close

Step #1 is the same for both clients and servers, so we’ll begin by looking at bind from the perspective of clients.

Clients Bind

Client sockets begin life in the same way as server sockets, with bind. In the server section we called bind with a specific address and port. While it’s rare for a server to omit its call to #bind, its rare for a client to make a call to bind. If the client socket (or the server socket, for that matter) omit its call to bind, it will be assigned a random port from the ephemeral range.

Why not call bind?

Clients don’t call bind because they don’t need to be accessible from a known port number. The reason that servers bind to a specific port number is that clients expect a server to be available at a certain port number.

Take FTP as an example. The well-known port for FTP is 21. Hence FTP servers should bind to that port so that clients know where to find them. But the client is able to connect from any port number. The client port number does not affect the server.

Clients don’t call bind because no one needs to know what their port number is.

There’s no code snippet for this section because the recommendation is: don’t do it!

Clients Connect

What really separates a client from a server is the call to connect. This call initiates a connection to a remote socket.

require 'socket'

socket = Socket.new(:INET, :STREAM)

# Initiate a connection to google.com on port 80.
remote_addr = Socket.pack_sockaddr_in(80, 'google.com')
socket.connect(remote_addr)

Again, since we’re using the low-level primitives here we’re needing to pack the address object into its C struct representation.

This code snippet will initiate a TCP connection from a local port in the ephemeral range to a listening socket on port 80 of google.com. Notice that we didn’t use a call to bind.

Connect Gone Awry

In the lifecycle of a client it’s quite possible for a client socket to connect to a server before said server is ready to accept connections. It’s equally possible to connect to a non-existent server. In fact, both of these situations produce the same outcome. Since TCP is optimistic, it waits as long as it can for a response from a remote host.

So, let’s try connecting to an endpoint that’s not available:

require 'socket'

socket = Socket.new(:INET, :STREAM)

# Attempt to connect to google.com on the known gopher port.
remote_addr = Socket.pack_sockaddr_in(70, 'google.com')
socket.connect(remote_addr)

If you run this bit of code it can take a long time to return from the connect call. There is a long timeout by default on a connect.

This makes sense for clients where bandwidth is an issue and it may actually take a long time to establish a connection. Even for bandwidth-rich clients the default behaviour is hopeful that the remote address will be able to accept our connection soon.

Nevertheless, if you wait it out, you’ll eventually see an Errno::ETIMEDOUT exception raised. This is the generic timeout exception when working with sockets and indicates that the requested operation timed out. If you’re interested in tuning your socket timeouts there’s an entire Timeouts chapter later in the book.

This same behaviour is observed when a client connects to a server that has called bind and listen but has not yet called accept. The only case in which connect returns successfully is if the remote server accepts the connect.

Ruby Wrappers

Much like the code for creating server sockets, the code for creating client sockets is verbose and low-level. As expected, Ruby has wrappers to make these easier to work with.

Client Construction

Before I show you the nice, rosy Ruby-friendly code I’m going to show the low-level verbose code so we have something to compare against:

require 'socket'

socket = Socket.new(:INET, :STREAM)

# Initiate a connection to google.com on port 80.
remote_addr = Socket.pack_sockaddr_in(80, 'google.com')
socket.connect(remote_addr)

Behold the syntactic sugar:

require 'socket'

socket = TCPSocket.new('google.com', 80)

That feels much better. Three lines, two constructors, and lots of context have been reduced into a single constructor.

There’s a similar client construction method using Socket.tcp that can take a block form:

require 'socket'

Socket.tcp('google.com', 80) do |connection|
  connection.write "GET / HTTP/1.1\r\n"
  connection.close
end

# Omitting the block argument behaves the same
# as TCPSocket.new().
client = Socket.tcp('google.com', 80)

System Calls From This Chapter

  • Socket#bind -> bind(2)
  • Socket#connect -> connect(2)