chapter04(网络文件传输)
BufferReader类
BufferedReader
是 Java 中用来包装一个 Reader
对象的类,它提供了一个缓冲区,可以提高读取文本数据的效率。BufferedReader
通常用于逐行读取文本文件,因为它提供了 readLine()
方法,该方法一次读取一行文本。
以下是 BufferedReader
的一些常见用法:
创建 BufferedReader
要使用 BufferedReader
,你首先需要创建一个实例。这通常是通过将现有的 Reader
对象(如 FileReader
)传递给 BufferedReader
的构造函数来完成的。
java
FileReader fileReader = new FileReader("path/to/file.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);
读取文本
使用 readLine()
方法逐行读取文本:
java
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
关闭 BufferedReader
读取完成后,应该关闭 BufferedReader
(以及它包装的 Reader
),以释放系统资源。
java
bufferedReader.close();
其他读取方法
除了 readLine()
,BufferedReader
还提供了其他方法来读取文本:
read()
:读取单个字符。read(char[] cbuf)
:将字符读入数组。read(char[] cbuf, int off, int len)
:从缓冲区读取字符到数组的某个部分。
示例代码
下面是一个使用 BufferedReader
读取文件内容的完整示例:
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample {
public static void main(String[] args) {
String filePath = "path/to/file.txt";
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们使用了 try-with-resources 语句来自动关闭 BufferedReader
。
注意事项
BufferedReader
是用于文本数据的,而不是二进制数据。- 读取操作可能会抛出
IOException
,因此需要适当的异常处理。 - 在读取大文件时,使用缓冲区可以显著提高性能,因为它减少了实际的磁盘访问次数。
如果你在处理二进制文件,应该使用 BufferedInputStream
或其他相关的输入流类。
FileOutputStream类
当你使用 FileOutputStream
来创建一个用于写入文件的输出流时,你可以直接将字节数据写入到一个文件中。FileOutputStream
是 OutputStream
的子类,专门用于将数据写入文件。
以下是如何使用 FileOutputStream
来写入数据到文件的示例:
创建 FileOutputStream
首先,你需要创建一个 FileOutputStream
实例,指定你想要写入数据的文件。
java
import java.io.FileOutputStream;
import java.io.File;
File saveFile = new File("path/to/your/file.txt");
FileOutputStream fileOut = new FileOutputStream(saveFile);
写入数据
使用 write()
方法将字节数据写入文件。
java
byte[] data = ...; // 这里是你要写入文件的数据
fileOut.write(data);
关闭 FileOutputStream
完成写入操作后,应该关闭 FileOutputStream
以释放系统资源。
java
fileOut.close();
示例代码
下面是一个完整的示例,演示如何使用 FileOutputStream
将字节数据写入文件:
java
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
public class FileWriteExample {
public static void main(String[] args) {
File saveFile = new File("path/to/your/file.txt");
byte[] data = ...; // 这里是你要写入文件的数据
try (FileOutputStream fileOut = new FileOutputStream(saveFile)) {
fileOut.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们使用了 try-with-resources 语句来自动关闭 FileOutputStream
。
注意事项
- 确保在写入数据前文件路径是有效的,并且你有足够的权限写入文件。
- 写入操作可能会抛出
IOException
,因此需要适当的异常处理。 - 如果文件不存在,
FileOutputStream
将会创建它。 - 如果文件已经存在,使用
FileOutputStream
写入将会覆盖原有内容。如果你想追加到现有文件,应该使用FileOutputStream
的另一个构造函数:new FileOutputStream(file, true)
。
这样,你就可以使用 FileOutputStream
将字节数据写入到文件中了。
向socket写入字符串
在 Java 中,PrintWriter
是一个方便的类,用于向流写入字符数据。它支持方法如 print()
、println()
和 printf()
,这些方法可以方便地将各种数据类型转换为字符串并写入流。
OutputStreamWriter
是一个将字节流转换成字符流的桥梁,它使用指定的字符集将字节数据解码为字符数据。OutputStreamWriter
本身不缓存输出,因此如果你需要提高效率,通常会将它包装在一个 BufferedWriter
中。
在你的代码示例中:
java
new PrintWriter(
new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);
以下是各个部分的解释:
-
OutputStreamWriter
:- 它接受一个字节输出流(在这个例子中是
socketOut
,即Socket
的输出流)。 - 它使用指定的字符集(这里是
StandardCharsets.UTF_8
)将字节转换为字符。
- 它接受一个字节输出流(在这个例子中是
-
PrintWriter
:- 它接受一个
Writer
对象(这里是OutputStreamWriter
的实例)。 - 第二个参数
true
表示使用自动刷新模式。这意味着每当缓冲区满了或者新的行分隔符被写入时,PrintWriter
会自动刷新其内部缓冲区。这对于网络应用程序很有用,因为它可以确保数据及时发送到网络上。
- 它接受一个
示例代码
下面是一个完整的示例,演示如何使用 PrintWriter
通过 Socket
发送字符串数据:
java
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SocketExample {
public static void main(String[] args) {
try (Socket socket = new Socket("hostname", port)) {
// 获取 Socket 的输出流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
// 创建 PrintWriter,自动刷新模式
PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);
// 发送字符串数据
printWriter.println("Hello, World!");
// 关闭 PrintWriter,它会自动刷新并关闭 OutputStreamWriter
printWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意事项
- 自动刷新 :
PrintWriter
的自动刷新模式非常有用,因为它确保数据及时发送。 - 字符集 :使用
StandardCharsets.UTF_8
可以确保文本数据在不同平台和语言环境中的一致性。 - 异常处理:网络操作可能会抛出异常,因此需要适当的异常处理。
- 资源管理 :使用 try-with-resources 语句可以确保
Socket
在使用后被正确关闭。
这样,你就可以使用 PrintWriter
通过 Socket
发送字符串数据了。
读取socket字节数据返回字符
java
private BufferedReader getReader(Socket socket) throws IOException {
//获得输入流缓冲区的地址
InputStream socketIn = socket.getInputStream();
//读取字节数据返回字符串
return new BufferedReader(
new InputStreamReader(socketIn, StandardCharsets.UTF_8));
}
关闭通信套接字
java
public void getFile(File saveFile) throws IOException {
if (dataSocket != null) {
FileOutputStream fileOut = new FileOutputStream(saveFile); // 新建本地空文件
byte[] buf = new byte[1024]; // 用来缓存接收的字节数据
// (2)向服务器发送请求的文件名,字符串读写功能
pw.println("require " + saveFile.getName());
pw.flush(); // 确保数据发送到服务器
// (3)接收服务器的数据文件,字节读写功能
int size;
// 这里服务器端必须退出输出流,要不然会一直读取
while ((size = bir.read(buf)) != -1) { // 读一块到缓存,读取结束返回-1
fileOut.write(buf, 0, size); // 写一块到文件
System.out.println("读取到的数据大小" + size);
}
System.out.println("getfile函数结束了");
fileOut.flush(); // 关闭前将缓存的数据全部推出
fileOut.close(); // 关闭文件输出流
}
}
如果你发现 while
循环一直无法退出,可能有几个原因:
-
服务器没有正确关闭连接 :服务器端在发送完数据后应该关闭其输出流(通常是
Socket
的输出流),这样客户端在读取完数据后会得到一个 -1,表示连接已经关闭。 -
客户端读取到非预期的数据:如果客户端读取到非预期的数据,如空数据或持续的心跳包,它可能会一直读取而不退出。
-
阻塞模式 :
Socket
默认是阻塞模式,如果服务器端没有数据发送,客户端的read()
方法将会一直阻塞等待。 -
异常处理:如果在读取过程中发生异常,并且没有正确处理,可能会导致循环无法退出。
-
服务器端代码逻辑:服务器端代码可能存在逻辑问题,没有正确处理文件发送后的连接关闭。
-
客户端代码逻辑:客户端代码可能没有正确处理从服务器接收到的数据,例如,如果服务器发送的数据超过了缓冲区的大小,可能会导致问题。
为了解决这个问题,你可以尝试以下步骤:
-
确保服务器端发送完数据后关闭输出流:
java// 服务器端发送完数据后 socket.shutdownOutput();
-
在客户端添加超时设置 :
你可以为
Socket
设置读取超时,这样如果服务器在指定时间内没有发送数据,read()
方法将会超时并返回 -1。javadataSocket.setSoTimeout(5000); // 设置读取超时为5000毫秒
-
检查服务器端代码:确保服务器端在发送完文件后正确关闭了连接。
-
检查客户端代码:确保客户端正确处理了从服务器接收到的数据。
-
添加异常处理:确保你添加了适当的异常处理,以便在发生错误时能够正确处理。
-
调试:使用调试工具或打印日志来查看循环中发生了什么,这可能会帮助你找到问题所在。
-
检查网络问题:确保网络连接没有问题,没有丢包或连接不稳定的情况。
设计思路
文本通信占用2021端口
,将socket的输入输出流封装成PrinterWriter
和BufferedReader
方便使用
java
// 得到网络输出字节流地址,并封装成网络输出字符流
// 设置最后一个参数为true,表示自动flush数据
OutputStream socketOut = socket.getOutputStream();
// 将字符转成字节数据输出到流中
pw = new PrintWriter(new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);
// 得到网络输入字节流地址,并封装成网络输入字符流
InputStream socketIn = socket.getInputStream();
// 将socket的字节(输出)转变成字符
br = new BufferedReader(new InputStreamReader(socketIn, StandardCharsets.UTF_8));
文件数据通信占用2020端口
,向socket写入依旧是字符转成字节,但是获取socket的数据(字节),直接使用字节类型数据写入到文件流对象(或者包装一下,但都是字节数据
)
java
private final Socket dataSocket;
private final PrintWriter pw; // 定义字符输出流
private final BufferedInputStream bir; // 定义字符输入流
public FileDataClient(String ip, String port) throws IOException {
dataSocket = new Socket(ip, Integer.parseInt(port));
// 得到网络输出字节流地址,并封装成网络输出字符流
// 设置最后一个参数为true,表示自动flush数据
OutputStream socketOut = dataSocket.getOutputStream();
pw = new PrintWriter(new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);
// 得到网络输入字节流地址
InputStream socketIn = dataSocket.getInputStream();
bir = new BufferedInputStream(socketIn);
}
线程设计
- 服务器端
msgThread
: 用于接收客户端请求构建通信套接字并监听/发送信息fileThread
: 用于接收客户端的请求构建用于文件数据通信
的通信套接字并监听/发送信息(数据)
- 客户端
- 主线程: UI更新
- 子线程:
receiveMsgThread
-> 用于接收服务器信息的单独线程,持续监听接收信息,实时显示
最终代码
FileClientFx
java
package client;
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();
}
}
FileDialogClient
java
package client;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class FileDialogClient {
private final Socket socket; // 定义套接字
private final PrintWriter pw; // 定义字符输出流
private final BufferedReader br; // 定义字符输入流
public FileDialogClient(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();
}
}
}
FileDataClient
java
package client;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class FileDataClient {
private final Socket dataSocket;
private final PrintWriter pw; // 定义字符输出流
private final BufferedInputStream bir; // 定义字符输入流
public FileDataClient(String ip, String port) throws IOException {
dataSocket = new Socket(ip, Integer.parseInt(port));
// 得到网络输出字节流地址,并封装成网络输出字符流
// 设置最后一个参数为true,表示自动flush数据
OutputStream socketOut = dataSocket.getOutputStream();
pw = new PrintWriter(new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);
// 得到网络输入字节流地址
InputStream socketIn = dataSocket.getInputStream();
bir = new BufferedInputStream(socketIn);
}
public void getFile(File saveFile) throws IOException {
if (dataSocket != null) {
FileOutputStream fileOut = new FileOutputStream(saveFile); // 新建本地空文件
byte[] buf = new byte[1024]; // 用来缓存接收的字节数据
// (2)向服务器发送请求的文件名,字符串读写功能
pw.println("require " + saveFile.getName());
pw.flush(); // 确保数据发送到服务器
// (3)接收服务器的数据文件,字节读写功能
int size;
// 这里服务器端必须退出输出流,要不然会一直读取
// 直接使用dataSocket.getInputStream()也可以
while ((size = bir.read(buf)) != -1) { // 读一块到缓存,读取结束返回-1
fileOut.write(buf, 0, size); // 写一块到文件
System.out.println("读取到的数据大小" + size);
}
System.out.println("getfile函数结束了");
fileOut.flush(); // 关闭前将缓存的数据全部推出
fileOut.close(); // 关闭文件输出流
}
}
}
FileDialogServer
java
package server;
import java.io.*;
import java.math.RoundingMode;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.Scanner;
public class FileDialogServer {
public static ServerSocket msgserverSocket = null;
public static ServerSocket fileserverSocket = null;
public void fileListPushToClient(PrintWriter pw) {
String path = "d:/ftpserver"; // 给出服务器下载目录路径
File filePath = new File(path);
if (!filePath.exists()) { // 路径不存在则返回
System.out.println("ftp下载目录不存在");
return;
}
if (!filePath.isDirectory()) { // 如果不是一个目录就返回
System.out.println("不是一个目录");
return;
}
// 开始显示目录下的文件,不包括子目录
String[] fileNames = filePath.list();
File tempFile;
// 格式化文件大小输出,不保留小数,不用四舍五入,有小数位就进1
DecimalFormat formater = new DecimalFormat();
formater.setMaximumFractionDigits(0);
formater.setRoundingMode(RoundingMode.CEILING);
for (String fileName : fileNames) {
tempFile = new File(filePath, fileName);
if (tempFile.isFile()) {
pw.println(fileName + " " + formater.format(tempFile.length() / (1024.0)) + "KB");
}
}
}
private PrintWriter getWriter(Socket socket) throws IOException {
//获得输出流缓冲区的地址
OutputStream socketOut = socket.getOutputStream();
//将字符转为字节写入到socket
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 msgService() throws IOException {
msgserverSocket = new ServerSocket(2021);
System.out.println("Server is running on port 2021");
Thread msgThread = new Thread(() -> {
while (true) {
Socket socket = null;
try {
socket = msgserverSocket.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("New client connected");
try {
PrintWriter pw = getWriter(socket);
fileListPushToClient(pw);
BufferedReader br = getReader(socket);
String msg;
while ((msg = br.readLine()) != null) {
if ("bye".equals(msg)) {
break;
}
// 处理其他消息
}
} catch (Exception e) {
System.out.println("Error while handling client: " + e.getMessage());
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}, "msgThread");
msgThread.start();
}
public void fileService() throws IOException {
fileserverSocket = new ServerSocket(2020);
System.out.println("fileServer is running on port 2020");
Thread fileThread = new Thread(() -> {
while (true) {
Socket socket = null;
try {
socket = fileserverSocket.accept();
System.out.println("New file client connected");
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
PrintWriter pw = getWriter(socket);
fileListPushToClient(pw);
BufferedReader br = getReader(socket);
String msg;
while ((msg = br.readLine()) != null) {
if (msg.startsWith("require ")) {
System.out.println(msg);
// 服务器请求文件
String fileName = msg.substring(8);
File requiredFile = new File("d:/ftpserver/" + fileName);
// 读取文件
Scanner sc = new Scanner(requiredFile, "UTF-8");
while (sc.hasNextLine()) { // 使用hasNextLine()确保换行符不会重复添加
pw.println(sc.nextLine()); // 输出文件的内容,字节类型
}
}
System.out.println("文件没内容了,哥们");
socket.close();
// 处理其他消息
}
} catch (Exception e) {
System.out.println("Error while handling client: " + e.getMessage());
e.printStackTrace();
} finally {
try {
socket.close();
System.out.println("socket关闭");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
});
fileThread.start();
}
public static void main(String[] args) {
try {
FileDialogServer server = new FileDialogServer();
server.msgService();
server.fileService();
} catch (Exception e) {
e.printStackTrace();
}
}
}
点击退出报错Socket closed
原因
因为直接点击退出可能会发送bye
之后立刻执行到关闭socket
,但是子线程还在阻塞等待读写socket,所以运行到子线程时报错--线程不可控性