Coffee Space


Listen:

Simple Chat

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):

0001 $uname -r
0002 4.4.0-96-generic

JRE:

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

JDK:

0007 $javac -version
0008 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.

0009 import java.net.ServerSocket;
0010 import java.net.Socket;

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

0011 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).

0012   private static final int PORT = 8080;
0013   private static final int LINES = 25;
0014   private static final int LENGTH = 80;
0015   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.

0016   private static byte[][] data = new byte[LINES][];
0017   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.

0018   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.

0019   private Socket cs;
0020   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.

0021   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).

0022     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).

0023       try{

Create a new ServerSocket, ss, and our PORT.

0024         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.

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

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

0029   public Chat(Socket cs, boolean read){

Store references to the input.

0030     this.cs = cs;
0031     this.read = read;

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

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

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.

0034   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.

0035     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.

0036     int cTop = dTop - 1;

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

0037     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.

0038     try{

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

0039       while(!cs.isClosed()){

This is the case of reading (true).

0040         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.

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

If we read something…

0042           if(len > 0){

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

0043             switch(line[2]){

The ! character defines the quit command.

0044               case '!' :

We forcibly close the client Socket off.

0045                 cs.close();
0046                 break;
0047             }

Place the id character at the start.

0048             line[0] = id;

Separate the id from the message.

0049             line[1] = '>';

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

0050             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.

0051             synchronized(data){

Store the read line into our data object.

0052               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.

0053               size[dTop] = len + 2;

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

0054               dTop = dTop + 1 < LINES ? dTop + 1 : 0;
0055             }
0056           }

This is the case of writing (false).

0057         }else{

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

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

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

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

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

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

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.

0064         Thread.sleep(SLEEP);
0065       }
0066     }catch(Exception e){}
0067   }
0068 }

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

0069 import java.net.ServerSocket;
0070 import java.net.Socket;
0071  
0072 public class Chat extends Thread{
0073   private static final int PORT = 8080;
0074   private static final int LINES = 25;
0075   private static final int LENGTH = 80;
0076   private static final int SLEEP = 100;
0077   private static byte[][] data = new byte[LINES][];
0078   private static int[] size = new int[LINES];
0079   private static int dTop = 0;
0080   private Socket cs;
0081   private boolean read;
0082  
0083   public static void main(String[] args){
0084     for(;;){
0085       try{
0086         ServerSocket ss = new ServerSocket(PORT);
0087         for(;;){ (new Chat(ss.accept(), true)).start(); }
0088       }catch(Exception e){}
0089     }
0090   }
0091  
0092   public Chat(Socket cs, boolean read){
0093     this.cs = cs;
0094     this.read = read;
0095     if(read){ (new Chat(cs, false)).start(); }
0096   }
0097  
0098   public void run(){
0099     byte id = (byte)((cs.hashCode() % 26) + (int)'a');
0100     int cTop = dTop - 1;
0101     byte[] line = new byte[LENGTH + 2];
0102     try{
0103       while(!cs.isClosed()){
0104         if(read){
0105           int len = cs.getInputStream().read(line, 2, LENGTH);
0106           if(len > 0){
0107             switch(line[2]){
0108               case '!' :
0109                 cs.close();
0110                 break;
0111             }
0112             line[0] = id;
0113             line[1] = '>';
0114             line[len + 2] = '\n';
0115             synchronized(data){
0116               data[dTop] = line;
0117               size[dTop] = len + 2;
0118               dTop = dTop + 1 < LINES ? dTop + 1 : 0;
0119             }
0120           }
0121         }else{
0122           if(cTop != dTop && data[cTop + 1] != null){
0123             cTop = cTop + 1 < LINES ? cTop + 1 : 0;
0124             cs.getOutputStream().write(data[cTop], 0, size[cTop]);
0125             cs.getOutputStream().flush();
0126           }
0127         }
0128         Thread.sleep(SLEEP);
0129       }
0130     }catch(Exception e){}
0131   }
0132 }

Use

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

0133 javac Chat.java
0134 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:

0135 $ nc 127.0.0.1 8080
0136 hello
0137 w>hello
0138 i>world

And:

0139 $ telnet 127.0.0.1 8080
0140 Trying 127.0.0.1...
0141 Connected to 127.0.0.1.
0142 Escape character is '^]'.
0143 w>hello
0144 world
0145 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.