Coffee Space


Listen:

Simple Chat

Preview Image

Preview Image

Introduction

Let us introduce this problem: you and a small group of friends that want to communicate over the great expanse of the internet. You have a very lightweight server available and you want a highly simple program to communicate with, that happens to be understandable in 64 lines. If this is you, then oh boy do I have something for you!

This plans to be a small embark into the journey of writing very simple chat servers, more towards the style of old IRC systems but far less complicated. The program is written in java and does not include comments.

Requirements

Like the simplicity of the program, the requirements of this project are kept very simple and should work on any popular modern platform. The computer used to host the chat program can be incredibly low powered and still achieve very good performance.

You will need:

If java is installed correctly, you should be able to run java -version on the command line and you should be given a version number. Java 7 or newer should work perfectly fine as we use only simple features. Equally, you can use either Oracle's Java or OpenJDK's Java (the second one is open source).

The versions this program was tested on:

Kernel (Linux):

$uname -r
4.4.0-96-generic

JRE:

$java -version
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (build 1.8.0_131-8u131-b11-2ubuntu1.16.04.3-b11)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)

JDK:

$javac -version
javac 1.7.0_95

NOTE: Here I use an older version to compile for a newer version of Java and everything works perfectly fine, testament to Java's backwards compatibility between versions!

Design

We first start by defining how we want our system to be, with the following requirements:

To keep things simple, we will use just one class. The server will run statically from the main() method and the clients will run in different threads defined by the Chat instance.

Implementation

Here we define our implementation. For reference, the explanation comes before the code.

Firstly we import classes so that we can use our ServerSocket for listening for connections and Socket to handle them.

import java.net.ServerSocket;
import java.net.Socket;

We define our class so that it extends the Thread class, allowing us to generate instances on different threads.

public class Chat extends Thread{

Define the basic values that will be used in our program:

These are constant values (they don't change), so we give them the property final and static because they should be accessible from any context (don't waste memory or time redeclaring these values).

  private static final int PORT = 8080;
  private static final int LINES = 25;
  private static final int LENGTH = 80;
  private static final int SLEEP = 100;

Now we declare the data to be stored in the server in data, as well as the length of each line stored in size. These values are define as static because they need to be accessible from each thread.

  private static byte[][] data = new byte[LINES][];
  private static int[] size = new int[LINES];

dTop keeps track of the next index to stored chat data into. We set this value to 0 at the start, but this is arbitrary.

  private static int dTop = 0;

Next we come to the variables defined for each of the clients, where we hold a reference to the Socket of the client. read is a boolean value that defines whether the client thread is to be used for read/write properties.

  private Socket cs;
  private boolean read;

This is the entry method into the program, where we begin executing our code. We are given args which are command line arguments, although we completely ignore these in this program.

  public static void main(String[] args){

for(;;) is a smaller version of while(true) which is an infinite loop. We want our program to run indefinitely (or at least until the program is forcibly closed).

    for(;;){

Next we have our try/catch pairing that captures Exception (all types of Exception) and in our case, completely ignores the output. This effectively stops the program from ever crashing (or at least makes it very difficult).

      try{

Create a new ServerSocket, ss, and our PORT.

        ServerSocket ss = new ServerSocket(PORT);

Infinite loop (for(;;)) and create a new Chat instance to handle our client. ss.accept() blocks until a client connects and true means that we start reading first. We then perform .start() on the new Client object that in turn creates a new thread, running the run() method defined later.

In theory, we can accept as many as 65k connections from connecting clients, although the program will not be very "workable" like this.

        for(;;){ (new Chat(ss.accept(), true)).start(); }
      }catch(Exception e){}
    }
  }

The constructor for the Chat class, taking a reference to the cs (client's Socket) and read (whether to read (true) or write (false)).

  public Chat(Socket cs, boolean read){

Store references to the input.

    this.cs = cs;
    this.read = read;

If we are reading (true), then start a new client write instance (false).

    if(read){ (new Chat(cs, false)).start(); }
  }

The run() method is run on a new thread when we run .start() on the Chat instance. This function overrides the function declared in Thread which we extend in the Chat class.

  public void run(){

We define the id character as some semi-random value (defined by the hash of the client Socket), between a and z. There is a 1 in 26 chance that two clients will have the same id, but we don't care so much. The only purpose is to try and make client's slightly individually defined.

    byte id = (byte)((cs.hashCode() % 26) + (int)'a');

cTop defines the top of what the client is aware of. This should only ever be the same as dTop or behind, meaning that we need to send data to one of our clients.

    int cTop = dTop - 1;

We define an array to contain the data read from the client, called line.

    byte[] line = new byte[LENGTH + 2];

Here we have a try/catch for the purpose of capturing any failures in reading/writing from the client Socket. The same as before, we completely ignore any errors and keep rolling with it.

    try{

Here we want to keep going until the client disconnects from the server, at which point we can just ignore them.

      while(!cs.isClosed()){

This is the case of reading (true).

        if(read){

len contains the number of bytes read, which should be greater than zero if we read some bytes and less than LENGTH. We read into line at an offset of 2.

          int len = cs.getInputStream().read(line, 2, LENGTH);

If we read something...

          if(len > 0){

Here we process commands, by looking at the first byte (offset by 2).

            switch(line[2]){

The ! character defines the quit command.

              case '!' :

We forcibly close the client Socket off.

                cs.close();
                break;
            }

Place the id character at the start.

            line[0] = id;

Separate the id from the message.

            line[1] = '>';

Place \n at the end so that it prints a new line in the client's display.

            line[len + 2] = '\n';

Here some java magic happens with the synchronized keyword. This means that anybody who comes through here must wait for anybody else who is also using this object, data. The case we want to avoid is two threads writing to the data, size and dTop objects.

            synchronized(data){

Store the read line into our data object.

              data[dTop] = line;

Store the length (plus the id and separator characters) in the size array so we know how much to read in the future.

              size[dTop] = len + 2;

Update the dTop pointer, bring back to zero if we overrun.

              dTop = dTop + 1 < LINES ? dTop + 1 : 0;
            }
          }

This is the case of writing (false).

        }else{

If cTop is behind, we need to send our client some information.

          if(cTop != dTop && data[cTop + 1] != null){

Update the position of cTop (like we did to dTop).

            cTop = cTop + 1 < LINES ? cTop + 1 : 0;

Send it and flush it out so that we can forget about it.

            cs.getOutputStream().write(data[cTop], 0, size[cTop]);
            cs.getOutputStream().flush();
          }
        }

Pause the loop - otherwise we will just make our CPU very hot for little performance gain. It's possible for the thread to be randomly woken up, but this case is usually very unlikely and of no issue to our program.

        Thread.sleep(SLEEP);
      }
    }catch(Exception e){}
  }
}

And here is a version you can just copy and paste easily:

import java.net.ServerSocket;
import java.net.Socket;
 
public class Chat extends Thread{
  private static final int PORT = 8080;
  private static final int LINES = 25;
  private static final int LENGTH = 80;
  private static final int SLEEP = 100;
  private static byte[][] data = new byte[LINES][];
  private static int[] size = new int[LINES];
  private static int dTop = 0;
  private Socket cs;
  private boolean read;
 
