Network Servers

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


A server spends its lifetime waiting for messages and answering them. It might have to do some serious processing in order 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 might respond to only one request at a time, or it might thread its responses. The former is easier to code, but the latter is better if there are many clients trying to connect simultaneously.

It's also conceivable that a server might 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.

A Simple Serialized Server: Time of Day

Let's look at the simplest server we can think of, which might 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.

We use the term port frequently in this chapter. A port is simply an "address" on a client or server used to identify the process with which we are communicating. The combination of IP address and port are enough to uniquely specify one end of a connection. Often port numbers are arbitrary, even randomly generated; but there are certain well-known ports that are universally agreed upon (such as port 80 for HTTP).

In our example, a single-threaded server handles requests in line. 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. The server has no knowledge of the client until the client makes contact, after which the server knows the client's identity (IP address). Because UDP is unreliable, we time out after a reasonable length of time.

Here 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 

Implementing a Threaded Server

Some servers get heavy traffic. It can be efficient to handle each request in a separate thread.

Here is a re-implementation 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.

Note how the session variable is passed into the thread, which then refers to it by the name my_session instead. For more information on this or other aspects of threading, refer to Chapter 7, "Ruby Threads."

The client code is, of course, unchanged. From the point of view of the client, the server's behavior is unchanged (except that it might appear more reliable).

Case Study: A Peer-to-Peer Chess Server

But helpless Pieces of the Game He plays

Upon this Chequer-board of Nights and Days;

Hither and thither moves, and checks, and slays,

And one by one back in the Closet lays.

The Rubaiyat, Omar Khayyam (trans. Fitzgerald)

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. An example is a peer-to-peer file sharing service such as Napster; other examples are various game servers or chat programs (including some sophisticated ones such as NetMeeting).

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.

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

First of all, 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 informationhis 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'll use a colon instead of the more intuitive @ so that it won't resemble an e-mail 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, then a message is sent back to each of them; each client is given enough information to contact his opponent.

Then 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. Once the server has introduced them, the clients talk to each other, so that effectively one of them is really a server by this point. This is a semantic distinction that we 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 which is "really" a server will instantiate a TCPServer and the other will instantiate a TCPSocket at the other end. We're assuming a mutually agreed-on 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 9.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 there could be multiple threads trying to add users to the list at one time.

Listing 9.1 The Chess Server
 require "thread"     require "socket"     ChessServer     = "127.0.0.1"  # Replace this IP address!     ChessServerPort = 12000     # 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 = { }     Player = Struct.new("Player", :opponent, :address,                         :port, :ipname, :session,                         :short, :long, :id)     def match?(p1, p2)       $list[p1] and $list[p2] and         $list[p1][0] == p2 and $list[p2][0] == p1     end     def handle_client(sess, msg, addr, port, ipname)       $mutex.synchronize do         cmd, p1short, p2long = msg.split         if cmd != "login"           puts "Protocol error: client msg was #{ msg} "           return         end         p1long = p1short.dup + ":#{ addr} "         p2short, host2 = p2long.split(":")         host2 = ipname if host2 == nil         p2long = p2short + ":" + IPSocket.getaddress(host2)         p1 = Struct::Player.new(p2long, addr, port, ipname,                                 sess, p1short, p1long)         p2 = Struct::Player.new         # Note: We get user:hostname on the command line,         # but we store it in the form user:address         $list[p1long] = p1         if match?(p1long, p2long)           # Note these names are "backwards" now: player 2           # logged in first, if we got here.           p1, p2 = $list[p1long], $list[p2long]           # Player ID = name:ipname:color           # Color: 0=white, 1=black           p1.id = "#{ p1short} :#{ p1.ipname} :1"           p2.id = "#{ p2short} :#{ p2.ipname} :0"           sess1, sess2 = [p1.session, p2.session]           sess1.puts "#{ p2.id} "           sess1.close           sess2.puts "#{ p1.id} "           sess2.close         end       end     end      text = nil     $server = TCPServer.new(ChessServer, ChessServerPort)     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 if user presses Enter (this is only                    # for convenience in running this example) 

