Search code examples
javafxwebserver

embedded Java web server hides app interface


I have an application written in JavaFX to control some lights in a theater with a very simple interface. Basically two buttons, one to fade lights up over 3 seconds and the other to fade them down over 3 seconds. The app connects to an Ethernet to Serial Server (Sealevel Sealink 4104) to control the lights.

I would like to add a browser interface so that the app can be controlled via any mobile device. I have added a Java web server based on code I got from this video.

https://www.youtube.com/watch?v=G4Z2PQfOHdY

The app runs, and I can get the web page I am looking for in the browser. However, my app interface never shows up. The idea is that the app interface is always present to indicate that it is running. The web page interface would be available to extend the control options to a mobile device.

The main question at this point is how do I get the web server to run in the background without interfering with the functioning of the app interface?

The web server code:

package lightcontrol2;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.StringTokenizer;


public final class JavaWebserver {

public final void StartServer() throws Exception {
    // Set port number.
    int port = 9000;

    // Establish the listening socket.
    ServerSocket serverSocket = new ServerSocket(port);

    // Process HTTP sevice requests in an infinite loop.
    while (true) {
        // Listen for TCP connection request.
        Socket connectionSocket = serverSocket.accept();

        // Construct an object to process the HTTP request message.
        HttpRequest request = new HttpRequest(connectionSocket);

        // Create a new thread to process the request.
        Thread thread = new Thread(request);

        // Start the thread.
        thread.start();
    }   
}
}

final class HttpRequest implements Runnable {

// Return carriage return (CR) and line feed (LF).
final static String CRLF = "\r\n";
Socket socket;

// Constructor.
public HttpRequest(Socket socket) throws Exception {
    this.socket = socket;
}

// Implement the run() method of the Runnable interface.
// Within run(), explicitly catch and handle exceptions
// with try/ catch block.

@Override
public void run() {
    try {
        processRequest();
    } catch (Exception e){
        System.out.println(e);
    }
}

private void processRequest() throws Exception {
    // Get a reference to the socket's input and output streams.
    InputStream instream = socket.getInputStream();
    DataOutputStream os = new DataOutputStream(socket.getOutputStream());

    // Set up input stream filters.
    // Page 169, 10th line down or so . . .
    // Reads the input data.
    BufferedReader br = new BufferedReader(new InputStreamReader(instream));

    // Get the request line of the HTTP request message.
    // Get path/file.html version of http
    String requestLine = br.readLine();

    // Display the request line.
    System.out.println();
    System.out.println(requestLine);

    // Deal with the request.
    // Extract the filename from the request line.
    // This is an input method with deliminators.
    StringTokenizer tokens = new StringTokenizer(requestLine);

    // Skip over the method, which should be 'GET'.
    tokens.nextToken();
    String fileName = tokens.nextToken();

    // Root of the server.
    String root = "/www/";
    fileName = root + fileName;

    // Open the requested file.
    FileInputStream fis = null;
    boolean fileExists = true;

    try {
        fis = new FileInputStream(fileName);
    } catch (FileNotFoundException e) {
        fileExists = false;
    }

    // Construct the response message.
    String statusLine = null;
    String contentTypeLine = null;
    String entityBody = null;

    if (fileExists) {
        statusLine = "HTTP/1.0 200 OK" + CRLF;
        contentTypeLine = "Content-type: " + contentType(fileName) + CRLF;
    } 
    else {
        statusLine = "HTTP/1.0 404 Not Found" + CRLF;
        contentTypeLine = "Content-type: " + "text/html" + CRLF;
        entityBody = "<HTML>" +
                "<HEAD><TITLE>Not Found</TITLE></HEAD>" +
                "<BODY>NOt Found</BODY></HTML>";
    }

    //Send the status line.
    os.writeBytes(statusLine);

    // Sent the content type line.
    os.writeBytes(contentTypeLine);

    // Send a blank line to indicate the end of the header lines.
    os.writeBytes(CRLF);

    // Send the entity body.
    if (fileExists) {
        sendBytes(fis, os);
        os.writeBytes(statusLine);
        fis.close();
    } else {
        os.writeBytes(statusLine);
        os.writeBytes(entityBody);
        os.writeBytes(contentTypeLine);
    }

    System.out.println("*****");
    System.out.println(fileName);
    System.out.println("*****");

    // Get and display the header lines.
    String headerLine = null;
    while ((headerLine = br.readLine()).length() != 0) {
        System.out.println(headerLine);
    }

    // Close streams and socket.
    os.close();
    br.close();
    socket.close();
}       

private static String contentType(String fileName) {
    if (fileName.endsWith(".htm") || fileName.endsWith(".html")) {
        return "text/html";
    }
    if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
        return "image/jpeg";
    }
    if (fileName.endsWith(".gif")) {
        return "image/gif";
    }

