JavaFx -- chapter03(多线程网络通信)

chapter03(多线程)

教学与实践目的

学会在网络应用开发中运用Java多线程技术。

程序的基本调试技术

程序无语法错误、能运行,但没有出现预期的结果,说明程序可能存在逻辑错误。解决这类错误的主要方法是查看程序运行过程中的内存变量值。一个常用的手段是通过打印语句打印出变量的值,例如使用 System.out.println(待排查的变量)。但更强大的方法是使用IDE提供的断点功能。

在Idea设断点并查看变量的方法
  1. 鼠标点击要查看变量所在代码行的行号右侧空白处,出现棕红色实心圆,即表示在此处打了断点。
  2. 调试时程序会在此处停住,方便观察程序运行的状况和各变量的即时值。
操作步骤
  • 首先新建一个包,命名为 chapter03
  • 然后将上一讲的 TCPServer.javaTCPClient.javaTCPClientFX.java 复制到这个包中,注意程序中第一行语句是否自动修改为 package chapter03;
  • 假如我们要观察获取的IP地址是否符合预期,可以在客户端窗口程序 TCPClientFX 中选择一行有相关变量的代码行,如图3.1,鼠标点击行号右侧标注断点。
  • 右上角下拉框选中 TCPClientFX,再点击"调试"图标(可以直接从"run"菜单或右键点击主窗体的弹出菜单中选择debug方式运行),窗口程序运行到红色断点行时会停留,便于观察此时IP、port等变量的状态值,如图3.2所示。
  • 通过图3.2所示红色框区域,可以让程序单步执行,一步一步地观察程序执行的情况,如果当前行代码中有方法的调用,step over 表示把方法当作一行代码直接执行,而 step into 则继续下钻,可以进入方法内部继续跟踪,一般只是用于进入自定义方法。

理解阻塞语句

在同一个进程中,一条阻塞语句的执行影响着下条语句何时被执行。如果该条语句没有执行完,那么下条语句是不可能进入执行状态的,因此,从字面上理解,该条语句阻塞了下面语句的执行。

使用 BufferedReaderreadLine() 方法
  • 若该套接字的输入流中没有带行结束符(如 \n)的字符可读,则该语句会处于阻塞状态,直到条件出现行结束符,才会执行下面的语句。
阻塞状态程序演示
  1. TCPServer.java 程序中的发送语句临时禁用(验证完再还原),例如:

    java 复制代码
    // 向输出流中输出一行字符串,远程客户端可以读取该字符串
    // pw.println("来自服务器:" + msg);  临时禁用
    即服务器不回传信息。
  2. 启动 TCPServer.java 服务程序,再启动 TCPClientFX.java 客户端程序,发送信息,发现客户程序不能正常运行,发送按钮甚至整个程序失去响应。

  3. 强行终止 TCPClientFX,在窗口程序的发送语句处设置断点,如图3.3所示。然后在调试状态运行该程序,逐行调试(遇到自定义的方法,建议使用 step into 跟踪进入)。在执行到 receive() 方法时,使用 step into 跟踪进方法会发现程序会阻塞在 msg = br.readLine(); 处(因为服务器没有返回,客户端的输入流队列中是空的,所以被阻塞)。

理解读一行功能

同理,若套接字的输入流中有多行信息,调用一次 readLine() 方法,只是读出当前的一行(当然你可以调用其他的"读"方法)。

程序演示
  1. TCPServer.java 程序中多增加一条信息返回语句,例如:

    java 复制代码
    pw.println("来自服务器:" + msg);
    // 下面多增加一条信息返回语句
    pw.println("来自服务器,重复发送: " + msg);

    然后启动服务端程序。

  2. 启动客户端 TCPClientFX 程序,发现客户显示区每次只显示一条信息,且与你发送的信息不同步。因为每一次互动,服务器返回两行信息,而客户端只是读取最前面的一行信息。

多线程技术

有了多线程技术,我们就有了更多选择。

编写读取服务器信息的线程

TCPClientFX.java 程序中,发送信息是可以通过"发送"按钮来实现主动控制,可接收信息是被动的,你不知道输入流中有多少信息。为此,在窗口程序中添加一个线程专门负责读取输入流中的信息,同时,"发送"按钮动作中,读取输入流信息的代码就需要删除。

