Network Programming - THE RUBY WAY, Third Edition (2015)

THE RUBY WAY, Third Edition (2015)

Chapter 18. Network Programming

Never underestimate the bandwidth of a station wagon full of tapes hurtling down the highway.

—Andrew S. Tanenbaum

When a marketing type says “networking,” he probably means he wants to give you his business card. But when a programmer says it, he’s talking about electronic communication between physically separated machines—whether across the room, across the city, or across the world.

In the programmer’s world, networking usually implies TCP/IP, the native tongue in which millions of machines whisper back and forth across the Internet. I’ll say a few words about this before diving into some concrete examples.

Network communication is conceptualized at different levels (or layers) of abstraction. The lowest level is the data link layer, or actual hardware-level communication, which we won’t discuss here. Immediately above this is the network layer, which is concerned with the actual moving around of packets; this is the realm of IP (Internet Protocol). At a still higher level of abstraction is the transport layer, where we find TCP (Transmission Control Protocol) and UDP (User Datagram Protocol). At the level above this, we find the application layer; at this point, we finally enter the world of telnet, FTP, email protocols, and much more.

It’s possible to communicate directly in IP, but normally you wouldn’t do such a thing. Most of the time, we are concerned with TCP or UDP.

TCP provides communication between two hosts with consistent ordering and automatic retries; it is concerned with formatting packet data, acknowledgment of receipt, and so on. Any application using it can know data will be read in the same order it was sent.

UDP is much simpler, merely sending packets (datagrams) to the remote host, like binary postcards. There is no guarantee that these will be received, let alone received in order. It is useful for situations such as video chat and gaming, where only the latest data matters, and missed packets should not be re-sent.

Ruby supports low-level networking (chiefly in TCP and UDP) as well as coding at higher levels. These higher levels include telnet, FTP, SMTP, and so on. Figure 18.1 is a class hierarchy showing the highlights of Ruby’s networking support.

Image

Figure 18.1 Partial inheritance hierarchy for networking support in Ruby.

Note that the bulk of these classes derive from the IO class. This means that we can use the methods of IO that are so familiar to us. In addition to these classes, the standard library includes Net::IMAP, Net::POPMail, Net::SMTP, Net::Telnet, and Net::FTP, all inheriting directly from Object.

Documenting all the features of all these classes would far exceed the space requirements of this book. I’ll only present a task-oriented approach to all of this and offer a little explanation. For a comprehensive list of available methods, consult a reference such as rdoc.info.

A few significant areas are not covered here at all, so we’ll mention these up front. The Net::Telnet class is mentioned only in connection with NTP servers in Section 18.2.2, “Contacting an Official Timeserver”; this class is not just for implementing your own telnet client but is potentially useful for automating anything that has a telnet interface.

The Net::FTP library is also not covered here. In general, FTP is easy to automate in its everyday form, so there is less motivation to use this class than there might be for some others.

The Net::Protocol class, which is the parent of HTTP and POP3, is not covered in this chapter. Although it would probably prove useful in the development of customized networking protocols, that is beyond the scope of this book.

That ends our broad overview. Let’s look at low-level networking in more detail.

18.1 Network Servers

A server spends its lifetime waiting for messages and answering them. It may have to do some serious processing to construct those answers, such as accessing a database, but from a networking point of view, it simply receives requests and sends responses.

Having said that, there is still more than one way to accomplish this. A server may respond to only one request at a time, or it may thread its responses. The former is easier to code, but the latter is better if many clients are trying to connect simultaneously.

It’s also conceivable that a server may be used to facilitate communication in some way between the clients. The classic examples are chat servers, game servers, and peer-to-peer file sharing.

18.1.1 A Simple Server: Time of Day

Let’s look at the simplest server we can think of, which may require a little suspension of disbelief. Let’s suppose we have a server whose clock is so accurate that we use it as a standard. There are such servers, of course, but they don’t communicate with the simple protocol we show here. (Actually, you can refer to Section 18.2.2, “Contacting an Official Timeserver,” for an example of contacting such a server via the Telnet interface.)

In this low-level example, a single-threaded server handles requests inline. When the client makes a request of us, we return a string with the time of day. Here’s the server code:

require "socket"

server = UDPSocket.open # Using UDP here...
server.bind nil, 12321

loop do
text, sender = server.recvfrom(1)
server.send("#{Time.now}\n", 0, sender[3], sender[1])
end

And here is the client code:

require "socket"
require "timeout"

socket = UDPSocket.new
socket.connect("localhost", 12321)

socket.send("", 0)
timeout(10) do
time = socket.gets
puts "The time is #{time}"
end

Note that the client makes its request simply by sending a null packet. Because network requests are unreliable, we time out after a reasonable length of time.

The following is a similar server implemented with TCP. It listens on port 12321 and can actually be used by telnetting into that port (or by using the client code we show afterward):

require "socket"


server = TCPServer.new(12321)


loop do
session = server.accept
session.puts Time.new
session.close
end

Note the straightforward use of the TCPServer class. The call to accept is a blocking one, pausing execution until a client connects. Once a client has connected, the connection socket is returned and can be used in the server code.

Here is the TCP version of the client code:

require "socket"


session = TCPSocket.new("localhost", 12321)
puts "The time is #{session.gets.chomp}"
session.close

18.1.2 Implementing a Threaded Server

If this server ever gets more than one request at the same time, one request will have to wait (or queue) until the first request is finished. To handle multiple requests at the same time, handle each request with a separate thread.

Here is a reimplementation of the time-of-day server in the previous example. It uses TCP and threads all the client requests:

require "socket"

server = TCPServer.new(12321)

loop do
Thread.new(server.accept) do |session|
session.puts Time.new
session.close
end
end

