TcpJLib
 
 

 

Computer Nodes Interlinked by TCP/IP

Running Behind Firewalls


The most common way to interlink computers via the Internet is using the TCP/IP protocol. Java's core API is well-designed for developing standard socket-based client-server applications, so an extra library layer for TCP/IP communication seems to be an overkill. The situation becomes more complicated if the hosts are protected by firewalls. In a typical situation (computers in a campus PC pool, inside an enterprise building, etc.) the clients are restricted solely to outgoing HTTP connections via IP port 80. The TcpJLib framework presented here allows you to interlink hosts located in a firewall-protected site and develop typical stream-based multitclient-server applications using a HTTP tunneling technique: A server using port 80 acts as a relay between nodes somewhere else, even if they are protected by firewalls. Seen from the network, the relay server looks like a standard HTTP server and the nodes connects as standard HTTP clients.

tcpjlib3

Standard socket connections use streams for transfering data. After a TCP client connects to a server, input and output streams remain open until the socket is closed. Using the HTTP protocol here causes another problem: The HTTP protocol is stateless, e.g. that a client performs a HTTP GET request to the server and the server opens a socket connection. Now the server replies by sending some data back to the requesting client and closes the socket. The socket may also be closed by the firewall that protects the server and admits only traffic that conforms to the HTTP protocol. With TcpJLib the relay server and the nodes are connected by a continuously opened up and down stream as long as you like. This is realized by feinting the network as if a long WAV file were up- and downloaded. Keep-alive data is transferred at an interval of about 1 second completely transparent to the user. Moreover this trick allows the relay to detect if a node fades away. The relay will send connecting and disconnecting messages down to all TcpNodes as a status notification. The nodes are identified by a nickname that they send to the relay while connecting. The relay handles name clashs by appending number tags.

Despite, seen from the network, a TcpJLib node is a HTTP client, logically it may act as a server as well as a client. We call the node a bridge when it acts as a server and call it an agent when it acts as a client. Obviously using a bridge and any number of agents you can develop a multi-client/server topology completely free of firewall constraints.

tcpjlib2

 

The relay server is part of the TcpJLib distribution. It is session-oriented, e.g. any number of bridge/clients-applications (up to a maximum for security reasons) may use its service in parallel by specifying a unique session ID. The relay IP must be known to all participants, so it should run with a fixed IP address or with a fixed IP alias that is DNS mapped to the current IP address. When you use the TcpJLib library in default mode, you are automatically linked through our relay server clab3.phbern.ch located in Bern, Switzerland.

TcpJLib is specially designed to develop multiuser game application with one game server and several game clients. Because for security reasons we do not want to relay binary data with high bandwidth, TcpJLib is not designed for game applications where updating the game state needs high data rates. In a typical game application suitable for TcpJLib, only few data is transferred each time the player makes a move. Despite the data streams are continuously open, we restrict the data type to ASCII character arrays (ASCII strings terminated with newline character). For many game applications the game state may be transferred using a command tag followed by some state values. Because both may be easily coded using integers, TcpJLib provides the classes TcpBridge and TcpAgent specially designed for integer data transfer.

 

Design principle explained by examples

The first example shows how extremely simple it is to exchange data between nodes connected in the same session. A node connects to the TcpRelay by calling connect() passing a session ID and a nickname. The relay opens a new session if there is no other node in the same session. After connection the node sends a line of text by calling sendMessage(). This message is sent to all nodes connected in the same session. When a node receives a message or a status information, the callback messageReceived() resp. statusReceived() is triggered.

import ch.aplu.tcp.*;
import ch.aplu.util.*;

public class SendLine extends Console
{
  // -------------- Inner class MyTcpNodeAdapter --------
  private class MyTcpNodeAdapter extends TcpNodeAdapter
  {
    public void messageReceived(String sender, String text)
    {
      println("Message from " + sender + ": " + text);
    }

    public void statusReceived(String text)
    {
      println("Status: " + text);
    }
  }
  // -------------- End of inner class ------------------

  private final String sessionID = "a17&&32";
  private final String nickname = "Max";
  
  public SendLine()
  {
    TcpNode tcpNode = new TcpNode();
    tcpNode.addTcpNodeListener(new MyTcpNodeAdapter());
    tcpNode.connect(sessionID, nickname);
    setTitle("Enter line to sent to all connected nodes");
    while (true)
      tcpNode.sendMessage(readLine());
  }

  public static void main(String[] args)
  {
    new SendLine();
  }
}

Execute the program locally using WebStart. You may start several program instances from the same or different computers with the same session ID and nickname.