操作步骤
  1. 现在右键选择 TCPClientFX.java 重构(Refactor),重命名为 TCPClientThreadFX.java(采用如图 3.5 所示的方式)。

  2. 在合适的位置添加如下线程代码(自己思考添加在什么位置合适,更新版本讲义会有更详细的提示),用于接收服务器的信息,为了简洁,匿名内部类使用了lambda的写法:

    java 复制代码
    // 用于接收服务器信息的单独线程
    receiveThread = new Thread(()->{ 
        String msg = null;
        // 不知道服务器有多少回传信息,就持续不断接收
        // 由于在另外一个线程,不会阻塞主线程的正常运行
        while ((msg = tcpClient.receive()) != null) { 
            String msgTemp = msg; // msgTemp 实质是final类型
            Platform.runLater(()->{ 
                taDisplay.appendText(msgTemp + "\n"); 
            });
        }
        // 跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
        Platform.runLater(()->{ 
            taDisplay.appendText("对话已关闭!\n" ); 
        });
    });
    receiveThread.start(); // 启动线程
  3. 以上代码中有四点注意:

    • 由于是新开的一个线程循环读取服务器的信息,所以不用考虑服务器是否有发欢迎信息,就算读取不到信息也只是阻塞这个线程,主程序本身使用没有任何影响(单线程就会卡住)。事实上服务器发多少信息都没问题,该线程通过循环语句来读取,没信息过来就阻塞等待,当服务器关闭连接时,就会跳出循环语句,结束本线程;
    • 现在接收并显示服务端信息的任务交给了一个单独的线程,那么原来主线程中连接按钮和发送按钮的动作事件代码中,关于接收并显示服务端信息的代码还需要保留吗?这个取舍非常重要!
    • 对于JavaFX窗体界面,在新线程中无法直接更新界面中有关控件的内容,只能将更新代码放在 Platform.runLater(Runnable XXX) 方法的 Runnable 子类实例中,如以上代码第12-14行、17-19行所示;
    • 匿名内部类或lambda表达式中,不能访问外部类方法中的非final类型的局部变量,例如上面第13行代码如果直接使用 taDisplay.appendText(msg + "\n"); 就会报错,所以代码第11行使用了个临时常量来解决这个问题,其实不使用final关键字,也会自动识别为final类型来使用(如果将msg定义为类中的成员变量,就没有这个限制,可以直接访问)。
示例代码
java 复制代码
receiveThread = new Thread(()->{ 
    String msg = null; 
    while ((msg = tcpClient.receive()) != null) { 
        String msgTemp = msg; // msgTemp 实质是final类型 
        Platform.runLater(()->{ 
            taDisplay.appendText(msgTemp + "\n"); 
        }); 
    }
    Platform.runLater(()->{ 
        taDisplay.appendText("对话已关闭!\n" ); 
    }); 
}); 
receiveThread.start();

lambda表达式

在 Java 中,()->{} 是一个 lambda 表达式的语法,它用于创建一个没有参数的函数式接口的匿名实现。Lambda 表达式是 Java8 引入的一个特性,它允许你以简洁的方式表示只有一个方法的接口的实现。

函数式接口是只有一个抽象方法的接口,这样的接口可以用 lambda 表达式来实现。例如,RunnableCallable 都是函数式接口。

下面是一个使用 lambda 表达式的简单例子:

java 复制代码
Runnable runnable = ()->{
    // 这里是代码块
    System.out.println("Hello, Lambda!");
};
runnable.run(); // 输出 "Hello, Lambda!"

在这个例子中,Runnable 是一个函数式接口,它有一个抽象方法 run()。我们通过 lambda 表达式 ()->{} 创建了一个 Runnable 的匿名实现,并在代码块中编写了要执行的代码。然后,我们通过调用 run() 方法来执行这个 lambda 表达式。

Lambda 表达式可以用于任何函数式接口,并且可以作为参数传递给方法,或者作为方法的返回值。这使得代码更加简洁和灵活。

Runnable接口

Runnable 是 Java 中的一个接口,属于 java.lang 包。它只有一个抽象方法 run(),通常用于创建线程时定义线程要执行的任务。

当你创建一个实现了 Runnable 接口的类时,你需要重写 run() 方法来定义线程的行为。然后,你可以将这个实现了 Runnable 接口的类的实例传递给 Thread 类的构造器来创建一个线程。

下面是 Runnable 接口的一个简单示例:

