Ruby’s networking capabilities are provided by the standard library rather than by core classes. For this reason, the subsections that follow do not attempt to enumerate each available class or method. Instead, they demonstrate how to accomplish common networking tasks with simple examples. Use ri for more complete documentation.
At the lowest level, networking is accomplished with sockets,
which are a kind of IO
object. Once
you have a socket opened, you can read data from, or write data to,
another computer just as if you were reading from or writing to a file.
The socket class hierarchy is somewhat confusing, but the details are
not important in the following examples. Internet clients use the
TCPSocket
class, and Internet servers
use the TCPServer
class (also a
socket). All socket classes are part of the standard library, so to use
them in your Ruby program, you must first write:
require 'socket'
To write Internet client applications, use the TCPSocket
class.
Obtain a TCPSocket
instance with the TCPSocket.open
class method, or with its synonym TCPSocket.new
. Pass the name of the host to
connect to as the first argument and the port as the second argument.
(The port should be an integer between 1 and 65535, specified as a
Fixnum
or String
object. Different internet protocols
use different ports. Web servers use port 80 by default, for example.
You may also pass the name of an Internet service, such as “http”, as
a string, in place of a port number, but this is not well documented
and may be system dependent.)
Once you have a socket open, you can read from it like any
IO
object. When done, remember to
close it, as you would close a file. The following code is a very
simple client that connects to a given host and port, reads any
available data from the socket, and then exits:
require 'socket' # Sockets are in standard library host, port = ARGV # Host and port from command line s = TCPSocket.open(host, port) # Open a socket to host and port while line = s.gets # Read lines from the socket puts line.chop # And print with platform line terminator end s.close # Close the socket when done
Like File.open
, the TCPSocket.open
method can be invoked with a
block. In that form, it passes the open socket to the block and
automatically closes the socket when the block returns. So we can also
write this code like this:
require 'socket' host, port = ARGV TCPSocket.open(host, port) do |s| # Use block form of open while line = s.gets puts line.chop end end # Socket automatically closed
This client code is for use with services like the old-style (and now defunct) Unix “daytime” service. With services like these, the client doesn’t make a query; the client simply connects and the server sends a response. If you can’t find an Internet host running a server to test the client with, don’t despair—the next section shows how to write an equally simple time server.
To write Internet servers, we use the TCPServer
class. In essence, a TCPServer
object is a factory for TCPSocket
objects. Call TCPServer.open
to specify a port for your
service and create a TCPServer
object. Next, call the accept
method of the returned TCPServer
object. This method waits until a client connects to the port you
specified, and then returns a TCPSocket
object that represents the
connection to that client.
The following code shows how we might write a simple time server. It listens for connections on port 2000. When a client connects to that port, it sends the current time to the client and closes the socket, thereby terminating the connection with the client:
require 'socket' # Get sockets from stdlib server = TCPServer.open(2000) # Socket to listen on port 2000 loop { # Infinite loop: servers run forever client = server.accept # Wait for a client to connect client.puts(Time.now.ctime) # Send the time to the client client.close # Disconnect from the client }
To test this code, run it in the background or in another terminal window. Then, run the simple client code from above with a command like this:
ruby client.rb localhost 2000
Most Internet protocols are implemented using TCPSocket
and TCPServer
, as shown earlier. A
lower-overhead alternative is to use UDP datagrams, with the UDPSocket
class. UDP allows computers to
send individual packets of data to other computers, without the
overhead of establishing a persistent connection. The following client
and server code demonstrate: the client sends a datagram containing a
string of text to a specified host and port. The server, which should
be running on that host and listening on that port, receives the text,
converts it to uppercase (not much of a service, I know), and sends it
back in a second datagram.
The client code is first. Note that although UDPSocket
objects are IO
objects, datagrams are pretty different
from other IO
streams. For this
reason, we avoid using IO
methods
and use the lower-level sending and receiving methods of UDPSocket
. The second argument to the
send
method specifies flags. It is
required, even though we are not setting any flags. The argument to
recvfrom
specifies the maximum
amount of data we are interested in receiving. In this case, we limit
our client and server to transferring 1 kilobyte:
require 'socket' # Standard library host, port, request = ARGV # Get args from command line ds = UDPSocket.new # Create datagram socket ds.connect(host, port) # Connect to the port on the host ds.send(request, 0) # Send the request text response,address = ds.recvfrom(1024) # Wait for a response (1kb max) puts response # Print the response
The server code uses the UDPSocket
class just as the client code
does—there is no special UDPServer
class for datagram-based servers. Instead of calling connect
to connect the socket, our server
calls bind
to tell the socket what
port to listen on. The server then uses send
and recvfrom
, just as the client does, but in
the opposite order. It calls recvfrom
to wait until it receives a
datagram on the specified port. When that happens, it converts the
text it receives to uppercase and sends it back. An important point to
notice is that the recvfrom
method
returns two values. The first is the received data. The second is an
array containing information about where that data came from. We
extract host and port information from that array and use it to send
the response back to the client:
require 'socket' # Standard library port = ARGV[0] # The port to listen on ds = UDPSocket.new # Create new socket ds.bind(nil, port) # Make it listen on the port loop do # Loop forever request,address=ds.recvfrom(1024) # Wait to receive something response = request.upcase # Convert request text to uppercase clientaddr = address[3] # What ip address sent the request? clientname = address[2] # What is the host name? clientport = address[1] # What port was it sent from ds.send(response, 0, # Send the response back... clientaddr, clientport) # ...where it came from # Log the client connection puts "Connection from: #{clientname} #{clientaddr} #{clientport}" end
The following code is a more fully developed Internet client in
the style of telnet. It connects to the specified
host and port and then loops, reading a line of input from the
console, sending it to the server, and then reading and printing the
server’s response. It demonstrates how to determine the local and
remote addresses of the network connection, adds exception handling, and
uses the IO
methods read_nonblock
and readpartial
described earlier in this
chapter. The code is well-commented and should be self-explanatory:
require 'socket' # Sockets from standard library host, port = ARGV # Network host and port on command line begin # Begin for exception handling # Give the user some feedback while connecting. STDOUT.print "Connecting..." # Say what we're doing STDOUT.flush # Make it visible right away s = TCPSocket.open(host, port) # Connect STDOUT.puts "done" # And say we did it # Now display information about the connection. local, peer = s.addr, s.peeraddr STDOUT.print "Connected to #{peer[2]}:#{peer[1]}" STDOUT.puts " using local port #{local[1]}" # Wait just a bit, to see if the server sends any initial message. begin sleep(0.5) # Wait half a second msg = s.read_nonblock(4096) # Read whatever is ready STDOUT.puts msg.chop # And display it rescue SystemCallError # If nothing was ready to read, just ignore the exception. end # Now begin a loop of client/server interaction. loop do STDOUT.print '> ' # Display prompt for local input STDOUT.flush # Make sure the prompt is visible local = STDIN.gets # Read line from the console break if !local # Quit if no input from console s.puts(local) # Send the line to the server s.flush # Force it out # Read the server's response and print out. # The server may send more than one line, so use readpartial # to read whatever it sends (as long as it all arrives in one chunk). response = s.readpartial(4096) # Read server's response puts(response.chop) # Display response to user end rescue # If anything goes wrong puts $! # Display the exception to the user ensure # And no matter what happens s.close if s # Don't forget to close the socket end
The simple time server shown earlier in this section never
maintained a connection with any client—it would simply tell the
client the time and disconnect. Many more sophisticated servers maintain a
connection, and in order to be useful, they must allow multiple
clients to connect and interact at the same time. One way to do this
is with threads—each client runs in its own thread. We’ll see an
example of a multithreaded server later in this chapter. The
alternative that we’ll consider here is to write a multiplexing server
using the Kernel.select
method.
When a server has multiple clients connected, it cannot call a
blocking method like gets
on the
socket of any one client. If it blocks waiting for input from one
client, it won’t be able to receive input from other clients or accept
connections from new clients. The select
method solves this problem; it allows
us to block on a whole array of IO
objects, and returns when there is activity on any one of those
objects. The return value of select
is an array of arrays of IO
objects. The first element of the array is the array of streams
(sockets, in this case) that have data to be read (or a connection to
be accepted).
With that explanation of select
, you should be able to understand the
following server code. The service it implements is trivial—it simply
reverses each line of client input and sends it back. It is the
mechanism for handling multiple connections that is interesting. Note
that we use select
to monitor both
the TCPServer
object and each of
the client TCPSocket
objects. Also
note that the server handles the case where a client asks to
disconnect as well as the case where the client disconnects
unexpectedly:
# This server reads a line of input from a client, reverses # the line and sends it back. If the client sends the string "quit" # it disconnects. It uses Kernel.select to handle multiple sessions. require 'socket' server = TCPServer.open(2000) # Listen on port 2000 sockets = [server] # An array of sockets we'll monitor log = STDOUT # Send log messages to standard out while true # Servers loop forever ready = select(sockets) # Wait for a socket to be ready readable = ready[0] # These sockets are readable readable.each do |socket| # Loop through readable sockets if socket == server # If the server socket is ready client = server.accept # Accept a new client sockets << client # Add it to the set of sockets # Tell the client what and where it has connected. client.puts "Reversal service v0.01 running on #{Socket.gethostname}" # And log the fact that the client connected log.puts "Accepted connection from #{client.peeraddr[2]}" else # Otherwise, a client is ready input = socket.gets # Read input from the client # If no input, the client has disconnected if !input log.puts "Client on #{socket.peeraddr[2]} disconnected." sockets.delete(socket) # Stop monitoring this socket socket.close # Close it next # And go on to the next end input.chop! # Trim client's input if (input == "quit") # If the client asks to quit socket.puts("Bye!"); # Say goodbye log.puts "Closing connection to #{socket.peeraddr[2]}" sockets.delete(socket) # Stop monitoring the socket socket.close # Terminate the session else # Otherwise, client is not quitting socket.puts(input.reverse) # So reverse input and send it back end end end end
We can use the socket library to implement any Internet protocol. Here, for example, is code to fetch the content of a web page:
require 'socket' # We need sockets host = 'www.example.com' # The web server port = 80 # Default HTTP port path = "/index.html" # The file we want # This is the HTTP request we send to fetch a file request = "GET #{path} HTTP/1.0 " socket = TCPSocket.open(host,port) # Connect to server socket.print(request) # Send request response = socket.read # Read complete response # Split response at first blank line into headers and body headers,body = response.split(" ", 2) print body # And display it
HTTP is a complex protocol, and the simple code above only
really handles straightforward cases. You might prefer to use a
prebuilt library like Net::HTTP
for
working with HTTP. Here is code that does the equivalent of the
previous code:
require 'net/http' # The library we need host = 'www.example.com' # The web server path = '/index.html' # The file we want http = Net::HTTP.new(host) # Create a connection headers, body = http.get(path) # Request the file if headers.code == "200" # Check the status code # NOTE: code is not a number! print body # Print body if we got it else # Otherwise puts "#{headers.code} #{headers.message}" # Display error message end
Similar libraries exist for working with the FTP, SMTP, POP, and IMAP protocols. Details of those standard libraries are beyond the scope of this book.
Finally, recall that the open-uri
library described earlier in the
chapter makes fetching a web page even easier:
require 'open-uri' open("http://www.example.com/index.html") {|f| puts f.read }
18.220.174.191