Remarks:

  • When the same nickname is used, the relay resolves the name clash by appending a (n) trailer to the connecting node's nickname.
  • When a new node enters the the same session or a node dies, a status message is sent to all connected nodes.
  • Nothing happens if you send the data while you are the only node in the session. The data is not buffered somewhere, it just gets lost.
  • You don't need to close the connection explicitely when terminating the program. Because the relay and the node continuously exchange hidden keep-alive data, the relay gets automatically informed when the node dies and passes this information to all remaing nodes.
  • It is very important to keep in mind that due to network delays and different machine architectures the classical sequential thinking must be reconsidered: When node A sends a message to node B and shortly after (maybe in the next statement) a message to node C, you are not sure that the message arrives at node B before the message arrives at node C. If A must be sure that both nodes got the messages before continuing, B and C must send a confirmation message back to A and A must wait until both replies arrived.

 

In the following slightely more complicated example 10 random numbers are sent to all nodes whenever the user of any node strikes a key.

import ch.aplu.tcp.*;
import ch.aplu.util.*;

public class SendInteger extends TcpNode
  implements TcpNodeListener
{
  private final static int nb = 10;
  private final String myNodeName = "Richard";
  private Console c = new Console();

  public SendInteger()
  {
    c.print("Enter a unique session ID: ");
    String sessionID = c.readLine();
    addTcpNodeListener(this);
    c.println("Connecting to relay '" + getRelay() + "'...");
    connect(sessionID, myNodeName, 2, 2, 2);
    // Halt the thread until connected or timeout
    Monitor.putSleep(4000);
    if (getConnectState() != TcpNodeState.CONNECTED)
      c.println("Connection failed (timeout).");
    else
    {
      while (true)
      {
        c.println("Press any key to send data...");
        c.getKeyWait();
        c.println("Sending " + nb + " doubles...");
        StringBuffer sb = new StringBuffer();
        double sum = 0;
        for (int i = 0; i < nb; i++)
        {
          double r = Math.random();
          sum += r;
          sb.append(r + "&");
        }
        sendMessage(sb.toString());
        c.println("sum: " + sum);
      }
    }
  }

  public void nodeStateChanged(TcpNodeState state)
  {
    if (state == TcpNodeState.CONNECTED)
    {
      c.println("Connection establised.");
      Monitor.wakeUp();
    }
    if (state == TcpNodeState.DISCONNECTED)
      c.println("Connection broken.");
  }

  public void messageReceived(String sender, String text)
  {
    String[] s = TcpTools.split(text, "&");
    int nb = s.length;
    double sum = 0;
    c.println("Received from '" + sender + "':");
    for (int i = 0; i < nb; i++)
    {
      double r = Double.parseDouble(s[i]);
      sum += r;
      c.println(r);
    }
    c.println(nb + " doubles. Sum: " + sum);
  }

  public void statusReceived(String text)
  {
    c.println("Status received: " + text);
  }

  public static void main(String[] args)
  {
    new SendInteger();
  }
}

Execute the program locally using WebStart.

You may start any number of program instances. All nodes with the same session ID shares the data exchange. The code is almost self-explaining, but please pay attention to the following points:

  • The doubles are sent in a single message line separated by '&'. On the receiving side, TcpTools.split() is used to restore them.
  • Because connect() returns immediately, we wait until the connection is confirmed or a timeout is reached by calling putSleep(long timeout). When the connection is confirmed by a nodeStateChange notification, we let the thread continue by calling wakeUp().
  • For convenience some helper classes (Monitor, Console) from the package ch.aplu.util are used (download here).
  • If you use your own relay, you can modify some important relay parameters by editing a tcpjlib.properties file and put it into the application or the user home directory:
 

# tcpjlib.properties
# User selectable options for the TcpJLib library

# You may modify the values (but not the keys). Keep the formatting very strictly.
# The search for this file is performed in the following order:
# - Application directory (user.dir)
# - Home directory (user.home)
# - tcpjlib.jar
# As soon as the properties are found, the search is cancelled.
# So there is no need to delete files in the later search order.

# Relay internet address in dotted notation xxx.xxx.xxx.xxx or
# alias to be resolved by a DNS
# default: clab3.phbern.ch
RelayIP = clab3.phbern.ch

# Socket port used exclusively by the relay server (default: 80)
RelayPort = 80

# Unique relay ID to protect the relay from unauthorized accesses
# -- commented out, using default ID for clab3.phbern.ch
#RelayID = this_is_your_relay_ID


Using the same technique it is easy to develop a fully functional Internet chat application with any number of participants in any number of rooms. All you need in addition to TcpJLib is some basic knowledge of GUI construction using Swing. (Pay attention to use ASCII characters only.)

Start the chat client locally using WebStart.
Download the source code for the chat client. Study it, run it, improve it!

chatclient