Because it uses threads and spawns a new one with every client request, greater parallelism is achieved. No join is done because the loop is essentially infinite, running until the server is interrupted manually.

The client code is, of course, unchanged. From the point of view of the client, the server’s behavior is unchanged (except that it may appear faster).

18.1.3 Case Study: A Peer-to-Peer Chess Server

It isn’t always the server that we’re ultimately concerned about communicating with. Sometimes the server is more of a directory service to put clients in touch with each other. One example is a peer-to-peer file-sharing service such as those so popular 10–15 years ago; other examples are chat servers or any number of game servers.

Let’s create a skeletal implementation of a chess server. Here, we don’t mean a server that will play chess with a client, but simply one that will point clients to each other so that they can then play without the server’s involvement.

I’ll warn you that for the sake of simplicity, the code really knows nothing about chess. All of the game logic is simulated (stubbed out) so that we can focus on the networking issues.

First, let’s use TCP for the initial communication between each client and the server. We could use UDP, but then we would have to handle retries and out-of-order data by ourselves.

We’ll let each client provide two pieces of information: his own name (such as a username) and the name of the desired opponent. We’ll introduce the notation user:hostname to fully identify the opponent; we use a colon instead of the more intuitive @ so that it won’t resemble an email address, which it isn’t.

When a client contacts the server, the server stores the client’s information in a list. When both clients have contacted the server, a message is sent back to each of them; each client is given enough information to contact his opponent.

There’s also the small issue of white and black. Somehow the roles have to be assigned in such a way that both players agree on what color they are playing. For simplicity, we’re letting the server assign this. The first client contacting the server will get to play white (and thus move first); the other player will play the black pieces.

Don’t get confused here. The initial clients talk to each other so that effectively one of them is really a server by this point. This is a semantic distinction that I won’t bother with.

Because the clients will be talking to each other in alternation and there is more than just a single brief exchange, we’ll use TCP for their communication. This means that the client that is “really” a server will instantiate a TCPServer, and the other will instantiate a TCPSocket at the other end. We’re assuming a well-known port for peer-to-peer communication as we did with the initial client-server handshaking. (The two ports are different, of course.)

What we’re really describing here is a simple application-level protocol. It could certainly be made more sophisticated.

Let’s look first at the server (see Listing 18.1). For the convenience of running it at a command line, we start a thread that terminates the server when a carriage return is pressed. The main server logic is threaded; we can handle multiple clients connecting at once. For safety’s sake, we use a mutex to protect access to the user data; without it, two clients connecting at once would mean multiple threads might try to add users to the list at the same time.

Listing 18.1 The Chess Server


require "thread"
require "socket"

PORT = 12000

# Exit if user presses Enter
waiter = Thread.new do
puts "Press Enter to exit the server."
gets
exit
end

@mutex = Mutex.new
@list = {}

def match?(p1, p2)
return false unless @list[p1] && @list[p2]
@list[p1][0] == p2 && @list[p2][0] == p1
end

def handle_client(sess, msg, addr, port, ipname)
cmd, player1, player2 = msg.split

# Note: We get user:hostname on the command line,
# but we store it in the form user:address
player1 << ":#{addr}" # Append user's IP addr

user2, host2 = player2.split(":")
host2 = ipname if host2 == nil
player2 = user2 + ":" + IPSocket.getaddress(host2)

if cmd != "login"
puts "Protocol error: client msg was #{msg}"
end

@mutex.synchronize do
@list[player1] = [player2, addr, port, ipname, sess]
end

if match?(player1, player2)
notify_clients(player1, player2)
end
end

def notify_clients(player1, player2)
# Note these names are "backwards" now: player2
# logged in first, if we got here.
p1, p2 = @mutex.synchronize do
[@list.delete(player1), @list.delete(player2)]
end

p1name = player1.split(":")[0]
p2name = player2.split(":")[0]

# Player ID = name:ip:color
# Color: 0=white, 1=black
p1id = "#{p1name}:#{p1[3]}:1"
p2id = "#{p2name}:#{p2[3]}:0"

sess2 = p2[4]
sess2.puts p1id
sess2.close

sleep 0.2 # let the player-server start up
sess1 = p1[4]
sess1.puts p2id
sess1.close
end

# Abort in this short example, skipping threaded error-handling
Thread.abort_on_exception = true

server = TCPServer.new("0.0.0.0", PORT)
loop do
Thread.new(server.accept) do |sess|
text = sess.gets
print "Received: #{text}" # So we know server gets it
domain, port, ipname, ipaddr = sess.peeraddr
handle_client sess, text, ipaddr, port, ipname
end
end


The handle_client method stores information for the client. If the corresponding client is already stored, each client is sent a message telling the whereabouts of the other client. As we’ve defined this simple problem, the server’s responsibility ends at this point.

The client code (see Listing 18.2) is naturally written so that there is only a single program; the first invocation will become the TCP server, and the second will become the TCP client. To be fair, we should point out that our choice to make the server white and the client black is arbitrary. There’s no particular reason we couldn’t implement the application so that the color issue was independent of such considerations.

Listing 18.2 The Chess Client


require "socket"
require "timeout"

ChessServer = "10.0.1.7" # Replace this IP address
ChessServerPort = 12000
PeerPort = 12001
White, Black = 0, 1

def draw_board(board)
puts <<-EOF
+----------------------------------+
| Stub! Drawing the board here... |
+----------------------------------+
EOF
end

def analyze_move(who, move, num, board)
# Stub - black always wins on 4th move
if who == Black and num == 4
move << " Checkmate!"
end
true # Stub again - always say it's legal.
end

