VidWorks Entertainment New Developer Training Program
Tutorial #8
Now, before we get started talking about networks, I'd just like to briefly mention that there are generally two transport protocols that are in use today. (For those who are already familiar with networks, you can go ahead and skip to the actual tutorial sections.) There's TCP (Transmission Control Protocol) and UDP (User Datagram Protocol, also sometimes jokingly called Unreliable Datagram Protocol). Today, we'll show you how to make server and client pairs that use TCP and UDP.
In general, TCP is more reliable--it guarantees that all packets you send out arrive at the destination, and that they arrive in the correct order. However, because of these guarantees (and what TCP has to do in order to honor these guarantees), TCP has decreased performance. TCP is usually used when the content of every packet is important, like in chat programs, web browsing, or file transfers. UDP, on the other hand, sends its packets willy-nilly and doesn't care whether or not they arrive or if they get thrown out of order. UDP, however, while unreliable, generally has better performance and is usually used in applications where performance is more important than the data, such is in streaming video or audio or in real-time online games.
You might have heard of IP (Internet Protocol). This is a network protocol. Both TCP and UDP run on top of this (hence the popular term, TCP/IP). I'd go into more detail, but that's outside the scope of this tutorial.
TCP Connections - Sockets and ServerSockets
In Java, all TCP connections are handled with objects called Sockets. When two computers are connected, they each have their own Socket, and these two Sockets are exclusively connected to each other. If one of the computers wants to connect to another computer, it will need to open a new Socket.
In order to send messages down a Socket, you'll need its OutputStream (usually wrapped in a PrintWriter). You can use this code to get the PrintWriter for a Socket:
new PrintWriter(socket.getOutputStream(),
true)
where the true is for auto-flushing the buffer on a print() or println().
In order to receive messages from a Socket, you'll need its InputStream (usually wrapped in an InputStreamReader wrapped in a BufferedReader). You can use this code to get the BufferedReader for a Socket:
new BufferedReader(new
InputStreamReader(socket.getInputStream())
Now, one method of creating a Socket is to use it's constructor:
Socket(string host, int port)
Unfortunately, this only works when the host is already listening for a connection request. If you want your machine to listen for connection requests (we'll call this machine a server, because that generally describes a server), we'll need to use a special kind of socket, called a ServerSocket.
Here's the ServerSocket constructor that we're interested in:
ServerSocket(int port)
Then, in order to listen for a connection, you can use this ServerSocket member method:
Socket
accept()
accept() listens for an incoming connection request, such as the one generated when you call the Socket constructor. It then creates a new Socket, connects the new Socket to the one generating the request over the network, and returns the Socket. This is what allows us to connect Sockets together.
Also, after accepting a connection, the ServerSocket is still available to accept connections, so you only need one ServerSocket to handle as many clients as you like. (Note that in our example, we still only accept one connection.)
Anyways, you now know how to create Sockets, connect them together, and communicate with them. It's time for our example:
/* TCPServer.java */ import java.io.*; import java.net.*; public class TCPServer { public
static void main(String[] args) { //
set default server port int serverPort = 13000; //
if specified, get server port from command-line if
(args.length >= 1) { try
{ int temp = Integer.parseInt(args[0]); serverPort = temp; }
catch (NumberFormatException e) { System.out.println("Invalid port number!"); System.out.println("Setting port number to default 13000."); } } try
{ //
open a socket and wait for a connection ServerSocket server = new ServerSocket(serverPort); Socket
connection = server.accept(); System.out.println("Client connected."); //
create readers and writers BufferedReader netin = new
BufferedReader(new InputStreamReader(connection.getInputStream())); PrintWriter netout = new
PrintWriter(connection.getOutputStream(),
true); //
read from the socket and echo back to the client String
str = null; while
((str = netin.readLine())
!= null) { System.out.println("Received: " + str); str = "You said, \"" + str
+ "\""; System.out.println("Sending: " + str); netout.println(str); } //
this means the client closed the connection; quit System.out.println("Client has disconnected."); System.out.println("Shutting down server."); netin.close(); netout.close(); connection.close(); server.close(); System.out.println("Goodbye!"); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
/* TCPClient.java */ import java.io.*; import java.net.*; public class TCPClient { public
static void main(String[] args) { //
set default hostname and portnum String
host = "localhost"; int port = 13000; //
if specified, get hostname and portnum from
command-line if
(args.length >= 2) { host
= args[0]; try
{ int temp = Integer.parseInt(args[1]); port
= temp; }
catch (NumberFormatException e) { System.out.println("Invalid port number!"); System.out.println("Setting port number to default 13000."); } } try
{ //
connect to the server Socket
socket = new Socket(host, port); System.out.println("Connected to server."); //
create readers and writers BufferedReader netin = new
BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter netout = new
PrintWriter(socket.getOutputStream(),
true); BufferedReader keyin = new BufferedReader(new
InputStreamReader(System.in)); String
str = null; while
(true) { //
read from the keyboard System.out.flush(); if
((str = keyin.readLine()).toLowerCase().equals("quit")) { break; } //
send keyboard input to server netout.println(str); //
read from server System.out.println("Received: " + netin.readLine()); } //
disconnect from server System.out.println("Disconnecting from server."); netin.close(); netout.close(); socket.close(); keyin.close(); System.out.println("Goodbye!"); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
Note, run the server before you run the client. You can run these two on the same machine by specifying the server as "localhost" on the client. Also, note that you have command-line options:
java TCPServer [port num]
java TCPClient [host name] [port num]
In this example, the client connects to the server. Then, the user types stuff in, which the client sends to the server, and the server echoes it back. The user can continue this as many times as he or she likes, until the user types "quit". Then, the client will disconnect and the server will shut down.
Anyways, here's some sample output:
Server output:
>java TCPServer
Client connected.
Received: This is a test.
Sending: You said, "This is a test."
Received: Hurray! It works!
Sending: You said, "Hurray! It works!"
Received: Now, I'm going to log out.
Sending: You said, "Now, I'm going to log
out."
Client has disconnected.
Shutting down server.
Goodbye!
>_
Client output:
>java TCPClient
Connected to server.
This is a test.
Received: You said, "This is a test."
Hurray! It works!
Received: You said,
"Hurray!
It works!"
Now, I'm going to log out.
Received: You said, "Now, I'm going to log
out."
quit
Disconnecting from server.
Goodbye!
>_
UDP Connections - DatagramSockets and DatagramPackets
UDP Connections follow a similar paradigm. Like TCP, they use sockets to communicate to each other, except in Java's UDP implementation, they are called DatagramSockets. Unlike TCP, no connection is actually made between the two communicating machines. Each machine only needs one DatagramSocket that can send messages to any other machine and can receive messages from any other machine.
"How does the DatagramSocket know where to send the data to?" you might ask. Well, that's because the DatagramPacket, the unit of information in UDP, contains the address and port of the sender and recipient, so in a way, the Datagrams route themselves.
Anyways, here's how to construct a DatagramPacket:
DatagramPacket(byte[] buf, int length) - this method is useful for creating black DatagramPackets to receive data with
DatagramPacket(byte[] buf, int length, InetAddress address, int port) - use this constructor for when you want to send a Datagram to the given address and port
Also, DatagramPacket has these useful member methods:
InetAddress getAddress() - if the Datagram is being sent, it returns the recipient; if it's being received, it returns the sender; in short, it always returns the other machine
int getPort() - like getAddress(), this method always returns the port number of the other machine
byte[] getData()
int getOffset()
int getLength()
The last three methods are quite useful for converting a byte array to a string, because strings have the following constructor:
String(byte[] data, int offset, int length)
And a string can be converted back into a byte array using the String member method:
byte[] getBytes()
Another thing you might be wondering about are those InetAddresses. These are basically a Java representation of Internet addresses. You can use the static InetAddress method:
static InetAddress getByName(String
host)
where the string "host" is either the IP address of the host or the domain name (such as java.sun.com).
Enough about DatagramPackets. Let's talk about DatagramSockets. There's no point in having packets if you don't have a socket to send them on.
DatagramSockets have the following constructors:
DatagramSocket() - opens a DatagramSocket on any available port
DatagramSocket(int port) - a better idea, since it's generally a good idea to have a well-known port number so that people can connect to you
DatagramSockets also have these useful member methods:
void send(DatagramPacket p) - sends a DatagramPacket p
void receive(DatagramPacket p) - receives a datagram and stores it in the blank DatagramPacket p
void close() - closes the DatagramSocket
Anyways, you know now enough about Java UDP programming to understand an example:
/* UDPServer.java */ import java.io.*; import java.net.*; public class UDPServer { public
static void main(String[] args) { //
set default server port int serverPort = 13010; //
if specified, get server port from command-line if
(args.length >= 1) { try
{ int temp = Integer.parseInt(args[0]); serverPort = temp; }
catch (NumberFormatException e) { System.out.println("Invalid port number!"); System.out.println("Setting port number to default 13010."); } } try
{ //
open a datagram socket DatagramSocket socket = new DatagramSocket(serverPort); System.out.println("Server started, waiting for message."); //
create a buffer to receive data byte[]
buffer = new byte[256]; DatagramPacket packet = new DatagramPacket(buffer,
buffer.length); String
str = null; InetAddress addy = null; int port = 0; byte[]
output = null; while
(true) { //
receive the packet socket.receive(packet); str = new String(packet.getData(), packet.getOffset(), packet.getLength()); System.out.println("Received: " + str); //
echo back to sender str = "You said, \"" + str
+ "\""; System.out.println("Sending: " + str); output
= str.getBytes(); addy = packet.getAddress(); port
= packet.getPort(); packet
= new DatagramPacket(output, output.length, addy, port); socket.send(packet); } }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
/* UDPClient.java */ import java.io.*; import java.net.*; public class UDPClient { public
static void main(String[] args) { //
set default hostname and portnum String
host = "localhost"; int serverPort = 13010; int myPort = 13020; //
if specified, get hostname and portnum from
command-line if
(args.length >= 3) { host
= args[0]; try
{ int temp = Integer.parseInt(args[1]); serverPort = temp; }
catch (NumberFormatException e) { System.out.println("Invalid server port number!"); System.out.println("Setting server port number to default
13010."); } try
{ int temp = Integer.parseInt(args[2]); myPort = temp; }
catch (NumberFormatException e) { System.out.println("Invalid client port number!"); System.out.println("Setting client port number to default
13020."); } } try
{ //
get the InetAddress of the server InetAddress serverAddy = InetAddress.getByName(host); //
open a datagram socket DatagramSocket socket = new DatagramSocket(myPort); //
create a buffer to receive data byte[]
buffer = new byte[256]; //
create standard input reader BufferedReader keyin = new
BufferedReader(new InputStreamReader(System.in)); String
str = null; byte[]
output = null; DatagramPacket packet = null; while
(true) { //
read from the keyboard System.out.flush(); if
((str = keyin.readLine()).toLowerCase().equals("quit")) { break; } //
send keyboard input to server output
= str.getBytes(); packet
= new DatagramPacket(output, output.length, serverAddy, serverPort); socket.send(packet); //
receive from server packet
= new DatagramPacket(buffer, buffer.length); socket.receive(packet); str = new String(packet.getData(), packet.getOffset(), packet.getLength()); System.out.println("Received: " + str); } //
quit (no need to disconnect) socket.close(); keyin.close(); System.out.println("Goodbye!"); }
catch (Exception e) { System.out.println(e.getMessage()); } } } |
This actually has the same behavior as the TCP example with a few differences. First of all, since the server has no concept of connections, any number of clients may send to the server and receive the echo back. Second of all, since the server has no concept of connections, it doesn't know if the clients are disconnected or if they're all just silent. Consequently, this server never shuts down (at least until the user hits Ctrl-C). Third of all, because this is UDP, you'll see packets getting dropped sometimes (depending on network activity and congestion).
Also, the command-line options are slightly different:
java UDPServer [port num]
java UDPClient [server name] [server port num] [client
port num]
Finally, some sample output:
Server output:
>java UDPServer
Server started, waiting for message.
Received: Hi, I'm Client #1.
Sending: You said, "Hi, I'm Client #1."
Received:
Sending: You said, ""
Received: Wow, my pack
Sending: You said, "Wow, my pack"
Received: This time, a partial pac
Sending: You said, "This time, a partial pac"
Received: Haha, Client
#2's packets are gettin
Sending: You said, "Haha,
Client #2's packets are gettin"
Received: D'oh! Mine are
getting dropped, too.
Sending: You said, "D'oh!
Mine are getting dropped, too."
Received: Now who's la
Sending: You said, "Now who's la"
^C
>_
Client #1 output:
>java UDPClient
Hi, I'm Client #1.
Received: You said, "Hi, I'm Client #1."
Haha, Client #2's packets are
getting dropped!
Received: You said, "Haha,
Client #2's packets are gettin"
D'oh! Mine are getting dropped,
too.
Received: You said, "D'oh!
Mine are getting dropped, too."
quit
Goodbye!
>_
Client #2 output:
>java UDPClient localhost 13010 13030
Hi, I'm Client #2.
Received: You said, ""
Wow, my packet got dropped!
Received: You said, "Wow, my pack"
This time, a partial packet got dropped.
Received: You said, "This time, a partial pac"
Now who's laughing?
Received: You said, "Now who's
la"
quit
Goodbye!
>_
Remember how I had said before that in UDP, each machine only needs on socket. This is actually very powerful, because as you add machines, the number of sockets you need grows linearly (one for each machine). On the other hand, with TCP, as you add machines, the number of sockets grows quadratically (each machine has a socket for every other machine). This means that UDP scales a lot better for networks of interconnected machines.
The alternative solution for TCP is (and this is the tradition TCP solution) to have one server that all machines connects to and communicate with, so we still have a linear number of sockets. However, the problem with this solution is that if the server goes down, your whole network is unusable. With UDP, you can have decentralized networks. Even if machines go down, if it's small compared to the entire network, you're network is still functional. No single machine is critical to the performance of the network. (Of course, you'll still need to know a machine or two to bootstrap the process and get yourself into the network.)
However, the scalability and non-centrality of UDP comes at a cost: UDP is, once again, unreliable, and machines running on UDP have no way of telling if a machine is still on the network or not other than by listening to it and assuming that silence means inactivity. Thus, when writing a UDP network, you have to build your own error checking (assuming it's important to you).
Conclusion
Anyways, enough chitchat about TCP and UDP and networking with Java. I've taught you all you need to know to do basic networking over Java.
Next time: Reflection!
For those of you who don't already know, Reflection is a way to get a class at run-time. This is a way to make programs extensible or to create user specified modules (like AI or plug-ins) without having to re-compile your program every single time. It's also the last tutorial of the series. (Awww...)
Homework Assignment!
Write your own online chat program. (Actually, this is more of a project than a homework assignment, but it's something that everyone does when they first learn how to write networking software. Either that or P2P file-sharing.) Bonus points if it uses UI and has sound effects (like AIM).