    return "application/octet-stream";
}

private static void sendBytes(FileInputStream fis, OutputStream os) throws Exception {
    // Construct 1K buffer to hold bytes on way to the socket.
    byte[] buffer = new byte[1024];
    int bytes = 0;

    // Copy requested file into the socket's output stream.
    // read() returns -1, indicating end of file.
    while ((bytes = fis.read(buffer)) != -1) {
        os.write(buffer, 0, bytes);
    }
}
}

Here is the interface code:

package lightcontrol2;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;


public class LightControl2 extends Application {

@Override
public void start(Stage primaryStage) throws Exception {
    GridPane grid = createGrid();

    SealinkConnect connect = new SealinkConnect();
    JavaWebserver webserver = new JavaWebserver();

    Button btnOn = new Button();
    grid.add(btnOn, 0, 1);
    btnOn.setText("3 Sec On");        
    btnOn.setOnAction((ActionEvent event) -> {
        System.out.println("3N:100:A");
        connect.sendCommand("3N:100:A");
    });

    Button btnOff = new Button();
    grid.add(btnOff, 0, 2);
    btnOff.setText("3 Sec Off");
    btnOff.setOnAction((ActionEvent event) -> {
        System.out.println("3F:A");
        connect.sendCommand("3F:A");
    });

    BorderPane root = new BorderPane();
    root.setPadding(new Insets(10));
    root.setCenter(grid);

    Scene scene = new Scene(root, 365, 300);

    primaryStage.setTitle("Light Control Test");
    primaryStage.setScene(scene);

    scene.getStylesheets().add
        (LightControl2.class.getResource("style.css").toExternalForm());

    primaryStage.show();

    connect.socketConnect();
    webserver.StartServer();
}

private GridPane createGrid() {
    GridPane grid = new GridPane();
    grid.setAlignment(Pos.CENTER);
    grid.setHgap(5);
    grid.setVgap(10);
    grid.setPadding(new Insets(10));
    return grid;
}

/**
 * @param args the command line arguments
 */
public static void main(String[] args) {
    launch(args);

}

}

Solution

  • I'm going to guess that JavaFX needs its thread back. It invokes start(), in which you call webserver.StartServer(), which in turns remains stuck in an infinite while(true) loop. You should do the socket accepting loop in a separate thread as well (and shut it down properly as needed) and let the start method return.

    That being said, I would not recommend trying to implement a pseudo-HTTP-server on your own - that's just extra code, work and maintenance and might break in various ways if it is not RFC-compliant. There are plenty of embeddable lightweight HTTP servers you can use. As the author of JLHTTP, I think it could be a good match for your use case, but there are many others to choose from.

    Using JLHTTP 2.1, you'd need something like this:

    public void startWebServer() {
        String dir = "."; // local folder to serve website files from
        HTTPServer server = new HTTPServer(9000); // pick a port, any port
        HTTPServer.VirtualHost host = server.getVirtualHost(null); // default virtual host
        host.addContext("/", new FileContextHandler(new File(dir), "/")); // serve website files from disk directory
        host.addContext("/api/lights", (request, response) -> {
            Map<String, String> params = request.getParams();
            String action = params.get("action");
            if (action == null)
                action = "";
            switch (action) {
                case "on":
                    connect.sendCommand("3N:100:A");
                    return 200; // ok
                case "off":
                    connect.sendCommand("3F:A");
                    return 200; // ok
                default:
                    return 400; // bad request
            }
        }, "GET", "POST"); // support both GET and POST requests
        server.start();
    }
    

    Notes:

    • The website files (html/js/css/imags etc.) are served from disk - the example uses the current directory, but you should change that to a dedicated directory to prevent unintended access to sensitive files.
    • Your client code can use either a POST or GET request, via a form, AJAX, url with query parameters, etc., as long as it sends the appropriate action parameter and value.
    • You should also properly close the application, connection, HTTPServer etc.
    • This example accepts a single on/off action parameter. If you need more flexibility on the client side, you can pass individual command/device/value parameters and create the light controller command string in the context handler.
    • After you get everything working, you should consider security as well, or some kid in the audience will start messing with your show :-)