def get_move(who, move, num, board)
ok = false
until ok do
print "\nYour move: "
move = STDIN.gets.chomp
ok = analyze_move(who, move, num, board)
puts "Illegal move" unless ok
end
move
end

def my_move(who, lastmove, num, board, opponent, sock)
move = get_move(who, lastmove, num, board)
sock.puts move
draw_board(board)

case move
when "resign"
puts "\nYou've resigned. #{opponent} wins."
true
when /Checkmate/
puts "\nYou have checkmated #{opponent}!"
true
else
false
end
end

def other_move(who, move, num, board, opponent, sock)
move = sock.gets.chomp
puts "\nOpponent: #{move}"
draw_board(board)

case move
when "resign"
puts "\n#{opponent} has resigned... you win!"
true
when /Checkmate/
puts "\n#{opponent} has checkmated you."
true
else
false
end
end

if ARGV[0]
myself = ARGV[0]
else
print "Your name? "
myself = STDIN.gets.chomp
end

if ARGV[1]
opponent_id = ARGV[1]
else
print "Your opponent? "
opponent_id = STDIN.gets.chomp
end

opponent = opponent_id.split(":")[0] # Remove hostname

# Contact the server

socket = TCPSocket.new(ChessServer, ChessServerPort)

socket.puts "login #{myself} #{opponent_id}"
socket.flush
response = socket.gets.chomp
name, ipname, color = response.split ":"
color = color.to_i

if color == Black # Other player's color
puts "\nConnecting..."

server = TCPServer.new(PeerPort)
session = server.accept

begin
timeout(30) do
str = session.gets.chomp
if str != "ready"
raise "Protocol error: ready-message was #{str}"
end
end
rescue Timeout::Error
raise "Did not get ready-message from opponent."
end

puts "Playing #{opponent}... you are white.\n"

who = White
move = nil
board = nil # Not really used in this dummy example
num = 0
draw_board(board) # Draw the board initially for white

loop do
num += 1
won = my_move(who, move, num, board, opponent, session)
break if won
lost = other_move(who, move, num, board, opponent, session)
break if lost
end
else # We're black
puts "\nConnecting..."

socket = TCPSocket.new(ipname, PeerPort)
socket.puts "ready"

puts "Playing #{opponent}... you are black.\n"

who = Black
move = nil
board = nil # Not really used in this dummy example
num = 0
draw_board(board) # Draw board initially

loop do
num += 1
lost = other_move(who, move, num, board, opponent, socket)
break if lost
won = my_move(who, move, num, board, opponent, socket)
break if won
end

socket.close
end


I’ve defined this little protocol so that the black client sends a “ready” message to the white client to let it know it’s prepared to begin the game. The white player then moves first. The move is sent to the black client so that it can draw its own board in sync with the other player’s board.

Again, there’s no real knowledge of chess built in to this application. There’s a stub in place to check the validity of each player’s move; this check is done on the local side in each case. However, this is only a stub that always says that the move is legal. At the same time, it does a bit of hocus-pocus; we want this simulated game to end after only a few moves, so we fix the game so that black always wins on the fourth move. This win is indicated by appending the string "Checkmate!" to the move; this prints on the opponent’s screen and also serves to terminate the loop.

Besides the “traditional” notation (for example, “P-K4”), there is also an “algebraic” notation preferred by most people. However, the code is stubbed so heavily that it doesn’t even know which notation we’re using.

Because it’s easy to do, we allow a player to resign at any time. This is simply a win for the opponent. The drawing of the board is also a stub. Those wanting to do so can easily design some bad ASCII art to output here.

The my_move method always refers to the local side. Likewise, other_move refers to the remote side. Listing 18.3 shows some sample output.

Listing 18.3 Sample Chess Client Execution


$ ruby chess.rb Hal $ ruby chess.rb Capablanca
Capablanca:deepthought.org Hal:deepdoodoo.org

Connecting... Connecting...
Playing Capablanca...you are white. Playing Hal... you are black.
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Your move: N-QB3 Opponent: N-QB3
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Opponent: P-K4 Your move: P-K4
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Your move: P-K4 Opponent: P-K4
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Opponent: B-QB4 Your move: B-QB4
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Your move: B-QB4 Opponent: B-QB4
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Opponent: Q-KR5 Your move: Q-KR5
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Your move: N-KB3 Opponent: N-KB3
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Opponent: QxP Checkmate! Your move: QxP
+---------------------------------+ +---------------------------------+
| Stub! Drawing the board here... | | Stub! Drawing the board here... |
+---------------------------------+ +---------------------------------+

Capablanca has checkmated you. You have checkmated Hal!


18.2 Network Clients

Sometimes the server is a well-known entity or is using a well-established protocol. In this case, we need simply to design a client that will talk to this server in the way it expects.

This can be done with TCP or UDP, as you saw in Section 18.1, “Network Servers.” However, it is common to use other higher-level protocols such as HTTP and SNMP. We’ll look at a few examples here.

18.2.1 Retrieving Truly Random Numbers from the Web

Anyone who attempts to generate random numbers by deterministic means is, of course, living in a state of sin.

—John von Neumann

The rand function in Kernel returns a random number, but there is a fundamental problem with it: It isn’t really random. If you are a mathematician, cryptographer, or other nitpicker, you will refer to this as a pseudorandom number generator because it uses algebraic methods to generate numbers in a deterministic fashion. These numbers “look” random to the casual observer, and may even have the correct statistical properties, but the sequences do repeat eventually, and we can even repeat a sequence purposely (or accidentally) by using the same seed.

However, processes in nature are considered to be truly random. That is why in state lotteries, winners of millions of dollars are picked based on the chaotic motions of little white balls. Other sources of randomness are radioactive emissions and atmospheric noise.

