嵌入式 Java 网络服务器隐藏了应用程序界面

embedded Java web server hides app interface

我有一个用 JavaFX 编写的应用程序,用于控制剧院中的一些灯光,界面非常简单。基本上有两个按钮,一个在 3 秒内淡化亮起,另一个在 3 秒内淡化。该应用程序连接到以太网到串行服务器 (Sealevel Sealink 4104) 以控制灯。

我想添加一个浏览器界面,以便可以通过任何移动设备控制该应用程序。我已根据从该视频中获得的代码添加了一个 Java 网络服务器。

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

应用程序运行s,我可以在浏览器中获取我要查找的网页。但是,我的应用程序界面从未出现。这个想法是应用程序界面始终存在以表明它是 运行ning。网页界面将可用于将控制选项扩展到移动设备。

此时的主要问题是如何在不干扰应用程序界面功能的情况下让 Web 服务器在后台 运行?

网络服务器代码:

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);
    }
}
}

接口代码如下:

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

}

}

我猜测 JavaFX 需要它的线程返回。它调用 start(),您在其中调用 webserver.StartServer(),后者又陷入无限 while(true) 循环。您也应该在单独的线程中执行套接字接受循环(并根据需要正确关闭它)并让 start 方法 return.

话虽这么说,我不建议您尝试自己实施 pseudo-HTTP-server - 这只是额外的代码、工作和维护,如果不是 RFC-compliant,可能会以各种方式出现问题。您可以使用大量可嵌入的轻量级 HTTP 服务器。作为 JLHTTP 的作者,我认为它可能很适合您的用例,但还有许多其他选择。

使用 JLHTTP 2.1,您需要这样的东西:

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();
}

备注:

  • 网站文件(html/js/css/imags 等)是从磁盘提供的 - 该示例使用当前目录,但您应该将其更改为专用目录以防止意外访问敏感文件。
  • 您的客户端代码可以通过表单、AJAX、url 和查询参数等使用 POST 或 GET 请求,只要它发送适当的动作参数和值。
  • 您还应妥善关闭应用程序、连接、HTTPServer 等
  • 此示例接受单个 on/off 操作参数。如果您在客户端需要更大的灵活性,您可以传递单独的 command/device/value 参数并在上下文处理程序中创建灯光控制器命令字符串。
  • 一切正常后,您还应该考虑安全性,否则观众中的某些孩子会开始搞乱您的节目:-)