17

I need to implement a fairly simple WebSocket server in Java SE. All it needs to do is accept connections and store the respective sessions, then send a message to all connected client whenever a certain event is fired.

I cannot find a single tutorial for how to do this in regular Java SE. All of them require running with Maven, or deploying it as a WAR - which are all out of the question for this project. I need to run this as a Java SE desktop app.

The tutorials I have found show how to implement an endpoint using annotations like @OnOpen, @OnMessage, and @OnClose. However, none of them explain how to actually initialize the server. I also need to be able to specify a different port number for incoming connections.

Am I missing something? I know people have made chat apps using WebSocket, and that really should not require a web application server. I am not using Maven either, and would prefer to keep it that way for simplicity's sake.

Derek
  • 799
  • 2
  • 8
  • 17
  • 1
    Jetty has websocket support. – David Ehrmann Apr 02 '17 at 01:07
  • 2
    If Maven is the stumbling block, that's easy to solve. Simply analyse the dependencies in the POM files, manually fetch their JAR files, and manually put them on your application's classpath. Maven is just a build tool. It is automating stuff that you can do by hand ... if you are so inclined. – Stephen C Apr 02 '17 at 01:07
  • I have seen Jetty implementations, but the tutorials only show how to implement the endpoint code. What do I need to do to initialize it, i.e. what would go in the "main" method for such an application? – Derek Apr 02 '17 at 01:08
  • Google for "websocket apache httpcomponents". I don't know if you will find a tutorial, but there are hits that suggest it can be done, and possibly that may have code that you could use as examples. – Stephen C Apr 02 '17 at 01:11
  • 1
    [Bare bones webSocket server for Java](https://github.com/TooTallNate/Java-WebSocket) with a bunch of [sample apps](https://github.com/TooTallNate/Java-WebSocket/tree/master/src/main/example). – jfriend00 Apr 02 '17 at 01:53
  • tyrus is the reference implementation of the java api for websocket (JSR 356), and it seems to run in a standalone manner https://tyrus.java.net/ – nandsito Apr 10 '17 at 19:02
  • Tyrus has a standalone client, but the server is dependent on GlassFish, which is an application server. I've tried out the barebones server at https://github.com/TooTallNate/Java-WebSocket and so far it has worked, and is completely standalone. – Derek Apr 10 '17 at 19:36

4 Answers4

13

https://github.com/TooTallNate/Java-WebSocket is a full WebSockets server and client implementation in Java SE, no enterprise/web app server needed.

Derek
  • 799
  • 2
  • 8
  • 17
7

Java 11 Server Code:

package org.treez.server.websocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.DatatypeConverter;;

public class WebSocketServer{


    public static void main(String[] args){

        int portNumber = 8000;

        ServerSocket server;
        try {
            server = new ServerSocket(portNumber);
        } catch (IOException exception) {
            throw new IllegalStateException("Could not create web server", exception);
        }


        Socket clientSocket;
        try {
            clientSocket = server.accept(); //waits until a client connects
        } catch (IOException waitException) {
            throw new IllegalStateException("Could not wait for client connection", waitException);
        }

        InputStream inputStream;
        try {
            inputStream  = clientSocket.getInputStream();
        } catch (IOException inputStreamException) {
            throw new IllegalStateException("Could not connect to client input stream", inputStreamException);
        }

        OutputStream outputStream;
        try {
            outputStream  = clientSocket.getOutputStream();
        } catch (IOException inputStreamException) {
            throw new IllegalStateException("Could not connect to client input stream", inputStreamException);
        }

        try {
            doHandShakeToInitializeWebSocketConnection(inputStream, outputStream);
        } catch (UnsupportedEncodingException handShakeException) {
            throw new IllegalStateException("Could not connect to client input stream", handShakeException);
        }


        try {        
            outputStream.write(encode("Hello from Server!"));
            outputStream.flush();
        } catch (UnsupportedEncodingException e) {          
            e.printStackTrace();
        } catch (IOException e) {       
            e.printStackTrace();
        }   

         try {
                printInputStream(inputStream);
            } catch (IOException printException) {
                throw new IllegalStateException("Could not connect to client input stream", printException);
            }

    }

    //Source for encoding and decoding:
    //https://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side


    private static void printInputStream(InputStream inputStream) throws IOException {
        int len = 0;            
        byte[] b = new byte[1024];
        //rawIn is a Socket.getInputStream();
        while(true){
            len = inputStream.read(b);
            if(len!=-1){

                byte rLength = 0;
                int rMaskIndex = 2;
                int rDataStart = 0;
                //b[0] is always text in my case so no need to check;
                byte data = b[1];
                byte op = (byte) 127;
                rLength = (byte) (data & op);

                if(rLength==(byte)126) rMaskIndex=4;
                if(rLength==(byte)127) rMaskIndex=10;

                byte[] masks = new byte[4];

                int j=0;
                int i=0;
                for(i=rMaskIndex;i<(rMaskIndex+4);i++){
                    masks[j] = b[i];
                    j++;
                }

                rDataStart = rMaskIndex + 4;

                int messLen = len - rDataStart;

                byte[] message = new byte[messLen];

                for(i=rDataStart, j=0; i<len; i++, j++){
                    message[j] = (byte) (b[i] ^ masks[j % 4]);
                }

                System.out.println(new String(message)); 

                b = new byte[1024];

            }
        }
    }


    public static byte[] encode(String mess) throws IOException{
        byte[] rawData = mess.getBytes();

        int frameCount  = 0;
        byte[] frame = new byte[10];

        frame[0] = (byte) 129;

        if(rawData.length <= 125){
            frame[1] = (byte) rawData.length;
            frameCount = 2;
        }else if(rawData.length >= 126 && rawData.length <= 65535){
            frame[1] = (byte) 126;
            int len = rawData.length;
            frame[2] = (byte)((len >> 8 ) & (byte)255);
            frame[3] = (byte)(len & (byte)255); 
            frameCount = 4;
        }else{
            frame[1] = (byte) 127;
            int len = rawData.length;
            frame[2] = (byte)((len >> 56 ) & (byte)255);
            frame[3] = (byte)((len >> 48 ) & (byte)255);
            frame[4] = (byte)((len >> 40 ) & (byte)255);
            frame[5] = (byte)((len >> 32 ) & (byte)255);
            frame[6] = (byte)((len >> 24 ) & (byte)255);
            frame[7] = (byte)((len >> 16 ) & (byte)255);
            frame[8] = (byte)((len >> 8 ) & (byte)255);
            frame[9] = (byte)(len & (byte)255);
            frameCount = 10;
        }

        int bLength = frameCount + rawData.length;

        byte[] reply = new byte[bLength];

        int bLim = 0;
        for(int i=0; i<frameCount;i++){
            reply[bLim] = frame[i];
            bLim++;
        }
        for(int i=0; i<rawData.length;i++){
            reply[bLim] = rawData[i];
            bLim++;
        }

        return reply;
    }

    private static void doHandShakeToInitializeWebSocketConnection(InputStream inputStream, OutputStream outputStream) throws UnsupportedEncodingException {
        String data = new Scanner(inputStream,"UTF-8").useDelimiter("\\r\\n\\r\\n").next();

        Matcher get = Pattern.compile("^GET").matcher(data);

        if (get.find()) {
            Matcher match = Pattern.compile("Sec-WebSocket-Key: (.*)").matcher(data);
            match.find();                 

            byte[] response = null;
            try {
                response = ("HTTP/1.1 101 Switching Protocols\r\n"
                        + "Connection: Upgrade\r\n"
                        + "Upgrade: websocket\r\n"
                        + "Sec-WebSocket-Accept: "
                        + DatatypeConverter.printBase64Binary(
                                MessageDigest
                                .getInstance("SHA-1")
                                .digest((match.group(1) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
                                        .getBytes("UTF-8")))
                        + "\r\n\r\n")
                        .getBytes("UTF-8");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            try {
                outputStream.write(response, 0, response.length);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } else {

        }
    }
}

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>TreezHttpServer</groupId>
  <artifactId>TreezHttpServer</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <resources>
      <resource>
        <directory>src</directory>
        <excludes>
          <exclude>**/*.java</exclude>
        </excludes>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <release>11</release>
        </configuration>
      </plugin>
    </plugins>    

  </build>

  <dependencies>

    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.2.11</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-core</artifactId>
        <version>2.2.11</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <version>2.2.11</version>
    </dependency>
    <dependency>
        <groupId>javax.activation</groupId>
        <artifactId>activation</artifactId>
        <version>1.1.1</version>
    </dependency>

  </dependencies>
</project>

JavaScript Client:

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>WebSocket Client</title>

 <script type="text/javascript">

      var wsocket;      

      function connect() {         

          wsocket = new WebSocket("ws://localhost:8000");          
          wsocket.onopen = onopen;
          wsocket.onmessage = onmessage;
          wsocket.onclose = onclose; 
      }

      function onopen() {
          console.log("Connected!");             
          wsocket.send('hello from client');
      }

      function onmessage(event) { 
         console.log("Data received: " + event.data); 
      }

      function onclose(e) {
         console.log("Connection closed.");              
      }

      window.addEventListener("load", connect, false);

  </script>

</head>

<body>
</body>

</html>
Stefan
  • 10,010
  • 7
  • 61
  • 117
1

Tried using Stefan's code. And currently it doesn't work well (I'm using Firefox). Websocket packets larger than 1024 bytes are being splitted into TCP segments, so reassembling is required. Here is an updated code for reading browser packet:

private static void processResponse(InputStream inputStream, OutputStream outputStream) throws IOException {
    int readPacketLength = 0;
    byte[] packet = new byte[1024];
    ByteArrayOutputStream packetStream = new ByteArrayOutputStream();

    while(true) {
        readPacketLength = inputStream.read(packet);

        if(readPacketLength != -1) {
            if ((packet[0] & (byte) 15) == (byte) 8) { // Disconnect packet
                outputStream.write(packet, 0, readPacketLength);
                // returning the same packet for client to terminate connection
                outputStream.flush();
                return;
            }
            byte messageLengthByte = 0;
            int messageLength = 0;
            int maskIndex = 2;
            int messageStart = 0;
            //b[0] is always text in my case so no need to check;
            byte data = packet[1];
            byte op = (byte) 127; // 0111 111
            messageLengthByte = (byte) (data & op);

            int totalPacketLength = 0;
            if (messageLengthByte == (byte) 126 || messageLengthByte == (byte) 127) {
                if (messageLengthByte == (byte) 126) {
                    maskIndex = 4;
                    // if (messageLengthInt==(byte)126), then 16-bit length is stored in packet[2] and [3]
                    ByteBuffer messageLength16Bit = ByteBuffer.allocateDirect(4);
                    messageLength16Bit.order(ByteOrder.BIG_ENDIAN);
                    messageLength16Bit.put((byte) 0x00);
                    messageLength16Bit.put((byte) 0x00);
                    messageLength16Bit.put(packet, 2, 2);
                    messageLength16Bit.flip();
                    messageLength = messageLength16Bit.getInt();
                    totalPacketLength = messageLength + 8;
                } else {
                    maskIndex = 10;
                    // if (messageLengthInt==(byte)127), then 64-bit length is stored in bytes [2] to [9]. Using only 32-bit
                    ByteBuffer messageLength64Bit = ByteBuffer.allocateDirect(4);
                    messageLength64Bit.order(ByteOrder.BIG_ENDIAN);
                    messageLength64Bit.put(packet, 6, 4);
                    messageLength64Bit.flip();
                    messageLength = messageLength64Bit.getInt();
                    totalPacketLength = messageLength + 14;
                }

                if (readPacketLength != totalPacketLength) {
                    packetStream.write(packet, 0, readPacketLength);

                    int lastPacketLength = 0;
                    while (readPacketLength < totalPacketLength) {
                        packet = new byte[1024];
                        readPacketLength += lastPacketLength = inputStream.read(packet);
                        packetStream.write(packet, 0, lastPacketLength);
                    }
                    packet = packetStream.toByteArray();
                    packetStream.reset();
                }
            }
            else { // using message length from packet[1]
                messageLength = messageLengthByte;
            }

            byte[] masks = new byte[4];
            int i=0; int j=0;
            for(i = maskIndex; i < (maskIndex+4); i++) {
                masks[j] = packet[i];
                j++;
            }

            messageStart = maskIndex + 4;

            byte[] message = new byte[messageLength];
            for(i = messageStart, j = 0; i < readPacketLength; i++, j++){
                message[j] = (byte) (packet[i] ^ masks[j % 4]);
            }
            System.out.println("Received message: " + new String(message));
            packet = new byte[1024];
        }
    }
}
Sergey K
  • 19
  • 5
0
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.DatatypeConverter;;

public class WebSocketServer{


    public static void main(String[] args){

        int portNumber = 8000;

        ServerSocket server;
        try {
            server = new ServerSocket(portNumber);
        } catch (IOException exception) {
            throw new IllegalStateException("Could not create web server", exception);
        }


        Socket clientSocket;
        try {
            clientSocket = server.accept(); //waits until a client connects
        } catch (IOException waitException) {
            throw new IllegalStateException("Could not wait for client connection", waitException);
        }

        InputStream inputStream;
        try {
            inputStream  = clientSocket.getInputStream();
        } catch (IOException inputStreamException) {
            throw new IllegalStateException("Could not connect to client input stream", inputStreamException);
        }

        OutputStream outputStream;
        try {
            outputStream  = clientSocket.getOutputStream();
        } catch (IOException inputStreamException) {
            throw new IllegalStateException("Could not connect to client input stream", inputStreamException);
        }

        try {
            doHandShakeToInitializeWebSocketConnection(inputStream, outputStream);
        } catch (UnsupportedEncodingException handShakeException) {
            throw new IllegalStateException("Could not connect to client input stream", handShakeException);
        }


        try {        
            outputStream.write(encode("Hello from Server!"));
            outputStream.flush();
        } catch (UnsupportedEncodingException e) {          
            e.printStackTrace();
        } catch (IOException e) {       
            e.printStackTrace();
        }   

         try {
                printInputStream(inputStream);
            } catch (IOException printException) {
                throw new IllegalStateException("Could not connect to client input stream", printException);
            }

    }

    //Source for encoding and decoding:
    //https://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side

    //this will handle incoming text only up to 64K only
    //it will handle multiple messages in one read and messages split over a read
    private static void printInputStream(InputStream inputStream) throws IOException {
        byte[] b = new byte[8000];//incoming buffer
        byte[] message =null;//buffer to assemble message in
        byte[] masks = new byte[4];
        boolean isSplit=false;//has a message been split over a read
        int length = 0; //length of message 
        int totalRead =0; //total read in message so far
        while (true) {
            int len = 0;//length of bytes read from socket
            try {
                len = inputStream.read(b);
            } catch (IOException e) {
                break;
            }
            if (len != -1) {
                boolean more = false;
                int totalLength = 0;
                do {
                    int j = 0;
                    int i = 0;
                    if (!isSplit) {
                        byte rLength = 0;
                        int rMaskIndex = 2;
                        int rDataStart = 0;
                        // b[0] assuming text
                        byte data = b[1];
                        byte op = (byte) 127;
                        rLength = (byte) (data & op);
                        length = (int) rLength;
                        if (rLength == (byte) 126) {
                            rMaskIndex = 4;
                            length = Byte.toUnsignedInt(b[2]) << 8;
                            length += Byte.toUnsignedInt(b[3]);
                        } else if (rLength == (byte) 127)
                            rMaskIndex = 10;
                        for (i = rMaskIndex; i < (rMaskIndex + 4); i++) {
                            masks[j] = b[i];
                            j++;
                        }

                        rDataStart = rMaskIndex + 4;

                        message = new byte[length];
                        totalLength = length + rDataStart;
                        for (i = rDataStart, totalRead = 0; i<len && i < totalLength; i++, totalRead++) {
                            message[totalRead] = (byte) (b[i] ^ masks[totalRead % 4]);
                        }

                    }else {
                        for (i = 0; i<len && totalRead<length; i++, totalRead++) {
                            message[totalRead] = (byte) (b[i] ^ masks[totalRead % 4]);
                        }
                        totalLength=i;
                    }

                    
                    if (totalRead<length) {
                        isSplit=true;
                    }else {
                        isSplit=false;
                        System.out.println(new String(message)); 
                        b = new byte[8000];
                    }
                    
                    if (totalLength < len) {
                        more = true;
                        for (i = totalLength, j = 0; i < len; i++, j++)
                            b[j] = b[i];
                        len = len - totalLength;
                    }else
                        more = false;
                } while (more);
            } else
                break;
        }

    }


    public static byte[] encode(String mess) throws IOException{
        byte[] rawData = mess.getBytes();

        int frameCount  = 0;
        byte[] frame = new byte[10];

        frame[0] = (byte) 129;

        if(rawData.length <= 125){
            frame[1] = (byte) rawData.length;
            frameCount = 2;
        }else if(rawData.length >= 126 && rawData.length <= 65535){
            frame[1] = (byte) 126;
            int len = rawData.length;
            frame[2] = (byte)((len >> 8 ) & (byte)255);
            frame[3] = (byte)(len & (byte)255); 
            frameCount = 4;
        }else{
            frame[1] = (byte) 127;
            long len = rawData.length; //note an int is not big enough in java
            frame[2] = (byte)((len >> 56 ) & (byte)255);
            frame[3] = (byte)((len >> 48 ) & (byte)255);
            frame[4] = (byte)((len >> 40 ) & (byte)255);
            frame[5] = (byte)((len >> 32 ) & (byte)255);
            frame[6] = (byte)((len >> 24 ) & (byte)255);
            frame[7] = (byte)((len >> 16 ) & (byte)255);
            frame[8] = (byte)((len >> 8 ) & (byte)255);
            frame[9] = (byte)(len & (byte)255);
            frameCount = 10;
        }

        int bLength = frameCount + rawData.length;

        byte[] reply = new byte[bLength];

        int bLim = 0;
        for(int i=0; i<frameCount;i++){
            reply[bLim] = frame[i];
            bLim++;
        }
        for(int i=0; i<rawData.length;i++){
            reply[bLim] = rawData[i];
            bLim++;
        }

        return reply;
    }

    private static void doHandShakeToInitializeWebSocketConnection(InputStream inputStream, OutputStream outputStream) throws UnsupportedEncodingException {
        String data = new Scanner(inputStream,"UTF-8").useDelimiter("\\r\\n\\r\\n").next();

        Matcher get = Pattern.compile("^GET").matcher(data);

        if (get.find()) {
            Matcher match = Pattern.compile("Sec-WebSocket-Key: (.*)").matcher(data);
            match.find();                 

            byte[] response = null;
            try {
                response = ("HTTP/1.1 101 Switching Protocols\r\n"
                        + "Connection: Upgrade\r\n"
                        + "Upgrade: websocket\r\n"
                        + "Sec-WebSocket-Accept: "
                        + DatatypeConverter.printBase64Binary(
                                MessageDigest
                                .getInstance("SHA-1")
                                .digest((match.group(1) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
                                        .getBytes("UTF-8")))
                        + "\r\n\r\n")
                        .getBytes("UTF-8");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            try {
                outputStream.write(response, 0, response.length);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } else {

        }
    }
}

This fixes issues with Stefan's answer. It can handle multiple messages in one read and messages split over a read. Also fixes an issue with using an int instead of a long in writes.

  • Welcome to Stack Overflow! While your answer may solve the question, [including an explanation](https://meta.stackexchange.com/q/114762) of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. You can edit your answer to add explanations and give an indication of what limitations and assumptions apply. - [From Review](https://stackoverflow.com/review/late-answers/28408571) – Adam Marshall Feb 25 '21 at 15:35