You can find sources of random numbers on the Web. One of these is www.random.org, which we use in this example.

The sample code in Listing 18.4 simulates the throwing of five ordinary (six-sided) dice. Of course, gaming fans could extend this to 10-sided or 20-sided dice, but the ASCII art would get tedious.

Listing 18.4 Casting Dice at Random


require 'net/http'

URL = "http://www.random.org/integers/"

def get_random_numbers(count=1, min=0, max=99)
uri = URI.parse(URL)
uri.query = URI.encode_www_form(
col: 1, base: 10, format: "plain", rnd: "new",
num: count, min: min, max: max
)
response = Net::HTTP.get_response(uri)
case response
when Net::HTTPOK
response.body.lines.map(&:to_i)
else
[]
end
end

DICE_LINES = [
"+-----+ +-----+ +-----+ +-----+ +-----+ +-----+ ",
"| | | * | |* | | * * | | * * | | * * | ",
"| * | | | | * | | | | * | | * * | ",
"| | | * | | *| | * * | | * * | | * * | ",
"+-----+ +-----+ +-----+ +-----+ +-----+ +-----+ "
]

DIE_WIDTH = DICE_LINES[0].length/6

def draw_dice(values)
DICE_LINES.each do |line|
for v in values
print line[(v-1)*DIE_WIDTH, DIE_WIDTH]
print " "
end
puts
end
end

draw_dice(get_random_numbers(5, 1, 6))


In this code, we’re using the Net::HTTP class to communicate directly with a web server. Think of it as a highly special-purpose web browser. We form the URL and try to connect; when we make a connection, we get a response; if the response indicates that all is well, we can parse the response body that we got back. Exceptions are assumed to be handled by the caller.

Let’s look at a variation on the same basic idea. What if we really wanted to use these random numbers in an application? Because the CGI at the server end allows us to specify how many numbers we want returned, it’s logical to buffer them. It’s a fact of life that a delay is usually involved when accessing a remote site. We want to fill a buffer so that we are not making frequent web accesses and incurring delays.

In Listing 18.5, we implement this variation. The buffer is filled by a separate thread, and it is shared among all the instances of the class. The buffer size and the “low water mark” (@slack) are both tunable; appropriate real-world values for them would be dependent on the reachability (ping time) of the server and on how often the application requested a random number from the buffer.

Listing 18.5 A Buffered Random Number Generator


require "net/http"
require "thread"

class TrueRandom

URL = "http://www.random.org/integers/"

def initialize(min = 0, max = 1, buffsize = 1000, slack = 300)
@buffer = SizedQueue.new(buffsize)
@min, @max, @slack = min, max, slack
@thread = Thread.new { fillbuffer }
end

def fillbuffer
count = @buffer.max - @buffer.size

uri = URI.parse(URL)
uri.query = URI.encode_www_form(
col: 1, base: 10, format: "plain", rnd: "new",
num: count, min: @min, max: @max
)

Net::HTTP.get(uri).lines.each do |line|
@buffer.push line.to_i
end
end

def rand
if @buffer.size < @slack && !@thread.alive?
@thread = Thread.new { fillbuffer }
end

@buffer.pop
end

end

t = TrueRandom.new(1, 6, 1000, 300)
count = Hash.new(0)

10000.times do |n|
count[t.rand] += 1
end

p count

# In one run:
# {4=>1692, 5=>1677, 1=>1678, 6=>1635, 2=>1626, 3=>1692}


18.2.2 Contacting an Official Timeserver

As we promised, here’s a bit of code to contact a Network Time Protocol (NTP) server on the Net, using TCPSocket to connect to a public time server and read the time:

require "socket"

resp = TCPSocket.new("time.nist.gov", 13).read
time = ts.split(" ")[2] + " UTC"
remote = Time.parse(time)

puts "Local : #{Time.now.utc.strftime("%H:%M:%S")}"
puts "Remote: #{remote.strftime("%H:%M:%S")}"

We establish a connection and read from the socket. The server response includes the Julian date, the date in YY-MM-DD format, the time in HH:MM:SS format, days left until a change to or from Daylight Saving Time, and some other informational flags. We read the entire response, parse only the time, and then print the time on the local machine for comparison.

Note that this example does not use the NTP protocol, which is a more complex format that contains additional data (such as the latency of responses with the time). Instead, we used the much simpler DAYTIME protocol. A full implementation of NTP is too long to reproduce here (although it is only about three pages of code). To see a full NTP implementation, read the source of the net-ntp gem online.

18.2.3 Interacting with a POP Server

The Post Office Protocol (POP) is commonly used by mail servers. Ruby’s POP3 class enables you to examine the headers and bodies of all messages waiting on a server and process them as you see fit. After processing, you can easily delete one or all of them.

The Net::POP3 class must be instantiated with the name or IP address of the server; the port number defaults to 110. No connection is established until the method start is invoked (with the appropriate username and password).

Invoking the method mails on this object will return an array of objects of class POPMail. (There is also an iterator each that will run through these one at a time.)

A POPMail object corresponds to a single email message. The header method will retrieve the message’s headers; the method all will retrieve the header and the body. (There are also other usages of all, as you’ll see shortly.)

A code fragment is worth a thousand words. Here’s a little example that will log on to the server and print the subject line for each email:

require "net/pop"

pop = Net::POP3.new("pop.fakedomain.org")
pop.start("gandalf", "mellon") # user, password
pop.mails.each do |msg|
puts msg.header.lines.grep /^Subject: /
end

The delete method will delete a message from the server. (Some servers require that finish be called to close the POP connection before such an operation becomes final.) Here is the world’s most trivial spam filter:

require "net/pop"

