JavaFx -- chapter04(网络文件传输)

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 来创建一个用于写入文件的输出流时,你可以直接将字节数据写入到一个文件中。FileOutputStreamOutputStream 的子类,专门用于将数据写入文件。

以下是如何使用 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);

以下是各个部分的解释:

  1. OutputStreamWriter

    • 它接受一个字节输出流(在这个例子中是 socketOut,即 Socket 的输出流)。
    • 它使用指定的字符集(这里是 StandardCharsets.UTF_8)将字节转换为字符。
  2. 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 循环一直无法退出,可能有几个原因:

  1. 服务器没有正确关闭连接 :服务器端在发送完数据后应该关闭其输出流(通常是 Socket 的输出流),这样客户端在读取完数据后会得到一个 -1,表示连接已经关闭。

  2. 客户端读取到非预期的数据:如果客户端读取到非预期的数据,如空数据或持续的心跳包,它可能会一直读取而不退出。

  3. 阻塞模式Socket 默认是阻塞模式,如果服务器端没有数据发送,客户端的 read() 方法将会一直阻塞等待。

  4. 异常处理:如果在读取过程中发生异常,并且没有正确处理,可能会导致循环无法退出。

  5. 服务器端代码逻辑:服务器端代码可能存在逻辑问题,没有正确处理文件发送后的连接关闭。

  6. 客户端代码逻辑:客户端代码可能没有正确处理从服务器接收到的数据,例如,如果服务器发送的数据超过了缓冲区的大小,可能会导致问题。

为了解决这个问题,你可以尝试以下步骤:

  • 确保服务器端发送完数据后关闭输出流

    java 复制代码
    // 服务器端发送完数据后
    socket.shutdownOutput();
  • 在客户端添加超时设置

    你可以为 Socket 设置读取超时,这样如果服务器在指定时间内没有发送数据,read() 方法将会超时并返回 -1。

    java 复制代码
    dataSocket.setSoTimeout(5000); // 设置读取超时为5000毫秒
  • 检查服务器端代码:确保服务器端在发送完文件后正确关闭了连接。

  • 检查客户端代码:确保客户端正确处理了从服务器接收到的数据。

  • 添加异常处理:确保你添加了适当的异常处理,以便在发生错误时能够正确处理。

  • 调试:使用调试工具或打印日志来查看循环中发生了什么,这可能会帮助你找到问题所在。

  • 检查网络问题:确保网络连接没有问题,没有丢包或连接不稳定的情况。

设计思路

文本通信占用2021端口,将socket的输入输出流封装成PrinterWriterBufferedReader方便使用

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,所以运行到子线程时报错--线程不可控性

相关推荐
浮游本尊38 分钟前
Java学习第22天 - 云原生与容器化
java
渣哥2 小时前
原来 Java 里线程安全集合有这么多种
java
间彧3 小时前
Spring Boot集成Spring Security完整指南
java
间彧3 小时前
Spring Secutiy基本原理及工作流程
java
Java水解4 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆6 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学6 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole7 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊7 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端