Coffee Space 
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.
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:
vim although this is overly complex fort the task at
hand.java - This is the runtime environment (JRE) used to
execute the Java byte code.javac - This is the compiler (JDK) to take the Java
source and produce the byte code for the program.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!
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.
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:
PORT - The port to run the server on, a high number so
that in most cases a non-admin can run this program.LINES - The number of vertical lines to store in server
memory.LENGTH - The maximum number of characters in each
line.SLEEP - How long to sleep on the thread before
servicing it (will make more sense later). In this case, 100ms.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 }
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.
There are multiple issues that a person should be aware of if using/starting from this system:
And these are not all… To say this is overall a complex problem is an understatement - and these are only very simple features!
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.