pop = Net::POP3.new("pop.fakedomain.org")
pop.start("gandalf", "mellon") # user, password
pop.mails.each do |msg|
if msg.all =~ /.*make money fast.*/
msg.delete
end
end
pop.finish

We’ll mention that start can be called with a block. By analogy with File.open, it opens the connection, executes the block, and closes the connection.

The all method can also be called with a block. This will simply iterate over the lines in the email message; it is equivalent to calling each_line on the string resulting from all:

# Print each line backwards... how useful!
msg.all { |line| print line.reverse }
# Same thing...
msg.all.each_line { |line| print line.reverse }

We can also pass an object into the all method. In this case, it will call the append operator (<<) repeatedly for each line in the string. Because this operator is defined differently for different objects, the behavior may be radically different, as shown here:

arr = [] # Empty array
str = "Mail: " # String
out = $stdout # IO object

msg.all(arr) # Build an array of lines
msg.all(str) # Concatenate onto str
msg.all(out) # Write to standard output

Finally, we’ll give you a way to return only the body of the message, ignoring all headers:

module Net
class POPMail
def body
# Skip header bytes
self.all[self.header.size..-1]
end
end
end

This doesn’t have all the properties that all has, but it could be extended. We’ll leave that to you.

For those who prefer IMAP to POP3, see Section 18.2.5, “Interacting with an IMAP Server.”

18.2.4 Sending Mail with SMTP

A child of five could understand this. Fetch me a child of five.

—Groucho Marx

The Simple Mail Transfer Protocol (SMTP) may seem like a misnomer. If it is “simple,” it is only by comparison with more complex protocols.

Of course, the smtp library shields the programmer from most of the details of the protocol. However, we have found that the design of this library is not entirely intuitive and perhaps overly complex (and we hope it will change in the future). In this section, we try to present a few examples to you in easily digested pieces.

The Net::SMTP class has two class methods, new and start. The new method takes two parameters—the name of the server (defaulting to localhost) and the port number (defaulting to the well-known port 25).

The start method takes these parameters:

server is the IP name of the SMTP server, defaulting to "localhost".

port is the port number, defaulting to 25.

domain is the domain of the mail sender, defaulting to ENV["HOSTNAME"].

account is the username; the default is nil.

password is the user password, defaulting to nil.

authtype is the authorization type, defaulting to :cram_md5.

Many or most of these parameters may be omitted under normal circumstances.

If start is called “normally” (without a block), it returns an object of class SMTP. If it is called with a block, that object is passed into the block as a convenience.

An SMTP object has an instance method called sendmail, which will typically be used to do the work of mailing a message. It takes three parameters:

source is a string or array (or anything with an each iterator returning one string at a time).

sender is a string that will appear in the “from” field of the email.

recipients is a string or an array of strings representing the addressee(s).

Here is an example of using the class methods to send an email:

require 'net/smtp'

msg = <<EOF
Subject: Many things
"The time has come," the Walrus said,
"To talk of many things —
Of shoes, and ships, and sealing wax,
Of cabbages and kings;
And why the sea is boiling hot,
And whether pigs have wings."
EOF

Net::SMTP.start("smtp-server.fake.com") do |smtp|
smtp.sendmail msg, 'walrus@fake1.com', 'alice@fake2.com'
end

Because the string Subject: was specified at the beginning of the string, Many things will appear as the subject line when the message is received.

There is also an instance method named start, which behaves much the same as the class method. Because new specifies the server, start doesn’t have to specify it. This parameter is omitted, and the others are the same as for the class method. This gives us a similar example using anSMTP object:

require 'net/smtp'

msg = <<EOF
Subject: Logic
"Contrariwise," continued Tweedledee,
"if it was so, it might be, and if it
were so, it would be; but as it isn't,
it ain't. That's logic."
EOF

smtp = Net::SMTP.new("smtp-server.fake.com")
smtp.start
smtp.sendmail msg, 'tweedledee@fake1.com', 'alice@fake2.com'

In case you are not confused yet, the instance method can also take a block:

require 'net/smtp'

msg = <<EOF
Subject: Moby-Dick
Call me Ishmael.
EOF

addressees = ['reader1@fake2.com', 'reader2@fake3.com']

smtp = Net::SMTP.new("smtp-server.fake.com")
smtp.start do |obj|
obj.sendmail msg, 'narrator@fake1.com', addressees
end

As the example shows, the object passed into the block (obj) certainly need not be named the same as the receiver (smtp). I’ll also take this opportunity to emphasize that the recipient can be an array of strings.

There is also an oddly named instance method called ready. This is much the same as sendmail, with some crucial differences. Only the sender and recipients are specified; the body of the message is constructed using an adapter—an object of classNet::NetPrivate::WriteAdapter—which has a write method as well as an append method. This adapter is passed into the block and can be written to in an arbitrary way:

require "net/smtp"

smtp = Net::SMTP.new("smtp-server.fake1.com")

smtp.start

smtp.ready("t.s.eliot@fake1.com", "reader@fake2.com") do |obj|
obj.write "Let us go then, you and I,\r\n"
obj.write "When the evening is spread out against the sky\r\n"
obj.write "Like a patient etherised upon a table...\r\n"
end

Note here that the carriage-return linefeed pairs are necessary (if we actually want line breaks in the message). Those who are familiar with the actual details of the protocol should note that the message is “finalized” (with “dot” and “QUIT”) without any action on our part.

We can append instead of calling write if we want:

smtp.ready("t.s.eliot@fake1.com", "reader@fake2.com") do |obj|
obj << "In the room the women come and go\r\n"
obj << "Talking of Michelangelo.\r\n"
end

Finally, we offer a minor improvement. We add a puts method that will tack on the newline for us:

