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.