chapter03(多线程)
教学与实践目的
学会在网络应用开发中运用Java多线程技术。
程序的基本调试技术
程序无语法错误、能运行,但没有出现预期的结果,说明程序可能存在逻辑错误。解决这类错误的主要方法是查看程序运行过程中的内存变量值。一个常用的手段是通过打印语句打印出变量的值,例如使用 System.out.println(待排查的变量)
。但更强大的方法是使用IDE提供的断点功能。
在Idea设断点并查看变量的方法
- 鼠标点击要查看变量所在代码行的行号右侧空白处,出现棕红色实心圆,即表示在此处打了断点。
- 调试时程序会在此处停住,方便观察程序运行的状况和各变量的即时值。
操作步骤
- 首先新建一个包,命名为
chapter03
。 - 然后将上一讲的
TCPServer.java
、TCPClient.java
、TCPClientFX.java
复制到这个包中,注意程序中第一行语句是否自动修改为package chapter03;
。 - 假如我们要观察获取的IP地址是否符合预期,可以在客户端窗口程序
TCPClientFX
中选择一行有相关变量的代码行,如图3.1,鼠标点击行号右侧标注断点。 - 右上角下拉框选中
TCPClientFX
,再点击"调试"图标(可以直接从"run"菜单或右键点击主窗体的弹出菜单中选择debug方式运行),窗口程序运行到红色断点行时会停留,便于观察此时IP、port等变量的状态值,如图3.2所示。 - 通过图3.2所示红色框区域,可以让程序单步执行,一步一步地观察程序执行的情况,如果当前行代码中有方法的调用,
step over
表示把方法当作一行代码直接执行,而step into
则继续下钻,可以进入方法内部继续跟踪,一般只是用于进入自定义方法。
理解阻塞语句
在同一个进程中,一条阻塞语句的执行影响着下条语句何时被执行。如果该条语句没有执行完,那么下条语句是不可能进入执行状态的,因此,从字面上理解,该条语句阻塞了下面语句的执行。
使用 BufferedReader
中 readLine()
方法
- 若该套接字的输入流中没有带行结束符(如
\n
)的字符可读,则该语句会处于阻塞状态,直到条件出现行结束符,才会执行下面的语句。
阻塞状态程序演示
-
将
TCPServer.java
程序中的发送语句临时禁用(验证完再还原),例如:java// 向输出流中输出一行字符串,远程客户端可以读取该字符串 // pw.println("来自服务器:" + msg); 临时禁用 即服务器不回传信息。
-
启动
TCPServer.java
服务程序,再启动TCPClientFX.java
客户端程序,发送信息,发现客户程序不能正常运行,发送按钮甚至整个程序失去响应。 -
强行终止
TCPClientFX
,在窗口程序的发送语句处设置断点,如图3.3所示。然后在调试状态运行该程序,逐行调试(遇到自定义的方法,建议使用step into
跟踪进入)。在执行到receive()
方法时,使用step into
跟踪进方法会发现程序会阻塞在msg = br.readLine();
处(因为服务器没有返回,客户端的输入流队列中是空的,所以被阻塞)。
理解读一行功能
同理,若套接字的输入流中有多行信息,调用一次 readLine()
方法,只是读出当前的一行(当然你可以调用其他的"读"方法)。
程序演示
-
在
TCPServer.java
程序中多增加一条信息返回语句,例如:javapw.println("来自服务器:" + msg); // 下面多增加一条信息返回语句 pw.println("来自服务器,重复发送: " + msg);
然后启动服务端程序。
-
启动客户端
TCPClientFX
程序,发现客户显示区每次只显示一条信息,且与你发送的信息不同步。因为每一次互动,服务器返回两行信息,而客户端只是读取最前面的一行信息。
多线程技术
有了多线程技术,我们就有了更多选择。
编写读取服务器信息的线程
在 TCPClientFX.java
程序中,发送信息是可以通过"发送"按钮来实现主动控制,可接收信息是被动的,你不知道输入流中有多少信息。为此,在窗口程序中添加一个线程专门负责读取输入流中的信息,同时,"发送"按钮动作中,读取输入流信息的代码就需要删除。
操作步骤
-
现在右键选择
TCPClientFX.java
重构(Refactor),重命名为TCPClientThreadFX.java
(采用如图 3.5 所示的方式)。 -
在合适的位置添加如下线程代码(自己思考添加在什么位置合适,更新版本讲义会有更详细的提示),用于接收服务器的信息,为了简洁,匿名内部类使用了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(); // 启动线程
-
以上代码中有四点注意:
- 由于是新开的一个线程循环读取服务器的信息,所以不用考虑服务器是否有发欢迎信息,就算读取不到信息也只是阻塞这个线程,主程序本身使用没有任何影响(单线程就会卡住)。事实上服务器发多少信息都没问题,该线程通过循环语句来读取,没信息过来就阻塞等待,当服务器关闭连接时,就会跳出循环语句,结束本线程;
- 现在接收并显示服务端信息的任务交给了一个单独的线程,那么原来主线程中连接按钮和发送按钮的动作事件代码中,关于接收并显示服务端信息的代码还需要保留吗?这个取舍非常重要!
- 对于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 表达式来实现。例如,Runnable
和 Callable
都是函数式接口。
下面是一个使用 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类的介绍
- 避免长时间运行的任务阻塞 UI:在 UI 线程中执行长时间运行的任务会导致应用程序无响应。因此,应该将这些任务放在单独的线程中执行。
- 更新 UI 线程 :由于 UI 组件只能在 JavaFX 的主线程(UI 线程)中安全地更新,因此需要使用
Platform.runLater(Runnable)
方法来确保 UI 更新操作在正确的线程中执行。 - 线程的创建和管理 :可以通过继承
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 组件状态不一致。
这个方法通常在以下几种情况下使用:
-
从后台线程更新 UI :当你在后台线程中完成一项任务后,需要更新 UI 时,可以使用
Platform.runLater()
来确保更新操作在 UI 线程上执行。 -
延迟 UI 更新:有时候你可能需要在 UI 线程上延迟执行某些操作,比如在动画结束后更新 UI。
-
处理事件:在处理某些事件时,你可能需要在 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();
}
}