class Net::NetPrivate::WriteAdapter
def puts(args)
args << "\r\n"
self.write(*args)
end
end

This new method enables us to write this way:

smtp.ready("t.s.eliot@fake1.com", "reader@fake2.com") do |obj|
obj.puts "We have lingered in the chambers of the sea"
obj.puts "By sea-girls wreathed with seaweed red and brown"
obj.puts "Till human voices wake us, and we drown."
end

If your needs are more specific than what we’ve detailed here, we suggest you do your own experimentation. And if you decide to write a new interface for SMTP, please feel free.

18.2.5 Interacting with an IMAP Server

The IMAP protocol is not the prettiest in the world, but it is superior to POP3 in many ways. Messages may be stored on the server indefinitely (individually marked as read or unread). Messages may be stored in hierarchical folders. These two facts alone are enough to establish IMAP as more powerful than POP3.

The standard library net/imap enables us to interact with an IMAP server. As you would expect, you connect to the server and then log in to an account with a username and password, as shown in the following code:

require 'net/imap'

host = "imap.hogwarts.edu"
user, pass = "lupin", "riddikulus"

imap = Net::IMAP.new(host)
begin
imap.login(user, pass)
# Or alternately:
# imap.authenticate("LOGIN", user, pass)
rescue Net::IMAP::NoResponseError
abort "Could not login as #{user}"
end

# Process as needed...
imap.logout # break the connection

After you have a connection, you can do an examine on a mailbox; the default mailbox in IMAP is called INBOX. The responses method retrieves information about the mailbox, returning a hash of arrays (with the interesting data in the last element of each array). The following code finds the total number of messages in the mailbox ("EXISTS") and the number of unread messages ("RECENT"):

imap.examine("INBOX")
total = imap.responses["EXISTS"].last # total messages
recent = imap.responses["RECENT"].last # unread messages
imap.close # close the mailbox

Note that examine gives you read-only access to the mailbox. If, for example, you want to delete messages or make other changes, you should use select instead.

IMAP mailboxes are hierarchical and look similar to UNIX pathnames. You can use the create, delete, and rename methods to manipulate mailboxes:

imap.create("lists")
imap.create("lists/ruby")
imap.create("lists/rails")
imap.create("lists/foobar")

# Oops, kill that last one:
imap.delete("lists/foobar")

There are also methods named list (to list all the available mailboxes) and lsub (to list all the “active” or “subscribed” mailboxes). The status method will return information about the mailbox.

The search method will find messages according to specified criteria, and fetch will fetch a given message. Here is an example:

msgs = imap.search("TO","lupin")
msgs.each do |mid|
env = imap.fetch(mid, "ENVELOPE")[0].attr["ENVELOPE"]
puts "From #{env.from[0].name} #{env.subject}"
end

The fetch command in the preceding code appears convoluted because it returns an array of hashes. The envelope itself is similarly complex; some of its accessors are arrays of complex objects, and some are simply strings.

IMAP has the concept of unique IDs (UIDs) and sequence numbers for messages. Typically, methods such as fetch deal with sequence numbers and have counterparts such as uid_fetch that deal with UIDs. There is no room here to explain why both numbering systems are appropriate; if you are doing any significant programming with IMAP, however, you will need to know the difference (and never get them mixed up).

The net/imap library has extensive support for handling mailboxes, messages, attachments, and so on. For more details, refer to the API reference documentation.

18.2.6 Encoding/Decoding Attachments

Files are usually attached to email or news messages in a special encoded form. More often than not, the encoding is base64, which can be encoded or decoded with the Base64 class:

bin = File.read("new.gif")
str = Base64.encode64(bin) # str is now encoded
orig = Base64.decode64(str) # orig == bin

Older mail clients may prefer to work with uuencode and uudecode; in a case like this, an attachment is more a state of mind than anything else. The attachment is simply appended to the end of the email text, bracketed inside begin and end lines, with the begin line also specifying file permissions (which may be ignored) and filename. The pack directive u serves to encode a uuencoded string. The following code shows an example:

# Assume mailtext holds the text of the email

filename = "new.gif"
bin = File.read(filename)
encoded = [bin].pack("u")

mailtext << "begin 644 #{filename}"
mailtext << encoded
mailtext << "end"
# ...

On the receiving end, we would extract the encoded information and use unpack to decode it:

# ...
# Assume 'attached' has the encoded data (including the
# begin and end lines)

lines = attached.lines
filename = /begin \d\d\d (.*)/.scan(lines[0]).first.first
encoded = lines[1..-2].join("\n")
decoded = encoded.unpack("u") # Ready to write to filename

More modern mail readers usually use MIME format for email; even the text part of the email is wrapped (although the client strips all the header information before the user sees it).

A complete treatment of MIME would be lengthy and off topic here. However, the following code shows a simple example of encoding and sending an email with a text portion and a binary attachment. The encoding for binaries is usually base64, as shown here:

require 'net/smtp'

def text_plus_attachment(subject,body,filename)
marker = "MIME_boundary"
middle = "—#{marker}\n"
ending = "—#{middle}—\n"
content = "Content-Type: Multipart/Related; " +
"boundary=#{marker}; " +
"typw=text/plain"
head1 = <<-EOF
MIME-Version: 1.0
#{content}
Subject: #{subject}
EOF
binary = File.read(filename)
encoded = [binary].pack("m") # base64
head2 = <<EOF
Content-Description: "#{filename}"
Content-Type: image/gif; name="#{filename}"
Content-Transfer-Encoding: Base64
Content-Disposition: attachment; filename="#{filename}"

EOF

# Return...
head1 + middle + body + middle + head2 + encoded + ending
end

