Sie sind auf Seite 1von 14

Building an Echo Server

Last updated Dec 9, 2005.

Echo Server

One of the simplest types of servers that you can build is an echo server. An echo server
simply returns exactly what you send it. For example, if you send it the string "abc" it
will echo "abc" back to you. There is hardly a demand for a high-performance echo
server, but because of its simplicity it lends itself well to exercises like this one, to
illustrate how to build a server from the framework we built.

Listing 5. EchoServer.java

package com.javasrc.server.echo;

import java.net.*;
import com.javasrc.server.*;

/**
* An echo server echos, or repeats, all data that it receives
*/
public class EchoServer extends AbstractServer
{
/**
* Creates a new EchoServer
*/
public EchoServer()
{
// Initialize our super class
super( 8899, 50, "com.javasrc.server.echo.EchoRequestHandler",
2000, 5, 10 );
}

/**
* Launch an echo server
*/
public static void main( String[] args )
{
EchoServer es = new EchoServer();
es.startServer();
}
}

The core functionality for the EchoServer is the single call in its constructor to its super
class's constructor. The options create the following behavior:

1. The echo server listens on port 8899


2. The ServerSocket holds up to 50 connections in its backlog waiting for the
connection to be added to the RequestQueue
3. com.javasrc.server.echo.EchoRequestHandler is the RequestHandler
implementation for the EchoServer
4. The request queue can grow up to 2000 before rejecting new connections
5. Initially, the RequestQueue creates five RequestThreads and the thread pool will
never drop below five threads
6. The thread pool can grow to a maximum of ten threads.

The AbstractServer class then handles creating the ServerSocket, listening on port
8899, and the request handling infrastructure to handle up to ten simultaneous echo
clients. As each RequestThread processes its connection, it delegates the actual
"conversation" with the client to the EchoRequestHandler class, shown in Listing 6.

Listing 6. EchoRequestHandler.java

package com.javasrc.server.echo;

// Import the Java classes


import java.io.*;
import java.net.*;

// Import our RequestHandler abstract base class


import com.javasrc.server.RequestHandler;

/**
* An EchoRequestHandler handles incoming requests sent to an Echo
Server
*/
public class EchoRequestHandler implements RequestHandler
{
/**
* Echo Server request handling logic
*/
public void handleRequest( Socket socket )
{
try
{
// Obtain input and output streams to the client
BufferedReader in = new BufferedReader( new
InputStreamReader( socket.getInputStream() ) );
PrintStream out = new PrintStream( socket.getOutputStream() );

// Send our header


out.println( "JavaSRC EchoServer" );
out.flush();

// Read lines from the client and echo them back; close the
connection as soon
// as the Echo receives a blank line
String line = in.readLine();
while( line != null "" line.length() > 0 )
{
// Echo the line back to the client
out.println( line );
out.flush();

// Read the next line


line = in.readLine();
}

// Be cordial and say goodbye


out.println( "Goodbye" );
out.flush();

// Close our streams and socket


out.close();
in.close();
}
catch( Exception e )
{
e.printStackTrace();
}
}
}

The EchoRequestHandler's handleRequest() method is called by the RequestThread


and passed the Socket to communicate with the client. The handlerRequest() method
begins by obtaining a BufferedReader to read data from the client and a PrintWriter
to write data back to the client. It then sits in a loop performing the following operations:

1. Read a line from the client.


2. Write that line back to the client (and flush the buffer to ensure that the client
receives the line).
3. Read the next line from the client.
4. Repeat.

We always need a condition in a server to terminate the conversation. In this case, two
conditions end the communication:

• The client sends a blank line


• The client closes the socket

Once the conversation is complete, the handleRequest() method returns. The


RequestThread closes the socket and waits for the next request.

Although an echo server is very simplistic, the fact that we were able to create a very
robust one (in terms of quickly handling several simultaneous connections) in under 200
lines of well-documented code is impressive.

Chat Server

A chat server is an interesting server. I was first involved in constructing one in 1998,
while working for an online video game company. The purpose of a chat server is to
enable clients to connect and chat with one another. A basic chat server provides the
following features:

• A user can logon with a unique username.


• All other users are notified that the new user has joined.
• A user can send messages to all other users.
• A user can send a private message to a specific user.
• A user can send an emotion to all other users.
• A user can exit the chat.
• All other users are notified that a user left the chat.

To implement this functionality in the JavaSRC server framework, we build three classes:
ChatServer, ChatRequestHandler, and DuplicateLoginException. While the echo
server completely disconnected the server from the request handler, the chat server needs
a much tighter coupling between the two.

The request handler represents a chat client. It needs to tell the chat server to send
messages to one or more other chat clients. Plus, the chat server needs to know about all
clients to facilitate this communication and to report management functionality (such as
current users and user joining and exiting events). To facilitate this, the ChatServer
provides a static reference to its instance that request handlers can use to register
themselves with.

