Section 18.2. Network Clients


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 that 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 our 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" PORT = 12321 HOST = ARGV[0] || 'localhost' server = UDPSocket.open       # Using UDP here... server.bind nil, PORT loop do   text, sender = server.recvfrom(1)   server.send(Time.new.to_s + "\n", 0, sender[3], sender[1]) end


And here is the client code:

require "socket" require "timeout" PORT = 12321 HOST = ARGV[0] || 'localhost' socket = UDPSocket.new socket.connect(HOST, PORT) socket.send("", 0) timeout(10) do   time = socket.gets   puts time end


Note that the client makes its request simply by sending a null packet. Because UDP is 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" PORT = 12321 server = TCPServer.new(PORT) while (session = server.accept)   session.puts Time.new   session.close end


Note the straightforward use of the TCPServer class. Here is the TCP version of the client code:

require "socket" PORT = 12321 HOST = ARGV[0] || "localhost" session = TCPSocket.new(HOST, PORT) time = session.gets session.close puts time


18.1.2. Implementing a Threaded Server

Some servers get heavy traffic. It can be efficient to handle each request in 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" PORT = 12321 server = TCPServer.new(PORT) while (session = server.accept)   Thread.new(session) do |my_session|     my_session.puts Time.new     my_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 more reliable).

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 in 2001; other examples are chat servers such as ICQ 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 it isn't reliable; we would have to use timeouts as we saw in an earlier example.

We'll let each client provide two pieces of information: His own name (like a username) and the name of the desired opponent. We'll introduce a 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 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 it. 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 be made more sophisticated, of course.

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; in theory multiple threads could be trying to add users to the list at one time.

Listing 18.1. The Chess Server

require "thread" require "socket" PORT = 12000 HOST = "96.97.98.99"  # Replace this IP address # Exit if user presses Enter at server end waiter = Thread.new do   puts "Press Enter to exit the server."   gets   exit end $mutex = Mutex.new $list = {} def match?(p1, p2)   return false if !$list[p1] or !$list[p2]   if ($list[p1][0] == p2 and $list[p2][0] == p1)     true   else     false   end end def handle_client(sess, msg, addr, port, ipname)   $mutex.synchronize do     cmd, player1, player2 = msg.split     # Note: We get user:hostname on the command line,     # but we store it in the form user:address     p1short = player1.dup              # Short names (i.e.,     p2short = player2.split(":")[0]    # no ":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     $list[player1] = [player2, addr, port, ipname, sess]     if match?(player1, player2)       # Note these names are "backwards" now: player2       # logged in first, if we got here.       p1 = $list[player1]       p2 = $list[player2]       # Player ID = name:ipname:color       # Color: 0=white, 1=black       p1id = "#{p1short}:#{p1[3]}:1"       p2id = "#{p2short}:#{p2[3]}:0"       sess1 = p1[4]       sess2 = p2[4]       sess1.puts "#{p2id}"       sess2.puts "#{p1id}"       sess1.close       sess2.close     end   end end text = nil $server = TCPServer.new(HOST, PORT) while session = $server.accept do   Thread.new(session) do |sess|     text = sess.gets     puts "Received: #{text}"  # So we know server gets it     domain, port, ipname, ipaddr = sess.peeraddr     handle_client sess, text, ipaddr, port, ipname     sleep 1   end end waiter.join    # Exit only if user presses Enter

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     = '96.97.98.99'  # Replace this IP address ChessServerPort = 12000 PeerPort        = 12001 WHITE, BLACK = 0, 1 Colors = %w[White Black] 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 my_move(who, lastmove, num, board, sock)   ok = false   until ok do     print "\nYour move: "     move = STDIN.gets.chomp     ok = analyze_move(who, move, num, board)     puts "Illegal move" if not ok   end   sock.puts move   move end def other_move(who, move, num, board, sock)   move = sock.gets.chomp   puts "\nOpponent: #{move}"   move 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) response = nil 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   str = nil   begin     timeout(30) do       str = session.gets.chomp       if str != "ready"         raise "Protocol error: ready-message was #{str}"       end     end   rescue TimeoutError     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     move = my_move(who, move, num, board, session)     draw_board(board)     case move       when "resign"         puts "\nYou've resigned. #{opponent} wins."         break       when /Checkmate/         puts "\nYou have checkmated #{opponent}!"         draw_board(board)         break     end     move = other_move(who, move, num, board, session)     draw_board(board)     case move       when "resign"         puts "\n#{opponent} has resigned... you win!"         break       when /Checkmate/         puts "\n#{opponent} has checkmated you."         break     end   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     move = other_move(who, move, num, board, socket)     draw_board(board)  # Draw board after white move     case move       when "resign"         puts "\n#{opponent} has resigned... you win!"         break       when /Checkmate/         puts "\n#{opponent} has checkmated you."         break     end     move = my_move(who, move, num, board, socket)     draw_board(board)     case move       when "resign"         puts "\nYou've resigned. #{opponent} wins."         break       when /Checkmate/         puts "\nYou have checkmated #{opponent}!"         break     end   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 into 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. But 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. The client executions are displayed side by side in this listing.

Listing 18.3. Sample Chess Client Execution

% ruby chess.rb Hal                     % ruby chess.rb   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!




The Ruby Way(c) Solutions and Techniques in Ruby Programming
The Ruby Way, Second Edition: Solutions and Techniques in Ruby Programming (2nd Edition)
ISBN: 0672328844
EAN: 2147483647
Year: 2004
Pages: 269
Authors: Hal Fulton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net