domain = "someserver.com"
smtp = "smtp.#{domain}"
user, pass = "elgar","enigma"

body = <<EOF
This is my email. There isn't
much to say. I attached a
very small GIF file here.

— Bob
EOF

mailtext = text_plus_attachment("Hi there...",body,"new.gif")

Net::SMTP.start(smtp, 25, domain, user, pass, :plain) do |mailer|
mailer.sendmail(mailtext, 'fromthisguy@wherever.com',
['destination@elsewhere.com'])
end

18.2.7 Case Study: A Mail-News Gateway

Online communities keep in touch with each other in many ways. Two of the most traditional of these are mailing lists and newsgroups.

Not everyone wants to be on a mailing list that may generate dozens of messages per day; some would rather read a newsgroup and pick through the information at random intervals. On the other hand, some people are impatient with Usenet and want to get the messages before the electrons have time to cool off.

Therefore, we get situations in which a fairly small, private mailing list deals with the same subject matter as an unmoderated newsgroup open to the whole world. Eventually someone gets the idea for a mirror—a gateway between the two.

Such a gateway isn’t appropriate in every situation, but in the case of the Ruby mailing list, there was such a gateway from 2001 to 2011. The newsgroup messages were copied to the list, and the list emails were posted on the newsgroup.

This task was accomplished by Dave Thomas (in Ruby, of course), and we present the code with his kind permission in Listings 18.6 and 18.7.

But let’s look at a little background first. We’ve taken a quick look at how email is sent and received, but how do we handle Usenet? As it turns out, we can access the newsgroups via a protocol called the Network News Transfer Protocol (NNTP). This creation, incidentally, was the work of Larry Wall, who later on gave us Perl.

Ruby doesn’t have a “standard” library to handle NNTP, but there are several gems available that provide NNTP functionality. These examples depend on the NNTP library written by a Japanese developer (known to us only as “greentea”).

The nntp.rb library defines a module NNTP containing a class called NNTPIO; it has instance methods connect, get_head, get_body, and post (among others). To retrieve messages, you connect to the server and call get_head and get_body, repetitively. (We’re oversimplifying this.) Likewise, to post a message, you basically construct the headers, connect to the server, and call the post method.

These programs use the smtp library, which we’ve looked at previously. The original code also does some logging to track progress and record errors; we’ve removed this logging for greater simplicity.

The file params.rb is used by both programs. This file contains the parameters that drive the whole mirroring process—the names of the servers, account names, and so on. The following is a sample file that you will need to reconfigure for your own purposes. (The domain names used in the code, which all contain the word fake, are obviously intended to be fictitious.)

# These are various parameters used by the mail-news gateway
module Params
NEWS_SERVER = "usenet.fake1.org" # name of the news server
NEWSGROUP = "comp.lang.ruby" # mirrored newsgroup
LOOP_FLAG = "X-rubymirror: yes" # avoid loops
LAST_NEWS_FILE = "/tmp/m2n/last_news" # last msg num read
SMTP_SERVER = "localhost" # host for outgoing mail
MAIL_SENDER = "myself@fake2.org" # Name used to send mail
# (On a subscription-based list, this
# name must be a list member.)

MAILING_LIST = "list@fake3.org" # Mailing list address
end

The module Params merely contains constants that are accessed by the two programs. Most are self-explanatory; we’ll only point out a couple of items here. First, the LAST_NEWS_FILE constant identifies a file where the most recent newsgroup message ID is stored; this is “state information,” so that work is not duplicated or lost.

Perhaps even more important, the LOOP_FLAG constant defines a string that marks a message as having already passed through the gateway. This avoids infinite recursion and prevents the programmer from being mobbed by hordes of angry netizens who have received thousands of copies of the same message.

You might be wondering, how do we actually get the mail into the mail2news program? After all, it appears to read standard input. Well, the author recommends a setup like this: The sendmail program’s .forward file first forwards all incoming mail to procmail. The.procmail file is set up to scan for messages from the mailing list and pipe them into the mail2news program. For the exact details of this, see the documentation associated with RubyMirror (found in the Ruby Application Archive). Of course, if you are on a non-UNIX system, you will likely have to come up with your own scheme for handling this situation.

Aside from what we’ve already said, we’ll let the code stand on its own, as shown in Listings 18.6 and 18.7.

Listing 18.6 Mail-to-News


# mail2news: Take a mail message and post it
# as a news article

require "nntp"
include NNTP

require "params"

# Read in the message, splitting it into a
# heading and a body. Only allow certain
# headers through in the heading

HEADERS = %w{From Subject References Message-ID
Content-Type Content-Transfer-Encoding Date}