Listing 7. ChatServer.java

package com.javasrc.server.chat;

// Import the Java classes


import java.util.*;

// Import the JavaSRC Server classes


import com.javasrc.server.*;

// Import our exceptions


import com.javasrc.server.chat.exception.*;

/**
* A chat server enables users to connect and chat
*/
public class ChatServer extends AbstractServer
{
/**
* A static reference to the ChatServer
*/
public static ChatServer theServer = null;

/**
* A Map of usernames to chat clients
*/
private Map chatClients = new TreeMap();
/**
* Creates a new chat server listening on port 9988
*/
public ChatServer()
{
super( 9988, 50, "com.javasrc.server.chat.ChatRequestHandler",
2000, 5, 50 );
theServer = this;
}

/**
* Adds a new chat client
*/
public void addChatClient( String name, ChatRequestHandler client )
throws DuplicateLoginException
{
if( this.chatClients.containsKey( name ) )
{
throw new DuplicateLoginException( name );
}
for( Iterator i=this.chatClients.keySet().iterator(); i.hasNext(); )
{
String username = ( String )i.next();
System.out.println( "notifying user: " + username + " that a new
user was added: " + name );
ChatRequestHandler user = ( ChatRequestHandler )
this.chatClients.get( username );
user.newUser( name );
}
this.chatClients.put( name, client );
}

/**
* Sends a message to all users
*/
public void sendMessage( String user, String message )
{
for( Iterator i=this.chatClients.keySet().iterator(); i.hasNext(); )
{
String username = ( String )i.next();
if( !username.equalsIgnoreCase( user ) )
{
ChatRequestHandler client = ( ChatRequestHandler )
this.chatClients.get( username );
client.sendMessage( user, message );
}
}
}

/**
* Sends an emotion to all users
*/
public void sendEmotion( String user, String message )
{
for( Iterator i=this.chatClients.keySet().iterator(); i.hasNext(); )
{
String username = ( String )i.next();
if( !username.equalsIgnoreCase( user ) )
{
ChatRequestHandler client = ( ChatRequestHandler )
this.chatClients.get( username );
client.sendEmotion( user, message );
}
}
}

/**
* Sends a message to a specific user
*/
public void sendMessage( String from, String to, String message )
{
for( Iterator i=this.chatClients.keySet().iterator(); i.hasNext(); )
{
String username = ( String )i.next();
if( username.equalsIgnoreCase( to ) )
{
ChatRequestHandler client = ( ChatRequestHandler )
this.chatClients.get( username );
client.privateMessage( from, message );
}
}
}

/**
* Returns a Set containing the currently logged in users
*/
public Set getUsers()
{
return this.chatClients.keySet();
}

/**
* Removes the specified chat client from the chat
*/
public void removeChatClient( String name )
{
if( name != null "" this.chatClients.containsKey( name ) )
{
this.chatClients.remove( name );
for( Iterator i=this.chatClients.keySet().iterator();
i.hasNext(); )
{
String username = ( String )i.next();
ChatRequestHandler user = ( ChatRequestHandler )
this.chatClients.get( username );
user.removeUser( name );
}
}
}

/**
* Creates a new stand-alone chat server
*/
public static void main( String[] args )
{
ChatServer cs = new ChatServer();
cs.startServer();
}
}

The constructor creates a server listening on port 9988 with a socket backlog of 50
connections, a request queue that can grow up to 2000 requests, 5 initial and minimum
request threads, and 50 maximum request threads. Request handling is delegated to the
com.javasrc.server.chat.ChatRequestHandler class.

Because the ChatServer is the main conduit between chat clients, it maintains references
to them in a private chatClients map and exposes the following methods for them to
use:

• addChatClient(): Adds the specified chat client to the chat server and notify all
existing clients that this new client has arrived; duplicate logins are forbidden.
• sendMessage(): Sends a message from a client to all other chat clients.
• sendMessage() (from and to names): Sends a private message from a client to
another client.
• sendEmotion(): Sends an emotion from a client to all other clients.
• getUsers(): Returns a Set containing the names of all currently logged on users.
• removeChatClient(): Removes the specified client from the chat server and
notifies all other clients that this user has exited.

Socket communications with chat clients are handled in the ChatRequestHandler class,
shown in listing 8.

Listing 8. ChatRequestHandler.java

package com.javasrc.server.chat;

import com.javasrc.server.*;

// Import the Java classes


import java.io.*;
import java.util.*;

// Import our exception classes


import com.javasrc.server.chat.exception.*;

/**
* A ChatRequestHandler manages the communications for a single chat
user
*/
public class ChatRequestHandler implements RequestHandler
{
/**
* The user’s username once he logs in
*/
private String username;

/**
* Has the user logged in yet?
*/
private boolean loggedIn = false;

/**
* Once a client has connected, this is the interface to read from
the client
*/
private BufferedReader in;

/**
* Once a client has connected, this is the interface to write out to
the client
*/
private PrintWriter out;

/**
* Handles the incoming request
*
* @param socket The socket communication back to the client
*/
public void handleRequest(java.net.Socket socket)
{
try
{
// Get input and output writers that we can use to communicate
with the client through
this.in = new BufferedReader( new
InputStreamReader( socket.getInputStream() ) );
this.out = new PrintWriter( socket.getOutputStream() );

// Say "hi"
System.out.println( "New user connected" );
out.println( "Welcome to the JavaSRC ChatServer!" );
out.flush();

// Keep reading lines until we get the "EXIT" command


String line = in.readLine();
boolean connected = true;
while( connected )
{
if( line.length() < 4 )
{
// All commands are required to contain a colon
showHelp();
}
else
{
try
{
// Extract the command name
String command = line.substring( 0, 4 );
// Handle the command; a false return value = the user
disconnected
connected = handleCommand( command, line );
}
catch( Exception e1 )
{
e1.printStackTrace();
}
}

// Read the next line from the client if they are still
connected
if( connected )
{
line = in.readLine();
}
}

// Say goodbye
System.out.println( "User exited: " + this.username );
this.loggedIn = false;
this.username = null;
out.println( "Goodbye" );
out.flush();
}
catch( Exception e )
{
e.printStackTrace();
}
}

/**
* Handles command processing
*/
private boolean handleCommand( String command, String line )
{
if( command.equalsIgnoreCase( "user" ) )
{
if( !this.loggedIn )
{
String argument = line.substring( 5 );
System.out.println( "Received login command for: " + argument );
if( argument.length() == 0 )
{
out.println( "ERROR Invalid username" );
}
else
{
try
{
ChatServer.theServer.addChatClient( argument, this );
this.username = argument;
this.loggedIn = true;
out.println( "SUCCESS User " + this.username + " logged in"
);
}
catch( DuplicateLoginException dpe )
{
out.println( "ERROR Username " + argument + " is already in
use" );
}
}
}
else
{
out.println( "ERROR You are already logged in" );
}
}
else if( command.equalsIgnoreCase( "help" ) )
{
showHelp();
}
else if( command.equalsIgnoreCase( "exit" ) )
{
System.out.println( "Received exit command for user: " +
this.username );
ChatServer.theServer.removeChatClient( this.username );
return false;
}
else
{
// All of these commands require the user to first be logged in
if( !this.loggedIn )
{
out.println( "ERROR You need to logon to send commands to the
ChatServer" );
out.flush();
}
else if( command.equalsIgnoreCase( "send" ) )
{
String argument = line.substring( 5 );
ChatServer.theServer.sendMessage( this.username, argument );
}
else if( command.equalsIgnoreCase( "priv" ) )
{
String argument = line.substring( 5 );
if( argument.indexOf( ’:’ ) == -1 )
{
out.println( "ERROR: Private messages need a recipient!" );
}
else
{
String recipient = argument.substring( 0,
argument.indexOf( ’:’ ) );
String message = argument.substring( argument.indexOf( ’:’ )
+ 1 );
ChatServer.theServer.sendMessage( this.username, recipient,
message );
}
}
else if( command.equalsIgnoreCase( "list" ) )
{
Set users = ChatServer.theServer.getUsers();
StringBuffer sb = new StringBuffer();
for( Iterator i=users.iterator(); i.hasNext(); )
{
String user = ( String )i.next();
if( !user.equalsIgnoreCase( this.username ) )
{
sb.append( user + "," );
}
}
if( sb.length() > 1 )
{
out.println( "LIST " + sb.substring( 0, sb.length() - 1 ) );
}
else
{
out.println( "LIST You are the only user online" );
}
}
else if( command.equalsIgnoreCase( "emot" ) )
{
String argument = line.substring( 5 );
ChatServer.theServer.sendEmotion( this.username, argument );
}
}

// Flush the buffer


out.flush();
return true;
}

/**
* A user sent, or broadcasted, the specified message to the group
*/
public void sendMessage( String from, String message )
{
out.println( "MESG " + from + ":" + message );
out.flush();
}

/**
* A user sent an emotion to the chat server
*/
public void sendEmotion( String from, String message )
{
out.println( "EMOT " + from + ":" + message );
out.flush();
}

/**
* This user has received a private message from the specified user
*/
public void privateMessage( String from, String message )
{
out.println( "PRIV " + from + ":" + message );
out.flush();
}

/**
* Notification that the specified user has entered the chat
*/
public void newUser( String otherUser )
{
out.println( "USER " + otherUser );
out.flush();
}

/**
* Notification that the specified user has left the chat
*/
public void removeUser( String otherUser )
{
out.println( "RUSR " + otherUser );
out.flush();
}

/**
* Displays help info
*/
private void showHelp()
{
StringBuffer sb = new StringBuffer();
sb.append( "COMMAND FORMAT: <four-letter-command> <argument>\r\n" );
sb.append( " CHAT COMMAND SUMMARY:\r\n" );
sb.append( " USER <username>\r\n" );
sb.append( " HELP\r\n" );
sb.append( " EXIT\r\n" );
sb.append( " SEND <message>\r\n" );
sb.append( " PRIV <recipient>:<message>\r\n" );
sb.append( " EMOT <emotion message>\r\n" );
sb.append( " MANAGEMENT COMMAND SUMMARY:\r\n" );
sb.append( " LIST\r\n" );
sb.append( " NOTIFICATION SUMMARY\r\n" );
sb.append( " MESG <user>:<message>\r\n" );
sb.append( " PRIV <user>:<message>\r\n" );
sb.append( " EMOT <user>:<message>\r\n" );
sb.append( " USER <user>\r\n" );
sb.append( " RUSR <user>\r\n" );

out.println( sb.toString() );
out.flush();
}
}

The handleRequest() method is the entry-point into this class from the RequestThread.
It operates as follows:

1. Obtain a BufferedReader and PrintWriter with which to communicate with the


client.
2. Read a line from the client.
3. Extract a 4-character command from the line.
4. Pass the command name and the line to the handleCommand() method.
5. Read the next line from the client, and
6. Repeat.

The exit condition for this server, as we'll see later, is a boolean condition set by the
result of the handleCommand() method. The exit condition is satisfied when the user
enters the command EXIT.

The handleCommand() method implements the logic of the chat server. The following
summarizes the commands that it supports:

• USER: logs in a new user by asking the ChatServer to add a new client; this
validates that the user's name is not already in use.
• HELP: displays the help menu (writes directly back to the socket).
• EXIT: asks the ChatServer to remove this user from its chat client list and then
returns false to cause the handleRequest() loop to cease.
• SEND: sends a message to all users in the chat server by calling the ChatServer's
sendMessage() method.
• PRIV: sends a private message to a specific user by calling an alternate version of
the ChatServer's sendMessage() method.
• LIST: retrieves a list of the current users in the chat server by calling the
ChatServer's getUsers() method.
• EMOT: sends an emotion to all users in the chat server by calling the
ChatServer's sendEmotion() method.

And then the ChatRequestHandler also handles notifications of these messages from the
ChatServer, generated by other ChatRequestHandler instances. The following
summarizes these notifications:

• sendMessage(): sends the message to our user; the notification command is


MESG.
• sendEmotion(): tells our user that the specified user has sent an emotion to the
group; the notification command is EMOT.
• privateMessage(): sends the private message to our user; the notification
command is PRIV.
• newUser(): notifies our user that the specified new user has just joined the chat;
the notification command is USER.
• removeUser(): notifies our user that the specified user has left the chat; the
notification command is RUSR.

For completeness, listing 9 shows the code for the DuplicateLoginException class.

Listing 9. DuplicateLoginException.java

package com.javasrc.server.chat.exception;

public class DuplicateLoginException extends Exception


{
public DuplicateLoginException( String username )
{
super( "User already logged in: " + username );
}
}

Because we do not yet have a chat client, the best way to test the chat server is using a
telnet program. The following is a sample telnet session with the Chat Server. Note that
you have to specify the port that you want to connect to using telnet; otherwise it will
connect on the default port (25).

telnet localhost 9988

And the conversation is as follows:

Welcome to the JavaSRC ChatServer!


USER userOne
SUCCESS User userOne logged in
LIST
LIST userTwo
SEND Hi guys...
MESG userTwo:Hi One..
PRIV userTwo:What’s up?
PRIV userTwo:not much...
EMOT tired...
RUSR userTwo
EXIT
Goodbye

In this example, the bold commands are the ones that I sent; the non-bold ones are
responses and notifications. In this conversation, "userOne" logged on, listed the current
users, sent a message, received a message, sent a private message, received a private
message, sent an emotion, saw that "userTwo" left, and then exited. It’s a simple
conversation that could be presented very well inside of a robust chat client, but it
suffices for demonstration purposes.

Summary

Thus far we have looked at creating the infrastructure for a robust Java server, which
inclues the introduction of a request queue that is serviced by a set of request threads, and
then built two examples on top of this infrastructure: a simple echo server and a more
complex chat server. These examples illustrate that the JavaSRC server framework
allows you to focus more on business logic and less on the implementation details behind
building a robust server.

Das könnte Ihnen auch gefallen