  public static void main(String[] args){
    for(;;){
      try{
        ServerSocket ss = new ServerSocket(PORT);
        for(;;){ (new Chat(ss.accept(), true)).start(); }
      }catch(Exception e){}
    }
  }
 
  public Chat(Socket cs, boolean read){
    this.cs = cs;
    this.read = read;
    if(read){ (new Chat(cs, false)).start(); }
  }
 
  public void run(){
    byte id = (byte)((cs.hashCode() % 26) + (int)'a');
    int cTop = dTop - 1;
    byte[] line = new byte[LENGTH + 2];
    try{
      while(!cs.isClosed()){
        if(read){
          int len = cs.getInputStream().read(line, 2, LENGTH);
          if(len > 0){
            switch(line[2]){
              case '!' :
                cs.close();
                break;
            }
            line[0] = id;
            line[1] = '>';
            line[len + 2] = '\n';
            synchronized(data){
              data[dTop] = line;
              size[dTop] = len + 2;
              dTop = dTop + 1 < LINES ? dTop + 1 : 0;
            }
          }
        }else{
          if(cTop != dTop && data[cTop + 1] != null){
            cTop = cTop + 1 < LINES ? cTop + 1 : 0;
            cs.getOutputStream().write(data[cTop], 0, size[cTop]);
            cs.getOutputStream().flush();
          }
        }
        Thread.sleep(SLEEP);
      }
    }catch(Exception e){}
  }
}

Use

To compile, we save the code as Chat.java and run the following to compile and then run the code:

javac Chat.java
java Chat

The program should now just wait - it's listening!

How do I connect to the server? You can simply use telnet or nc (netcat) to achieve this, for example:

$ nc 127.0.0.1 8080
hello
w>hello
i>world

And:

$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
w>hello
world
i>world

127.0.0.1 is the IP address, in this case it's local because the program is running on the machine. The 8080 is the port to connect to, as defined in the code previously. You can clearly see the two clients are communicating via the server, one via nc and the other via telnet.

Conclusion

Limitations

There are multiple issues that a person should be aware of if using/starting from this system:

  • Anyone can pretend to be anyone else. There is a 1 in 26 chance that somebody could end up with the same letter as somebody else. Using IPs is possible, but IPs are not fixed length and longer, but also with the IPV4 space running low on addresses somebody could still be talking to somebody else on the same address.
  • Communication is unencrypted - it can be read and changed at an attackers will.
  • This system is very easy to block/attack. A simple script could easily lock all other clients out of the system.
  • Too many clients sending messages at the same time could cause messages to not be delivered.
  • Too many clients could consume all available ports on the host server.

And these are not all... To say this is overall a complex problem is an understatement - and these are only very simple features!

Overview

Well, we built a simple chat server that uses minimal resources. You could use it online and not have to worry too much, as long as the host is correctly patched. If all you want to achieve is some simple communications with friends for a while, this would suffice. If you wanted to do this on a private network, potentially this could work indefinitely.