allowed_headers = Regexp.new(%{^(#{HEADERS.join("|")}):})

# Read in the header. Only allow certain
# ones. Add a newsgroups line and an
# X-rubymirror line.

head = "Newsgroups: #{Params::NEWSGROUP}\n"
subject = "unknown"

while line = gets
exit if line =~ /^#{Params::LOOP_FLAG}/o # shouldn't happen
break if line =~ /^\s*$/
next if line =~ /^\s/
next unless line =~ allowed_headers

# strip off the [ruby-talk:nnnn] prefix on the subject before
# posting back to the news group
if line =~ /^Subject:\s*(.*)/
subject = $1

# The following strips off the special ruby-talk number
# from the front of mailing list messages before
# forwarding them on to the news server.

line.sub!(/\[ruby-talk:(\d+)\]\s*/, '')
subject = "[#$1] #{line}"
head << "X-ruby-talk: #$1\n"
end
head << line
end

head << "#{Params::LOOP_FLAG}\n"

body = ""
while line = gets
body << line
end

msg = head + "\n" + body
msg.gsub!(/\r?\n/, "\r\n")

nntp = NNTPIO.new(Params::NEWS_SERVER)
raise "Failed to connect" unless nntp.connect
nntp.post(msg)


Listing 18.7 News-to-Mail


##
# Simple script to help mirror the comp.lang.ruby
# traffic on to the ruby-talk mailing list.
#
# We are called periodically (say once every 20 minutes).
# We look on the news server for any articles that have a
# higher message ID than the last message we'd sent
# previously. If we find any, we read those articles,
# send them on to the mailing list, and record the
# new hightest message id.

require 'nntp'
require 'net/smtp'
require 'params'

include NNTP

##
# Send mail to the mailing-list. The mail must be
# from a list participant, although the From: line
# can contain any valid address
#

def send_mail(head, body)
smtp = Net::SMTP.new
smtp.start(Params::SMTP_SERVER)
smtp.ready(Params::MAIL_SENDER, Params::MAILING_LIST) do |a|
a.write head
a.write "#{Params::LOOP_FLAG}\r\n"
a.write "\r\n"
a.write body
end
end

##
# We store the message ID of the last news we received.

begin
last_news = File.open(Params::LAST_NEWS_FILE) {|f| f.read} .to_i
rescue
last_news = nil
end

##
# Connect to the news server, and get the current
# message numbers for the comp.lang.ruby group
#
nntp = NNTPIO.new(Params::NEWS_SERVER)
raise "Failed to connect" unless nntp.connect
count, first, last = nntp.set_group(Params::NEWSGROUP)

##
# If we didn't previously have a number for the highest
# message number, we do now

if not last_news
last_news = last
end

##
# Go to the last one read last time, and then try to get more.
# This may raise an exception if the number is for a
# nonexistent article, but we don't care.

begin
nntp.set_stat(last_news)
rescue
end

##
# Finally read articles until there aren't any more,
# sending each to the mailing list.

new_last = last_news

begin
loop do
nntp.set_next
head = ""
body = ""
new_last, = nntp.get_head do |line|
head << line
end

# Don't sent on articles that the mail2news program has
# previously forwarded to the newsgroup (or we'd loop)
next if head =~ %r{^X-rubymirror:}

nntp.get_body do |line|
body << line
end

send_mail(head, body)
end
rescue
end

##
# And record the new high water mark

File.open(Params::LAST_NEWS_FILE, "w") do |f|
f.puts new_last
end unless new_last == last_news


18.2.8 Retrieving a Web Page from a URL

Suppose that, for whatever reason, we want to retrieve an HTML document from where it lives on the Web. Maybe our intent is to do a checksum and find whether it has changed so that our software can inform us of this automatically. Maybe our intent is to write our own web browser; this would be the proverbial first step on a journey of a thousand miles.

Here’s the code:

require "net/http"

begin
# Space as in space travel
uri = URI(http://www.space.com/index.html)
res = Net::HTTP.get(uri)
rescue => err
puts "Error: #{err.class}: #{err.message}"
exit
end

puts "Retrieved #{res.body.lines.size} lines " \
"and#{res.body.size} bytes"
# Process as desired...

We begin by creating a URI object with the complete protocol, domain name, and path. (The port is implied by the protocol to be 80, but can be added with a colon after the domain.) We then call the Net::HTTP.get method, which returns a Net::HTTP response object. Here, we don’t actually test the response, but if there is some kind of error, we’ll catch it and exit.

If we skip the rescue clause as we normally would, we can expect to have an entire web page stored in the res.body string. We can then process it however we want.

What could go wrong here—what kind of errors do we catch? Actually, there are several. The domain name could be nonexistent or unreachable; there could be a timeout error, an IO error, a socket error, or any of several system errors.

In addition, the response might not have succeeded: There could be a redirect to another page (which we don’t handle here), or we might get the dreaded 404 error (meaning that the document was not found). We’ll leave handling these errors and responses to you.

The next section will also be useful to you. It shows a slightly simpler way of handling this kind of task.

18.2.9 Using the Open-URI Library

The Open-URI library is the work of Tanaka Akira. Its purpose is to “unify” the programmatic treatment of net resources so that they are all intuitive and easy to handle.

This code is essentially a wrapper for the net/http, net/https, and net/ftp libraries, making available an open method that will handle an arbitrary URI. The example from the preceding section can be written this way:

require 'open-uri'
site = open("http://www.marsdrive.com/")
data = site.read
puts "Retrieved #{data.split.size} lines, #{data.size} bytes"

The file returned by open (site in the previous case) is not just a file. This object also has the methods of the OpenURI::Meta module so that we can access metadata:

# ...
uri = f.base_uri # a URI object with its own readers
ct = f.content_type # "text/html"
cs = f.charset # "utf-8"
ce = f.content_encoding # []

The library allows the specifying of additional header fields by using a hash with the open command. It also handles proxy servers and has several other useful features. There are cases where this library may be insufficient; for example, you may need to parse HTTP headers, buffer extremely large downloads, send cookies, and other such tasks. In those cases, you can use the net/http library or a gem for making HTTP requests, such the http, curb, or httparty gem. As always, API documentation for these libraries and gems is available online at rdoc.info.

18.3 Conclusion

In this chapter, we’ve had a good introduction to lower-level networking, including simple servers and clients. We’ve seen how to write a client for an existing server that we didn’t create.

We’ve looked at higher-level protocols such as POP and IMAP for receiving mail. Likewise, we’ve looked at SMTP for sending mail. In connection with these, we’ve seen how to encode and decode file attachments. We’ve also had an exposure to NNTP through the mail-news gateway code.

Now it’s time to look more closely at a subset of this topic. One of the most important types of network programming today is web development, which is the topic of the next chapter.