18.1. Network ServersA 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 DayLet'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 ServerSome 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 ServerIt 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
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
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
|