The two important protocols in the transport layer are the User Datagram Protocol (UDP) and the Transmission Control Protocol (TCP). UDP is the connectionless protocol while TCP is the connection-oriented protocol.
In a connection-oriented protocol processes exchange bytes using stream-mode sockets. A stream-mode socket, s, consists of two streams, an input stream (s.in) and an output stream (s.out). Assume processes A and B want to have a conversation, then A and B will each need a stream-mode socket, A.s and B.s. Furthermore, A.s.out must be connected to B.s.in and B.s.out must be connected to A.s.in:
Think of a socket as a telephone. For A to call B, both need phones (sockets). A needs to know the number (address) of B's phone, the transmitter (output stream) of A's phone must be connected to the receiver (input stream) of B's phone, and the receiver of A's phone (input stream) must be connected to the transmitter of B's phone (output stream).
class java.net.Socket; // stream-mode socket
class java.net.ServerSocket; // connection request socket
Here are the import statements most of our files will need:
import java.util.*;
import java.io.*;
import java.net.*;
import jutil.*;
A correspondent has a socket. The input and output streams of the socket are filtered by readers and writers, respectively:
class Correspondent {
protected Socket sock;
protected BufferedReader sockIn;
protected PrintWriter sockOut;
public Correspondent() { } // init
fields later
public Correspondent(Socket s) {
sock = s;
initStreams();
}
Let's convert the input and output streams of our sockets into something a bit more user friendly:
protected void initStreams()
{
try {
sockIn = new BufferedReader(
new InputStreamReader(
sock.getInputStream()));
sockOut = new PrintWriter(
sock.getOutputStream(),
true);
} catch(IOException e) {
System.err.println(e.getMessage());
}
}
Subclasses can close streams without worrying about exceptions:
protected void close() {
try {
sock.close();
} catch(IOException e) {
System.err.println(e.getMessage());
}
}
This method is used to request a connection to a new server:
protected void requestConnection(String
host, int port) {
try {
sock = new Socket(host,
port);
initStreams();
} catch(UnknownHostException
uhe) {
System.err.println("unknown
host " + uhe);
System.exit(1);
} catch(IOException ioe) {
System.err.println("failed
to create streams " + ioe);
System.exit(1);
}
}
Basic send and receive:
public void send(String
msg) {
sockOut.println(msg);
}
public String receive() {
String msg = null;
try {
msg = sockIn.readLine();
} catch(Exception e) {
System.err.println(e.getMessage());
System.exit(1);
}
return msg;
}
} // Correspondent
A client perpetually:
1. reads user's command
2. sends the command to the server
3. receives the server's response
4. displays response
Of course the client is a correspondent:
public class SimpleClient extends Correspondent {
protected BufferedReader stdin;
protected PrintWriter stdout;
protected PrintWriter stderr;
The constructor sets up the standard input, output, and error streams. It also requests a connection to the server:
public
SimpleClient(String host, int port) {
requestConnection(host,
port);
stdout = new PrintWriter(
new BufferedWriter(
new
OutputStreamWriter(System.out)), true);
stderr = new PrintWriter(
new BufferedWriter(
new
OutputStreamWriter(System.out)), true);
stdin = new BufferedReader(
new
InputStreamReader(System.in));
}
Our basic read-send-receive-display control loop:
public void controlLoop()
{
while(true) {
try {
stdout.print("->
");
stdout.flush();
String msg =
stdin.readLine();
if (msg == null) continue;
if
(msg.equals("quit")) break;
stdout.println("sending:
" + msg);
send(msg);
msg = receive();
stdout.println("received:
" + msg);
} catch(IOException e) {
stderr.println(e.getMessage());
break;
}
}
send("quit");
stdout.println("bye");
}
Users can specify the location of the server on the command line:
public static void
main(String[] args) {
int port = 5555;
String host =
"localhost";
if (1 <= args.length) {
port =
Integer.parseInt(args[0]);
}
if (2 <= args.length) {
host = args[1];
}
SimpleClient client = new
SimpleClient(host, port);
client.controlLoop();
}
}
The server perpetually listens for incoming client requests using a server socket:
public class Server {
protected ServerSocket mySocket;
protected int myPort;
protected Class handlerType;
public Server() { this(5555); }
public Server(int port) { this(port,
"RequestHandler"); }
public Server(int port, String htp) {
try {
myPort = port;
mySocket = new
ServerSocket(myPort);
handlerType = Class.forName(htp);
} catch(Exception e) {
System.err.println(e.getMessage());
System.exit(1);
} // catch
}
The listener perpetually calls:
socket = mySocket.accept();
This causes the server to block until a request arrives from the client. This happens when the client calls:
Correspondent.requestConnection()
When the accept() method returns, it returns a server-side socket connected to the client-side socket. Next, the server creates a slave thread to correspond with the client. This is necessary to keep the response time of the server reasonable. The slave thread is an instance of the RequestHandler class:
public void listen() {
System.out.println("server
address: " +
mySocket.getInetAddress());
try {
while(true) {
System.out.println("Server
listening at port " + myPort);
Socket socket =
mySocket.accept(); // blocks
RequestHandler handler =
makeHandler(socket);
if (handler == null) continue;
Thread slave = new
Thread(handler);
slave.start();
} // while
} catch(IOException ioe) {
System.err.println("Failed
to accept socket, " + ioe);
System.exit(1);
} // catch
}
The field:
protected Class handlerType;
indicates the subclass of RequestHandler that slaves actually instantiate. This field was initialized by the constructor using the name of the class. The request handler factory uses reflection to create a new instance of this class, then initializes its socket to be the server-side socket, which is already connected to the client-side socket:
public RequestHandler
makeHandler(Socket s) {
RequestHandler handler = null;
try {
handler = (RequestHandler)
handlerType.newInstance();
handler.setSocket(s);
} catch(Exception e) {
System.err.println(e.getMessage());
}
return handler;
}
Users can specify the port and request handler subclass at the command line:
public static void
main(String[] args) {
int port = 5555;
String service =
"RequestHandler";
if (1 <= args.length) {
port = Integer.parseInt(args[0]);
}
if (2 <= args.length) {
service = args[1];
}
Server server = new Server(port,
service);
server.listen();
}
}
A request handler is a correspondent and runs in its own thread of control. It perpetually:
1. receives a command from the client
2. creates a response to the command
3. sends the response back to the client
Notice that the current implementation of response simply echoes the command back to the client. Naturally, this can be overridden in a subclass:
class RequestHandler extends Correspondent implements Runnable {
public RequestHandler(Socket s) {
super(s); }
// override in a subclass:
protected String response(String msg) {
return "echo: " + msg;
}
public void run() {
while(true) {
String msg = receive();
System.out.println("received:
" + msg);
//if (msg == null) continue;
if (msg.equals("quit"))
break;
msg = response(msg);
System.out.println("sending:
" + msg);
send(msg);
}
close();
System.out.println("request
handler shutting down");
}
}
As an example, we extend the RequestHandler and override the response method so it executes commands of the form:
add NUMBER ... NUMBER
mul NUMBER ... NUMBER
sub NUMBER NUMBER
div NUMBER NUMBER
The response method splits the users command into an array of tokens (i.e., strings that don't contain white space characters,) starting from position 1, parses tokens into numbers, then performs the arithmetic combination required by the first token:
class CommandProcessor extends RequestHandler {
protected String response(String msg) {
String[] tokens =
msg.split("\\s");
double[] args = new
double[tokens.length - 1];
for(int i = 1; i < tokens.length;
i++) {
try {
args[i - 1] =
Double.parseDouble(tokens[i]);
} catch(NumberFormatException
nfe) {
return "arguments must be
numbers";
}
}
if
(tokens[0].equals("add")) return "" + add(args);
if (tokens[0].equals("mul"))
return "" + mul(args);
return "unrecognized command:
" + tokens[0];
}
Here's a sample helper method for adding numbers. The helper methods for the other arithmetic operators are similar:
private double
add(double[] args) {
double result = 0;
for (int i = 0; i < args.length;
i++) {
result += args[i];
}
return result;
}
// etc.
} // CommandProcessor
Note all classes except for the command processor are in the jutil package.