The handle_client method stores information for the client. If the corresponding client is already stored, each client is sent a message with 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 9.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 9.2 The Chess Client
 require "socket" require "timeout" ChessServer     = '127.0.0.1'  # Replace this IP address ChessServerPort = 12000 PeerPort        = 12001 WHITE, BLACK = 0, 1 Colors = %w[White Black] def drawBoard(board)   puts <<-EOF +                  + | Stub! Drawing the board here...   | +                  +   EOF end def analyzeMove(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 ended?(myColor, whoseMove,            whiteName, blackName, move)   opponent = (myColor==WHITE ? blackName : whiteName)   case move     when /resign/i     # Player explicitly resigns       if myColor == whoseMove         puts "You've resigned. #{ opponent}  wins."       else         puts "#{ opponent}  resigns. You win!"       end       return true     when /Checkmate/   # analyzeMove detects mate       if myColor == whoseMove         puts "You have checkmated #{ opponent} ."       else         puts "#{ opponent}  has checkmated you."       end       return true   end   return false end def myMove(who, lastmove, num, board, sock)   # lastmove not actually used in dummy example   ok = false   until ok do     print "\nYour move: "     move = STDIN.gets.chomp     ok = analyzeMove(who, move, num, board)     puts "Illegal move" if not ok   end   sock.puts move   drawBoard(board)   move end def otherMove(who, lastmove, num, board, sock)   # lastmove not actually used in dummy example   move = sock.gets.chomp   puts "\nOpponent: #{ move} "   drawBoard(board)   move end def setupWhite(opponent)   puts "\nWaiting for a connection..."   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"   session              # Return connection end def setupBlack(opponent, ipname)   # Asymmetrical because we assume black is the client   puts "\nConnecting..."   socket = TCPSocket.new(ipname, PeerPort)   socket.puts "ready"   puts "Playing #{ opponent} ... you are black.\n"   socket               # Return connection end # "Main"... if ARGV.size != 2   puts "Parameters required: myname opponent[:hostname]"   exit end myself, opponentID = ARGV opponent = opponentID.split(":")[0]   # Remove hostname # Contact the chess server print "Connecting to the chess server... " STDOUT.flush socket = TCPSocket.new(ChessServer, ChessServerPort) socket.puts "login #{ myself}  #{ opponentID} " response = socket.gets.chomp puts "got response.\n" name, ipname, oppColor = response.split ":" oppColor = oppColor.to_i myColor = if oppColor == WHITE then BLACK else WHITE end move = nil board = nil      # Not really used in this dummy example num = 0 if myColor == WHITE         # We're white   who, opp = WHITE, BLACK   session = setupWhite(opponent)   drawBoard(board)   loop do     num += 1     move = myMove(who, move, num, board, session)     break if ended?(who, who, myself, opponent, move)     move = otherMove(who, move, num, board, session)     break if ended?(who, opp, myself, opponent, move)   end else                        # We're black   who, opp = BLACK, WHITE   socket = setupBlack(opponent, ipname)   drawBoard(board)   loop do     num += 1     move = otherMove(who, move, num, board, socket)     break if ended?(who, opp, opponent, myself, move)     move = myMove(who, move, num, board, socket)     break if ended?(who, who, opponent, myself, move)   end   socket.close end 

We've defined our 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.

As we said, 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.

A move, by the way, is simply a string. Two notations are in common use, but our code is stubbed so heavily that it doesn't even know which one we're using.

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 myMove method always refers to the local side; likewise, otherMove refers to the remote side.

We show some sample output in Listing 9.3. The client executions are displayed side by side in this listing.

Listing 9.3 Sample Chess Client Execution
 % ruby chess.rb Hal Capablanca:        % 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! 

This little example could serve as the beginning of a genuine application. We'd have to add knowledge of chess, add security, improve reliability, add a graphical interface, and so on. The possibilities are endless. If you do anything with this idea, let us know about it.


   

 

 



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

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