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

相关推荐
开心工作室_kaic18 分钟前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王20 分钟前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康25 分钟前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神1 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
黑客Ash1 小时前
【D01】网络安全概论
网络·安全·web安全·php
->yjy1 小时前
计算机网络(第一章)
网络·计算机网络·php
qq_327342731 小时前
Java实现离线身份证号码OCR识别
java·开发语言
摘星星ʕ•̫͡•ʔ2 小时前
计算机网络 第三章:数据链路层(关于争用期的超详细内容)
网络·计算机网络
阿龟在奔跑3 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list