java 复制代码
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 这里是线程要执行的代码
        System.out.println("线程正在运行...");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建 Runnable 实例
        MyRunnable myRunnable = new MyRunnable();
        
        // 创建并启动线程
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

在这个例子中,MyRunnable 类实现了 Runnable 接口,并重写了 run() 方法来定义线程的行为。然后,在 main 方法中,我们创建了 MyRunnable 的实例,并将其传递给 Thread 类的构造器来创建一个线程。最后,我们调用 start() 方法来启动线程。

Runnable 接口的另一个常见用途是作为参数传递给 ExecutorService,这是一个用于管理线程池的类,它允许你以更高效的方式执行并发任务。

Runnable 接口的使用是 Java 多线程编程的基础之一,它提供了一种简单的方式来定义线程任务。

Thread类

Thread类的介绍
  1. 避免长时间运行的任务阻塞 UI:在 UI 线程中执行长时间运行的任务会导致应用程序无响应。因此,应该将这些任务放在单独的线程中执行。
  2. 更新 UI 线程 :由于 UI 组件只能在 JavaFX 的主线程(UI 线程)中安全地更新,因此需要使用 Platform.runLater(Runnable) 方法来确保 UI 更新操作在正确的线程中执行。
  3. 线程的创建和管理 :可以通过继承 Thread 类并重写 run 方法来创建新线程。也可以使用 ExecutorService 来管理线程池,这通常是更高效和灵活的方式。
java 复制代码
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ThreadExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        Label label = new Label("任务开始");

        // 创建并启动线程
        Thread thread = new Thread(() -> {
            try {
                // 模拟长时间运行的任务
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 在 UI 线程中更新标签
            Platform.runLater(() -> {
                label.setText("任务完成");
            });
        });

        thread.start();

        StackPane root = new StackPane(label);
        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("JavaFX Thread Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

在这个示例中,我们创建了一个 Thread 来执行一个模拟的长时间运行的任务。任务完成后,我们使用 Platform.runLater 来更新 UI 组件(在这个例子中是一个 Label)。

记住,虽然 Thread 类在 JavaFX 中仍然可以使用,但更推荐的做法是使用 JavaFX 提供的 Task 类或者 Service 类来处理后台任务,因为它们提供了更好的集成和更简单的 UI 更新机制。

runLater方法

Platform.runLater() 是 JavaFX 中的一个方法,用于将一个 Runnable 任务安排在 JavaFX 主线程(也称为 UI 线程)上执行。JavaFX 应用程序的 UI 组件必须在主线程上进行修改,以确保线程安全和正确的 UI 更新。

Platform.runLater() 方法接受一个 Runnable 参数,这个 Runnable 包含了要在 UI 线程上执行的代码。如果你在后台线程中更新 UI,而没有使用 Platform.runLater(),那么可能会导致不可预知的行为,比如应用程序崩溃或者 UI 组件状态不一致。

这个方法通常在以下几种情况下使用:

  1. 从后台线程更新 UI :当你在后台线程中完成一项任务后,需要更新 UI 时,可以使用 Platform.runLater() 来确保更新操作在 UI 线程上执行。

  2. 延迟 UI 更新:有时候你可能需要在 UI 线程上延迟执行某些操作,比如在动画结束后更新 UI。

  3. 处理事件:在处理某些事件时,你可能需要在 UI 线程上执行一些操作,以确保 UI 的响应性和一致性。

下面是一个使用 Platform.runLater() 的简单示例:

java 复制代码
import javafx.application.Platform;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class RunLaterExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        Button button = new Button("Click Me");
        button.setOnAction(event -> {
            // 模拟一个耗时操作
            new Thread(() -> {
                try {
                    Thread.sleep(2000); // 模拟耗时操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 使用 Platform.runLater 来更新 UI
                Platform.runLater(() -> {
                    button.setText("Clicked!");
                });
            }).start();
        });

        StackPane root = new StackPane();
        root.getChildren().add(button);

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

        primaryStage.setTitle("Platform.runLater Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

在这个示例中,当用户点击按钮时,会启动一个后台线程来模拟一个耗时操作。操作完成后,我们使用 Platform.runLater() 来更新按钮的文本,确保这个更新操作在 UI 线程上执行。

添加事件处理EventHandler

java 复制代码
Port_input.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<javafx.scene.input.KeyEvent>() {
    @Override
    public void handle(javafx.scene.input.KeyEvent event) {
        if (event.getCode() == KeyCode.ENTER) {
            btnCon.fire();
        }
    }
});

最终代码

SimpleFx.java/LookUpScoreFx.java
java 复制代码
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class LookUpScoreFX extends Application {

    private LookUpScore lookUpScore;

    private final Button btnCon = new Button("连接");
    private final Button btnExit = new Button("退出");
    private final Button btnSend = new Button("发送");

    private Thread receiveThread = null;

    private final TextField IpAdd_input = new TextField();
    private final TextField Port_input = new TextField();
    private final TextArea OutputArea = new TextArea();

    private final TextField InputField = new TextField();


    public void start(Stage primaryStage) {
        //
        // 新增,设置标题,类名改成LookUpScoreFX
        primaryStage.setTitle("查看平时成绩");
        btnSend.setDisable(true);
        BorderPane mainPane = new BorderPane();
        VBox mainVBox = new VBox();

        HBox hBox = new HBox();
        hBox.setSpacing(10);//各控件之间的间隔
        //HBox面板中的内容距离四周的留空区域
        hBox.setPadding(new Insets(20, 20, 10, 20));
        hBox.getChildren().addAll(new Label("IP地址: "), IpAdd_input, new Label("端口: "), Port_input, btnCon);

        hBox.setAlignment(Pos.TOP_CENTER);
        //内容显示区域
        VBox vBox = new VBox();
        vBox.setSpacing(10);//各控件之间的间隔
        //VBox面板中的内容距离四周的留空区域
        vBox.setPadding(new Insets(10, 20, 10, 20));
        vBox.getChildren().addAll(new Label("信息显示区:"), OutputArea, new Label("信息输入区"), InputField);
        //设置显示信息区的文本区域可以纵向自动扩充范围
        VBox.setVgrow(OutputArea, Priority.ALWAYS);
        // 设置文本只读和自动换行
        OutputArea.setEditable(false);
        OutputArea.setStyle("-fx-wrap-text: true; /* 实际上是默认的 */ -fx-font-size: 14px;");

        InputField.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) {
                btnSend.fire();
            }
        });

        //底部按钮区域
        HBox hBox2 = new HBox();
        hBox2.setSpacing(10);
        hBox2.setPadding(new Insets(10, 20, 10, 20));

        // 设置按钮的交互效果
        btnCon.setOnAction(event -> {
            String ip = IpAdd_input.getText().trim();
            String port = Port_input.getText().trim();
            // 设置不能再次点击
            btnCon.setDisable(true);
            try {
                //tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
                lookUpScore = new LookUpScore(ip, port);
                // 用于接收服务器信息的单独线程
                receiveThread = new Thread(() -> {
                    String msg = null;
                    // 不知道服务器有多少回传信息,就持续不断接收
                    // 由于在另外一个线程,不会阻塞主线程的正常运行
                    while ((msg = lookUpScore.receive()) != null) {
                        String msgTemp = msg; // msgTemp 实质是final类型
                        Platform.runLater(() -> {
                            OutputArea.appendText(msgTemp + "\n");
                        });
                    }
                    // 跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
                    Platform.runLater(() -> {
                        OutputArea.appendText("对话已关闭!\n");
                    });
                }, "receiveThread");
                receiveThread.start(); // 启动线程
                btnSend.setDisable(false);
            } catch (Exception e) {
                OutputArea.appendText("服务器连接失败!" + e.getMessage() + "\n");
            }
        });
        btnExit.setOnAction(event -> {
            if (lookUpScore != null) {
                //
                // 新增代码
                try {
                    //向服务器发送关闭连接的约定信息
                    lookUpScore.send("bye");
                    // 等待子线程(服务器)收到/读取信息再关闭输入输出流,这样不会报错
                    Thread.sleep(1000);
                    lookUpScore.close();
                    btnSend.setDisable(true);
                    // 等待线程回收资源
                    receiveThread.join();
                } catch (Exception e) {
                    System.out.println(e.getStackTrace());
                }
            }
            System.exit(0);
        });
        Port_input.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<javafx.scene.input.KeyEvent>() {
            @Override
            public void handle(javafx.scene.input.KeyEvent event) {
                if (event.getCode() == KeyCode.ENTER) {
                    btnCon.fire();
                }
            }
        });
        //
        // 结束
        btnSend.setOnAction(event -> {
            String sendMsg = InputField.getText();
            lookUpScore.send(sendMsg);//向服务器发送一串字符
            InputField.clear();
            OutputArea.appendText("客户端发送:" + sendMsg + "\n");
        });

        hBox2.setAlignment(Pos.CENTER_RIGHT);
        hBox2.getChildren().addAll(btnSend, btnExit);
		
        VBox.setVgrow(vBox, Priority.ALWAYS);
        mainVBox.getChildren().addAll(hBox, vBox, hBox2);

        mainPane.setCenter(mainVBox);
        Scene scene = new Scene(mainPane, 700, 400);

        IpAdd_input.setText("127.0.0.1");
        Port_input.setText("8888");

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
报错原因
  • 是因为直接点击退出可能会发送bye之后里面关闭socket(115行),但是子线程还在阻塞等待读写socket(第90行)
TCPClient.java/LookUpScore.java
java 复制代码
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class LookUpScore {
    private final Socket socket; // 定义套接字
    private final PrintWriter pw; // 定义字符输出流
    private final BufferedReader br; // 定义字符输入流

    public LookUpScore(String ip, String port) throws IOException {
        // 主动向服务器发起连接,实现TCP的三次握手过程
        // 如果不成功,则抛出错误信息,其错误信息交由调用者处理
        socket = new Socket(ip, Integer.parseInt(port));

        // 得到网络输出字节流地址,并封装成网络输出字符流
        // 设置最后一个参数为true,表示自动flush数据
        OutputStream socketOut = socket.getOutputStream();
        pw = new PrintWriter(new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);

        // 得到网络输入字节流地址,并封装成网络输入字符流
        InputStream socketIn = socket.getInputStream();
        br = new BufferedReader(new InputStreamReader(socketIn, StandardCharsets.UTF_8));
    }

    public void send(String msg) {
        // 输出字符流,由Socket调用系统底层函数,经网卡发送字节流
        pw.println(msg);
    }

    public String receive() {
        String msg = null;
        try {
            // 从网络输入字符流中读信息,每次只能接收一行信息
            // 如果不够一行(无行结束符),则该语句阻塞等待
            msg = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return msg;
    }

    // 实现close方法以关闭socket连接及相关的输入输出流
    public void close() {
        try {
            if (pw != null) {
                pw.close(); // 关闭PrintWriter会先flush再关闭底层流
            }
            if (br != null) {
                br.close(); // 关闭BufferedReader
            }
            if (socket != null) {
                socket.close(); // 关闭Socket连接
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
TCPServer.java
java 复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class TCPServer {
    private final int port; // 服务器监听端口号
    private final ServerSocket serverSocket; //定义服务器套接字

    public TCPServer() throws IOException {
        Scanner scanner = new Scanner(System.in); // 创建一个Scanner对象来读取标准输入
        System.out.println("请输入服务器监听的端口号:");
        if (scanner.hasNextInt()) { // 检查是否有下一个输入项并且是一个整数
            port = scanner.nextInt(); // 读取整数并赋值给port
        } else {
            System.out.println("输入错误,请输入一个有效的整数端口号。");
            // 这里可以根据需要处理错误情况,比如使用默认值或者退出程序
            port = 8080; // 例如,使用8080作为默认端口号
        }
        scanner.close(); // 关闭scanner对象

        serverSocket = new ServerSocket(port);
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

    private PrintWriter getWriter(Socket socket) throws IOException {
        //获得输出流缓冲区的地址
        OutputStream socketOut = socket.getOutputStream();

        //网络流写出需要使用flush,这里在PrintWriter构造方法中直接设置为自动flush
        return new PrintWriter(
                new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);

    }

    private BufferedReader getReader(Socket socket) throws IOException {
        //获得输入流缓冲区的地址
        InputStream socketIn = socket.getInputStream();
        return new BufferedReader(
                new InputStreamReader(socketIn, StandardCharsets.UTF_8));
    }

    //单客户版本,即每一次只能与一个客户建立通信连接
    public void Service() {
        while (true) {
            Socket socket = null;
            try {
                //此处程序阻塞等待,监听并等待客户发起连接,有连接请求就生成一个套接字。
                socket = serverSocket.accept();

                //本地服务器控制台显示客户端连接的用户信息
                System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
                BufferedReader br = getReader(socket);//定义字符串输入流
                PrintWriter pw = getWriter(socket);//定义字符串输出流
                //客户端正常连接成功,则发送服务器的欢迎信息,然后等待客户发送信息
                pw.println("From 服务器:欢迎使用本服务!");

                String msg = null;
                //此处程序阻塞,每次从输入流中读入一行字符串
                while ((msg = br.readLine()) != null) {
                    //如果客户发送的消息为"bye",就结束通信
                    if (msg.equals("bye")) {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From服务器:服务器断开连接,结束服务!");
                        System.out.println("客户端离开");
                        break; //结束循环
                    }
                    //向输出流中输出一行字符串,远程客户端可以读取该字符串
                    pw.println("From服务器:" + msg);
                    pw.println("From服务器重复发送:" + msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            } finally {
                try {
                    if (socket != null)
                        socket.close(); //关闭socket连接及相关的输入输出流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TCPServer server = new TCPServer();
        System.out.println("服务器将监听端口号: " + server.port);
        server.Service();
    }
}

更新SimpleFx(添加文本选择功能)

java 复制代码
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

import java.io.File;
import java.io.IOException;

public class FileClientFx extends Application {

  private final Button btnCon = new Button("连接");
  private final Button btnExit = new Button("退出");
  private final Button btnSend = new Button("发送");
  private final Button btnDownload = new Button("下载");
  private final TextField IpAdd_input = new TextField();
  private final TextField Port_input = new TextField();
  private final TextArea OutputArea = new TextArea();
  private final TextField InputField = new TextField();
  private FileDialogClient fileDialogClient;
  private Thread receiveMsgThread = null;
  private String ip, port;

  public static void main(String[] args) {
    launch(args);
  }

  public void start(Stage primaryStage) {
    primaryStage.setTitle("文件传输");
    btnSend.setDisable(true);
    BorderPane mainPane = new BorderPane();
    VBox mainVBox = new VBox();

    HBox hBox = new HBox();
    hBox.setSpacing(10);//各控件之间的间隔
    //HBox面板中的内容距离四周的留空区域
    hBox.setPadding(new Insets(20, 20, 10, 20));
    hBox.getChildren().addAll(new Label("IP地址: "), IpAdd_input, new Label("端口: "), Port_input, btnCon);

    hBox.setAlignment(Pos.TOP_CENTER);
    //内容显示区域
    VBox vBox = new VBox();
    vBox.setSpacing(10);//各控件之间的间隔
    //VBox面板中的内容距离四周的留空区域
    vBox.setPadding(new Insets(10, 20, 10, 20));
    vBox.getChildren().addAll(new Label("信息显示区:"), OutputArea, new Label("信息输入区"), InputField);
    //设置显示信息区的文本区域可以纵向自动扩充范围
    VBox.setVgrow(OutputArea, Priority.ALWAYS);
    // 设置文本只读和自动换行
    OutputArea.setEditable(false);
    OutputArea.setStyle("-fx-wrap-text: true; /* 实际上是默认的 */ -fx-font-size: 14px;");

    InputField.setOnKeyPressed(event -> {
      if (event.getCode() == KeyCode.ENTER) {
        btnSend.fire();
      }
    });

    //底部按钮区域
    HBox hBox2 = new HBox();
    hBox2.setSpacing(10);
    hBox2.setPadding(new Insets(10, 20, 10, 20));

    // 重构thread,使用runnable接口,不要使用lambda表达式
    class ReceiveHandler  implements Runnable{
      @Override
      public void run(){
        String msg = null;
        // 不知道服务器有多少回传信息,就持续不断接收
        // 由于在另外一个线程,不会阻塞主线程的正常运行
        while ((msg = fileDialogClient.receive()) != null) {
          String msgTemp = msg; // msgTemp 实质是final类型
          Platform.runLater(() -> {
            OutputArea.appendText(msgTemp + "\n");
          });
        }
        // 跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
        Platform.runLater(() -> {
          OutputArea.appendText("对话已关闭!\n");
        });
      }
    }


    // 设置按钮的交互效果
    btnCon.setOnAction(event -> {
      ip = IpAdd_input.getText().trim();
      port = Port_input.getText().trim();
      // 设置不能再次点击
      btnCon.setDisable(true);
      try {
        fileDialogClient = new FileDialogClient(ip, port);
        // 用于接收服务器信息的单独线程
        receiveMsgThread = new Thread(new ReceiveHandler(), "receiveThread");
        receiveMsgThread.start(); // 启动线程
        btnSend.setDisable(false);
      } catch (Exception e) {
        OutputArea.appendText("服务器连接失败!" + e.getMessage() + "\n");
      }
    });
    btnDownload.setOnAction(event -> {
      if (InputField.getText().equals("")) //没有输入文件名则返回
        return;
      String fName = InputField.getText().trim();
      InputField.clear();
      FileChooser fileChooser = new FileChooser();
      fileChooser.setInitialFileName(fName);
      File saveFile = fileChooser.showSaveDialog(null);
      if (saveFile == null) {
        return;//用户放弃操作则返回
      }
      try {
        //数据端口是2020
        FileDataClient fdclient = new FileDataClient(ip, "2020");
        fdclient.getFile(saveFile);
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText(saveFile.getName() + " 下载完毕!");
        alert.showAndWait();
        //通知服务器已经完成了下载动作,不发送的话,服务器不能提供有效反馈信息
        fileDialogClient.send("客户端开启下载");
      } catch (IOException e) {
        e.printStackTrace();
      }
    });
    btnExit.setOnAction(event -> {
      if (fileDialogClient != null) {
        //
        // 新增代码
        try {
          //向服务器发送关闭连接的约定信息
          fileDialogClient.send("bye");
          // 等待子线程和服务器 收到/读取信息完毕再关闭输入输出流,这样不会报错
          Thread.sleep(500);
          fileDialogClient.close();
          btnSend.setDisable(true);
          // 等待线程回收资源
          receiveMsgThread.join();
        } catch (Exception e) {
          System.out.println(e.getMessage());
        }
      }
      System.exit(0);
    });
    Port_input.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
      @Override
      public void handle(KeyEvent event) {
        if (event.getCode() == KeyCode.ENTER) {
          btnCon.fire();
        }
      }
    });
    //信息显示区鼠标拖动高亮文字直接复制到信息输入框,方便选择文件名
    //taDispaly 为信息选择区的 TextArea,tfSend 为信息输入区的 TextField
    //为 taDisplay 的选择范围属性添加监听器,当该属性值变化(选择文字时)会触发监听器中的代码
    OutputArea.selectionProperty().addListener((observable, oldValue, newValue) -> {
      //只有当鼠标拖动选中了文字才复制内容
      if(!OutputArea.getSelectedText().equals(""))
        InputField.setText(OutputArea.getSelectedText());
    });


    btnSend.setOnAction(event -> {
      String sendMsg = InputField.getText();
      fileDialogClient.send(sendMsg);//向服务器发送一串字符
      InputField.clear();
      OutputArea.appendText("客户端发送:" + sendMsg + "\n");
    });

    hBox2.setAlignment(Pos.CENTER_RIGHT);
    hBox2.getChildren().addAll(btnSend, btnDownload, btnExit);

    mainVBox.getChildren().addAll(hBox, vBox, hBox2);
	VBox.setVgrow(vBox, Priority.ALWAYS);
    mainPane.setCenter(mainVBox);
    Scene scene = new Scene(mainPane, 700, 400);

    IpAdd_input.setText("127.0.0.1");
    Port_input.setText("8888");

    primaryStage.setScene(scene);
    primaryStage.show();
  }
}
相关推荐
小马爱打代码3 分钟前
Spring Boot项目开发常见问题及解决方案(下)
java·spring boot·后端
潜意识起点5 分钟前
计算机专业文献检索期末论文
java·开发语言
Q_192849990644 分钟前
基于Spring Boot的工商局商家管理系统
java·spring boot·后端
m0_748232641 小时前
[MySQL报错]关于发生net start mysql 服务无法启动,服务没有报告任何错误的五种解决方案。
java
小学鸡!1 小时前
idea报错:There is not enough memory to perform the requested operation.
java·intellij-idea
L.S.V.2 小时前
Java 溯本求源之基础(三十)——封装,继承与多态
java·开发语言
码农爱java2 小时前
设计模式--装饰器模式【结构型模式】
java·设计模式·面试·装饰器模式·原理·23 中设计模式
星就前端叭3 小时前
【开源】一款基于SpringBoot的智慧小区物业管理系统
java·前端·spring boot·后端·开源
带刺的坐椅3 小时前
RxSqlUtils(base R2dbc)
java·reactor·solon·r2dbc
silence2503 小时前
深入了解 Reactor:响应式编程的利器
java·spring