chapter0(课程前导)

  • 查分以及登陆端口为9009

  • 服务器文件传输端口为2021,2020

0zxm/JavaFx at master

写文章-CSDN博客

final关键字

在Java中,final关键字是一个修饰符,它可以用来修饰类、方法和变量,具有不同的含义:

  1. 修饰变量

    • 当修饰一个类变量(静态变量)时,表示这个变量一旦被初始化后,其值就不能被改变。
    • 当修饰一个实例变量时,表示这个变量在初始化后不能被修改,即它是一个常量。
    public final int MY_CONSTANT = 42; // 类变量
    public final int value; // 实例变量,必须在声明时或构造器中初始化
  2. 修饰方法

    • 表示这个方法不能被子类覆盖。如果一个类的方法被声明为final,那么子类将不能重写这个方法。
    public final void myMethod() {
        // 方法体
    }
  3. 修饰类

    • 表示这个类不能被继承。如果一个类被声明为final,那么它不能被其他类继承。
    public final class MyClass {
        // 类体
    }
  4. 修饰本地变量

    • 表示这个局部变量一旦被赋值后就不能再被修改。
    public void myMethod() {
        final int myLocalVariable = 10;
        // myLocalVariable = 20; // 这行代码会编译错误
    }
  5. 修饰泛型

    • 在泛型中使用final可以限制类型参数必须是最终类型(final type),即不能是任何类的子类。
    public <T extends final Number> void myMethod(T t) {
        // T 必须是 Number 或其子类的最终类型
    }

使用final关键字可以提高代码的安全性和清晰度,因为它限制了变量和方法的修改,有助于防止错误和意外的副作用。同时,它还可以帮助编译器优化代码,因为编译器知道final变量的值不会改变,可以进行一些额外的优化。

chapter01(简单窗口搭建及文件读写)

JavaFX 是一个用于构建富客户端应用程序的框架,提供了一种现代化的方式来创建桌面应用程序和互联网应用程序。以

创建JavaFx项目

​ 在Java体系中,最常用的图形界面设计库主要是SwingJavaFX,本课程使用JavaFX,采用手写代码方式创建窗体界面,建议的jdk版本为jdk8 (因为jdk8已经内置JavaFX库,高版本JDK中被剥离,需要额外下载jar 包),https://oc.gdufs.edu.cn 教学资源站点可下载JDK8(如果使用下面所述 的idea2018 整合版,则不用下载,整合版已经包含)。

  1. 启动Intellij Idea(以后简称 idea),新建项目之前,首先设置默认 的编码为utf-8
  2. 新建一个项目,如图1.3所示,左方选择“Java”,右方不要勾选任何Additional Libraries and Frameworks,按提示一路 next,选择一个合适的项目名完成新建过程,例如NetworkApp。
  3. 新建JavaFX源程序,命名为SimpleFX;鼠标右键点击对应的包chapter01,在弹出菜单中选择``New -> JavaFXApplication`,然后命名即可。

image-20240903174728085

Application类

JavaFx的Application类是主程序类,是JavaFX应用程序的入口点,里面封装了一系列成员方法.

launch()和start()方法

  • launch()方法: 启动 JavaFX 应用程序launch 方法负责初始化 JavaFX 应用程序的生命周期。它会调用 Application 类的 start 方法,这是你定义应用程序界面的地方。

    • 当调用 launch 方法时,JavaFX 会创建一个新的应用程序线程,并在该线程中调用 start 方法。
  • start()方法,参数是一个 Stage primaryStage 对象,不需要你自己调用此函数,会被 launch 方法间接调用,用于初始化应用程序

    • Stage primaryStage:这是应用程序的主窗口,所有的用户界面组件都将添加到这个窗口中

    • start 方法是应用程序的入口点,你可以在这里设置用户界面和处理事件。

    • start 方法执行完成后,JavaFX 应用程序会进入事件循环,等待用户交互。

通常在main函数中调用launch方法

import javafx.application.Application;  
import javafx.scene.Scene;  
import javafx.scene.control.Button;  
import javafx.scene.layout.StackPane;  
import javafx.stage.Stage;  

public class HelloWorld extends Application {  
    @Override  
    public void start(Stage primaryStage) {  
        Button btn = new Button();  
        btn.setText("说 '你好'");  
        btn.setOnAction(event -> System.out.println("你好,世界!"));  

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

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

        primaryStage.setTitle("Hello World!");  
        primaryStage.setScene(scene);  
        primaryStage.show();  
    }  

    public static void main(String[] args) {  
        launch(args); // 启动 JavaFX 应用程序,调用 start 方法  
    }  
}

setStyle()方法

几乎所有的JavaFx控件都可以使用 setStyle()方法 设置样式,参数为类似于CSS样式表语法的字符串

窗体创建

SimpleFx类中,可以定义成员变量,语法如private Button btnExit = new Button("退出");

设置窗口标题

start方法中使用primaryStage.setTitle();设置标题

Button按钮类

  • Button类是JavaFx中非常常见的一个类,构造函数的参数是String类型,用于显示按钮的文本,或者可以通过setText()方法来修改里面的文字
  • setOnAction() 方法,用于处理按钮点击事件

在JavaFX中,处理按钮点击事件通常涉及到为按钮设置动作监听器。如果事件处理逻辑较为简单,直接在setOnAction方法中通过匿名内部类或Lambda表达式来实现是一种非常简洁的方式。以下是如何在JavaFX中分别使用匿名内部类和Lambda表达式来处理四个按钮点击事件的示例。

使用匿名内部类

假设你有四个按钮button1, button2, button3, button4,你可以分别为它们设置动作监听器,如下所示:

button1.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Button 1 clicked!");
    }
});

button2.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Button 2 clicked!");
    }
});

// 对button3和button4的处理类似...
使用Lambda表达式

如果你的Java版本支持Lambda表达式(Java 8及以上),并且事件处理逻辑简单,那么使用Lambda表达式可以使得代码更加简洁。以下是如何使用Lambda表达式来设置相同按钮的动作监听器:

button1.setOnAction(event -> System.out.println("Button 1 clicked!"));

button2.setOnAction(event -> System.out.println("Button 2 clicked!"));

// 对button3和button4的处理类似...
单独写一个内部类

如果事件处理逻辑较为复杂,或者需要在多个地方重用相同的处理逻辑,那么将事件处理逻辑封装到一个单独的内部类中会是一个更好的选择。以下是如何实现这一点的示例:

class MyButtonHandler implements EventHandler<ActionEvent> {
    private String buttonName;

    public MyButtonHandler(String buttonName) {
        this.buttonName = buttonName;
    }

    @Override
    public void handle(ActionEvent event) {
        System.out.println(buttonName + " clicked!");
        // 这里可以添加更复杂的逻辑
    }
}

// 然后为按钮设置动作监听器
button1.setOnAction(new MyButtonHandler("Button 1"));
button2.setOnAction(new MyButtonHandler("Button 2"));

// 对button3和button4的处理类似...

通过这种方式,你可以将事件处理逻辑从界面构建代码中分离出来,使得代码更加模块化和易于维护。同时,通过为MyButtonHandler类添加更多的属性和方法,你还可以扩展其功能,以支持更复杂的事件处理场景。

Lable类

new Label("信息显示区:")创建一个显示信息的区域

TextField文本编辑框

private TextField tfSend = new TextField();方法用于创建一个文本编辑框

TextArea文本显示框

private TextArea taDisplay= new TextArea();方法用于创建一个文本显示框

  • setEditable()方法: 设置是否只读属性

VBox垂直布局(容器类)

VBox 是 JavaFX 中的一个布局容器类,用于垂直排列其子节点(通过vBox.getChildren().addAll()方法)。它是 javafx.scene.layout 包的一部分,提供了一种简单的方式来组织和管理用户界面组件的布局

1. 基本特性
  • 垂直排列VBox 会将其子节点从上到下垂直排列,子节点的顺序与添加顺序相同。
  • 自动调整大小VBox 会根据其子节点的大小自动调整自身的宽度和高度。
  • 间距和对齐:可以设置子节点之间的间距以及对齐方式。
2. 常用属性
  • spacing:设置子节点之间的间距(以像素为单位)。
  • alignment:设置子节点的对齐方式,例如顶部对齐、底部对齐、居中对齐等。
  • padding:设置 VBox 内部边距,控制内容与边框之间的距离。(类似于CSS中的padding属性)
3. 对齐方式

VBox 的对齐方式可以通过 setAlignment 方法设置,常用的对齐方式包括:

Pos.TOP_LEFTPos.TOP_CENTERPos.TOP_RIGHTPos.CENTER_LEFTPos.CENTERPos.CENTER_RIGHTPos.BOTTOM_LEFTPos.BOTTOM_CENTERPos.BOTTOM_RIGHT

HBox(水平容器)

类似于上面的VBox,只不过是水平排列子元素

Stage,Scene和Pane的关系

在 JavaFX 中,StageScenePane 是三个核心概念,它们之间有明确的层次关系和功能分工。以下是它们之间的关系和各自的作用:

1.Stage(舞台)

  • 定义Stage 是 JavaFX 应用程序的顶级容器,代表一个独立的窗口。
  • 作用:它用于管理窗口的显示、隐藏、大小、位置、标题等属性。
  • 特点
    • 一个 Stage 对应一个独立的窗口。
    • Stage 是 JavaFX 应用程序的入口点,所有 UI 元素都必须放在 Stage 中。
    • 可以设置模态(Modality),控制是否允许用户与主应用程序交互。

2.Scene(场景)

  • 定义SceneStage 的直接子元素,代表舞台上的内容区域。
  • 作用:它用于包含和管理 UI 元素(如按钮、文本框等)。
  • 特点
    • 一个 Stage 只能有一个 Scene
    • Scene 是 UI 元素的容器,所有 UI 元素都必须放在 Scene 中。
    • 可以设置 Scene 的宽度和高度,以及背景颜色等属性。

3.Pane(布局容器)

  • 定义Pane 是一种布局容器,用于放置和管理 UI 元素。
  • 作用:它用于组织 UI 元素的布局,如 VBoxHBoxGridPane 等。
  • 特点
    • 一个 Scene 可以包含一个或多个 Pane
    • Pane 是 UI 元素的直接父容器,用于定义它们的排列方式。
    • 不同类型的 Pane 提供不同的布局策略,例如 VBox 垂直排列,HBox 水平排列,GridPane 网格排列等。

它们之间的关系

  1. 层次结构
    • Stage 是最外层的容器,代表一个窗口。
    • SceneStage 的直接子元素,代表窗口的内容区域。
    • PaneScene 的子元素,用于放置和管理具体的 UI 元素。
  2. 依赖关系
    • Stage 必须包含一个 Scene
    • Scene 必须包含一个根布局容器(如 Pane)。
    • UI 元素必须放置在某个 Pane 中。

窗体布局代码

import javafx.application.Application;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class SimpleFx extends Application {

    private Button btnExit = new Button("退出");

    private Button btnSend = new Button("发送");
    private Button btnOpen = new Button("加载");
    private Button btnSave = new Button("保存");

    //待发送信息的文本框
    private TextField tfSend = new TextField();
    //显示信息的文本区域
    private TextArea taDisplay = new TextArea();


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

        // 设置按钮的交互效果
        btnExit.setOnAction(event -> {System.exit(0);});
        btnSend.setOnAction(event -> {
            String msg = tfSend.getText();
            taDisplay.appendText(msg + "\n");
            tfSend.clear();
        });

        tfSend.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) {
                String text = tfSend.getText();
                String prefix = event.isShiftDown() ? "echo: " : "";
                taDisplay.appendText(prefix + text + "\n");
                tfSend.setText(""); // 清空文本框
            }
        });

        hBox.setAlignment(Pos.CENTER_RIGHT);
        hBox.getChildren().addAll(btnSend, btnSave, btnOpen, btnExit);
        mainPane.setBottom(hBox);
        Scene scene = new Scene(mainPane, 700, 400);

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


    }

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

}

文本读写

  1. 新增一个文件操作类TextFileIO,负责文件操作的相关功能,至少实现 append方法load方法用于保存和读取文件;
  2. SimpleFX类中的合适位置将TextFileIO类实例化为textFileIO,在“保存”按钮的响应事件代码中添加相应功能

FileChooser类

FileChooser fileChooser = new FileChooser();
File file = fileChooser.showSaveDialog(null);

FileChooser 类是一个用于让用户通过图形用户界面(GUI)选择文件的工具。你提供的代码片段中,FileChooser 被实例化,并调用其 showSaveDialog(null) 方法来显示一个保存文件的对话框。

这里的 null 参数传递给 showSaveDialog 方法,意味着没有将对话框的父窗口(或所有者)设置为特定的窗口。这通常意味着对话框会作为一个顶级窗口出现,不依赖于任何其他窗口。

showSaveDialog 方法会阻塞当前线程(在JavaFX应用中通常是JavaFX应用线程),直到用户关闭对话框。根据用户的操作,该方法会返回:

  • 如果用户选择了文件并点击了“保存”或类似的确认按钮,则返回一个代表用户所选文件的 File 对象。
  • 如果用户取消了操作(例如,点击了“取消”按钮或关闭了对话框),则返回 null

PrintWriter类

  1. new FileOutputStream(file, true)
    • 创建一个 FileOutputStream 对象,该对象用于将字节写入到指定的文件(file)。
    • 第二个参数 true 表示以追加模式打开文件。如果文件不存在,则尝试创建该文件。如果文件已存在,则写入的数据会被追加到文件内容的末尾,而不是覆盖原有内容。
  2. new OutputStreamWriter(…, “UTF-8”)
    • 创建一个 OutputStreamWriter 对象,该对象是将字符流转换为字节流的桥梁。它使用指定的字符编码(在这个例子中是 “UTF-8”)将字符转换为字节。
    • 它接收一个 OutputStream 对象(在这个例子中是 FileOutputStream)作为参数。
  3. new PrintWriter(…)
    • 创建一个 PrintWriter 对象,该对象可以向文本输出流打印字符的便捷方式。
    • 它接收一个 Writer 对象(在这个例子中是 OutputStreamWriter)作为参数
PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8"));

这行代码就是创建一个PrintWriter对象,可以将字符转变成字节在写入到文件中;

StringBuilder类

StringBuilder 是 Java 中的一个可变字符序列类,它位于 java.lang 包中。与 String 类不同,StringBuilder 的内容是可以修改的。当你需要频繁地对字符串进行修改(如拼接、删除、替换等操作)时,使用 StringBuilder 会比使用 String 更高效,因为 String 是不可变的,每次修改都会生成一个新的字符串对象,而 StringBuilder 的修改是在原对象上进行的,不会创建新的对象。

StringBuilder 提供了一系列的方法来操作字符序列,比如:

  • append(CharSequence csq): 将指定的字符序列追加到此字符序列的末尾。
  • append(CharSequence csq, int start, int end): 追加指定 CharSequence 的子序列到此序列的末尾。
  • insert(int offset, char c): 在此序列的指定位置插入指定的 char 值。
  • delete(int start, int end): 移除此序列的子字符串中的字符。
  • replace(int start, int end, String str): 用给定 String 中的字符替换此序列的子字符串中的字符。

StringBuilder 的构造方法有几个版本,最常用的两个是:

  1. StringBuilder(): 构造一个空的 StringBuilder
  2. StringBuilder(int capacity): 构造一个具有指定初始容量的 StringBuilder。这里的容量指的是 StringBuilder 可以容纳的字符数,但它会自动扩容以容纳更多的字符。

示例代码:

StringBuilder sb = new StringBuilder(); // 创建一个空的StringBuilder
sb.append("Hello, "); // 追加字符串
sb.append("World!"); // 继续追加字符串
System.out.println(sb.toString()); // 输出: Hello, World!

// 使用指定容量的构造方法
StringBuilder sbWithCapacity = new StringBuilder(100); // 初始容量为100
sbWithCapacity.append("This is a longer string than the previous one.");
System.out.println(sbWithCapacity.toString()); // 输出: This is a longer string than the previous one.

StringBuilder 是线程不安全的,如果在多线程环境下需要频繁操作字符串,建议使用 StringBuffer,它是 StringBuilder 的线程安全版本。但在单线程环境下,StringBuilder 的性能通常比 StringBuffer 要好。

文本读写部分代码

import javafx.stage.FileChooser;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Scanner;

public class TextFileIO {

    // 注意:这里移除了private PrintWriter pw; 和 private Scanner sc; 字段,因为我们在方法内部管理它们  

    // 内容添加到文件中,文件通过对话框来确定

    public TextFileIO(){};  // 空构造方法
    public void append(String msg) {
        FileChooser fileChooser = new FileChooser();
        File file = fileChooser.showSaveDialog(null);
        if (file == null) { // 用户放弃操作则返回  
            return;
        }

        // 使用try-with-resources语句自动关闭资源  
        try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8"))) {
            pw.println(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 从文件中加载内容  
    public String load() {
        FileChooser fileChooser = new FileChooser();
        File file = fileChooser.showOpenDialog(null);
        if (file == null) { // 用户放弃操作则返回  
            return null;
        }

        StringBuilder sb = new StringBuilder();
        try (Scanner sc = new Scanner(file, "UTF-8")) {
            while (sc.hasNextLine()) { // 使用hasNextLine()确保换行符不会重复添加  
                sb.append(sc.nextLine()).append("\n"); // 补上行读取的行末尾回车  
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 移除字符串末尾可能存在的多余换行符  
        if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n') {
            sb.setLength(sb.length() - 1);
        }

        return sb.toString();
    }
}

chapter02(网络对话)

简单网络对话程序

设计任务:客户端向服务器发送字符串,并能读取服务器返回的字符串。

知识点:TCP套接字技术,C/S软件架构程序设计

重点理解:Java客户套接字类Socket和服务器套接字类ServerSocket,以及配套使用流的读/写类BufferedReader/PrintWriter

C/S软件架构程序设计技术中,实现网络通信的两个应用进程,一个叫做服务进程,另一个叫做客户进程,如图所示。服务进程首先被动打开一个 监听端口,如8008,客户进程主动访问这个端口,完成对话聊天前的TCP三 次握手连接。

image-20240904094650896

Java 的 TCP/IP 套接字编程将底层的细节进行了封装,其编程模型如图

image-20240904094922114

socket套接字类

Java TCP/IP 编程模型中,有两个套接字类:服务进程中的是 ServerSocket 类客户进程中的是Socket类。 服务进程首先开启一个或多个监听端口,客户进程向服务进程发起TCP三次握手连接。

TCP 连接成功后,逻辑上可理解为通信进程的双方具有两个流(输出流和输入流)。逻辑上可将两个流理解为两个通信管道的全双工通信模式,一个用于向对方发送数据,另一个用于接收对方的数据。

成员方法

套接字类有两个基本的方法可以获得两个通信管道:

  1. socket.getInputStream() 方法可获得输入字节流的入口;
  2. socket.getOutputStream() 方法可获得输出字节流的出口;

监听套接字和通信套接字

在网络编程中,监听套接字(Listening Socket)和通信套接字(Communication Socket)是两个重要的概念,通常用于实现客户端和服务器之间的通信。以下是对这两个概念的详细解释:

1. 监听套接字(Listening Socket)
  • 定义:监听套接字是服务器端创建的套接字,用于监听来自客户端的连接请求。它不直接用于数据传输,而是用于接受连接。
  • 创建:在服务器端,首先需要创建一个套接字并将其绑定到一个特定的地址和端口,然后调用 listen() 方法使其进入监听状态。
  • 功能
    • 等待客户端的连接请求。
    • 一旦有客户端请求连接,服务器会接受这个连接并创建一个新的通信套接字。

示例代码(C++):

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建监听套接字
    if (server_fd == 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口
    address.sin_port = htons(8080); // 端口号

    bind(server_fd, (struct sockaddr *)&address, sizeof(address)); // 绑定地址和端口
    listen(server_fd, 3); // 开始监听,最大连接数为3

    std::cout << "Listening on port 8080..." << std::endl;

    return 0;
}
2. 通信套接字(Communication Socket)
  • 定义:通信套接字是服务器在接受客户端连接请求后创建的套接字,用于与特定客户端进行数据传输。
  • 创建:当监听套接字接受到连接请求后,服务器会调用 accept() 方法,返回一个新的套接字(通信套接字),用于与该客户端进行通信。
  • 功能
    • 进行数据的发送和接收。
    • 每个连接的客户端都有一个独立的通信套接字。
  • 示例代码(C++):
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    
    // 省略监听套接字的创建和绑定代码...

    listen(server_fd, 3);
    std::cout << "Listening on port 8080..." << std::endl;

    int client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); // 接受连接
    if (client_socket < 0) {
        std::cerr << "Accept failed" << std::endl;
        return -1;
    }

    const char *message = "Hello from server";
    send(client_socket, message, strlen(message), 0); // 发送数据
    std::cout << "Message sent to client" << std::endl;

    close(client_socket); // 关闭通信套接字
    close(server_fd); // 关闭监听套接字

    return 0;
}
总结
  • 监听套接字:用于等待和接受客户端的连接请求。
  • 通信套接字:用于与已连接的客户端进行数据传输。

这种设计使得服务器能够同时处理多个客户端连接,因为每个连接都有自己的通信套接字,而监听套接字则保持在监听状态,等待新的连接请求。如果你有更多问题或需要进一步的帮助,请告诉我!

模拟Button的点击事件

在JavaFX中,模拟按钮(Button)的点击可以通过直接调用按钮的fire()方法来实现,但请注意,Button类本身并没有直接提供fire()方法。不过,Button类继承自javafx.scene.control.Control,而Control类有一个受保护的fire()方法,但这个方法通常不是用来直接模拟用户点击事件的。

更好的方法是定义一个可以在代码中调用的方法,该方法包含你想要在按钮点击时执行的代码。然后,你可以在按钮的事件监听器中调用这个方法,也可以直接从代码的其他部分调用它。

import javafx.application.Application;  
import javafx.event.ActionEvent;  
import javafx.event.EventHandler;  
import javafx.scene.Scene;  
import javafx.scene.control.Button;  
import javafx.scene.layout.StackPane;  
import javafx.stage.Stage;  
  
public class ButtonClickSimulationExample extends Application {  
  
    @Override  
    public void start(Stage primaryStage) {  
        Button btn = new Button();  
        btn.setText("Click Me!");  
  
        // 定义点击时执行的方法  
        EventHandler<ActionEvent> onButtonClick = event -> {  
            System.out.println("Button clicked!");  
            // 这里可以放置更多的代码  
        };  
  
        // 将事件监听器绑定到按钮  
        btn.setOnAction(onButtonClick);  
  
        // 直接从代码中“模拟”按钮点击  
        // 注意:这不是真正的模拟点击,而是直接调用了点击时要执行的代码  
        onButtonClick.handle(new ActionEvent());  
  
        StackPane root = new StackPane();  
        root.getChildren().add(btn);  
        Scene scene = new Scene(root, 300, 250);  
  
        primaryStage.setTitle("Button Click Simulation Example");  
        primaryStage.setScene(scene);  
        primaryStage.show();  
    }  
  
    public static void main(String[] args) {  
        launch(args);  
    }  
}

可以通过设置button的disable属性来控制是否能点击按钮

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class DisableButtonExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        Button btn = new Button("点击我");
        
        // 按钮点击事件
        btn.setOnAction(event -> {
            btn.setDisable(true);
            btn.setText("按钮已禁用");
            
            // 假设这里有一个异步操作,完成后重新启用按钮
            new Thread(() -> {
                try {
                    Thread.sleep(2000); // 模拟异步操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // 重新启用按钮
                btn.setDisable(false);
                btn.setText("点击我");
            }).start();
        });

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

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

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

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

最终代码

TCPServer.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);

                }
            } 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();
    }
}

TCPClient.java

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

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

    public TCPClient(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();
        }
    }
}

SimpleFx(窗口)

import javafx.application.Application;
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.layout.BorderPane;
import javafx.scene.layout.*;
import javafx.stage.Stage;

import java.io.IOException;


public class SimpleFx extends Application {

    private TCPClient tcpClient;

    private final Button btnCon = new Button("连接");
    private final Button btnExit = new Button("退出");
    private final Button btnSend = 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();


    public void start(Stage primaryStage) {
        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();
            try {
                //tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
                tcpClient = new TCPClient(ip, port);
                //成功连接服务器,接收服务器发来的第一条欢迎信息
                String firstMsg = tcpClient.receive();
                OutputArea.appendText(firstMsg + "\n");
            } catch (Exception e) {
                OutputArea.appendText("服务器连接失败!" + e.getMessage() + "\n");
            }
        });
        btnExit.setOnAction(event -> {
         if (tcpClient != null){
             //向服务器发送关闭连接的约定信息
             tcpClient.send("bye");
             tcpClient.close();
         }
            System.exit(0);
        });
        btnSend.setOnAction(event -> {
            String sendMsg = InputField.getText();
            tcpClient.send(sendMsg);//向服务器发送一串字符
            InputField.clear();
            OutputArea.appendText("客户端发送:" + sendMsg + "\n");
            String receiveMsg = tcpClient.receive();//从服务器接收一行字符
            OutputArea.appendText(receiveMsg + "\n");
        });

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

        mainVBox.getChildren().addAll(hBox, vBox, hBox2);

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

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


    }

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

}

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 程序中的发送语句临时禁用(验证完再还原),例如:
    // 向输出流中输出一行字符串,远程客户端可以读取该字符串
    // pw.println("来自服务器:" + msg);  临时禁用
    即服务器不回传信息。
  2. 启动 TCPServer.java 服务程序,再启动 TCPClientFX.java 客户端程序,发送信息,发现客户程序不能正常运行,发送按钮甚至整个程序失去响应。
  3. 强行终止 TCPClientFX,在窗口程序的发送语句处设置断点,如图3.3所示。然后在调试状态运行该程序,逐行调试(遇到自定义的方法,建议使用 step into 跟踪进入)。在执行到 receive() 方法时,使用 step into 跟踪进方法会发现程序会阻塞在 msg = br.readLine(); 处(因为服务器没有返回,客户端的输入流队列中是空的,所以被阻塞)。

理解读一行功能

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

程序演示

  1. TCPServer.java 程序中多增加一条信息返回语句,例如:
    pw.println("来自服务器:" + msg);
    // 下面多增加一条信息返回语句
    pw.println("来自服务器,重复发送: " + msg);
    然后启动服务端程序。
  2. 启动客户端 TCPClientFX 程序,发现客户显示区每次只显示一条信息,且与你发送的信息不同步。因为每一次互动,服务器返回两行信息,而客户端只是读取最前面的一行信息。

多线程技术

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

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

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

操作步骤

  1. 现在右键选择 TCPClientFX.java 重构(Refactor),重命名为 TCPClientThreadFX.java(采用如图 3.5 所示的方式)。
  2. 在合适的位置添加如下线程代码(自己思考添加在什么位置合适,更新版本讲义会有更详细的提示),用于接收服务器的信息,为了简洁,匿名内部类使用了lambda的写法:
    // 用于接收服务器信息的单独线程
    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定义为类中的成员变量,就没有这个限制,可以直接访问)。

示例代码

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 表达式的简单例子:

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 接口的一个简单示例:

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 来管理线程池,这通常是更高效和灵活的方式。
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() 的简单示例:

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

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

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

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

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(添加文本选择功能)

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();
  }
}

chapter04(网络文件传输)

BufferReader类

BufferedReader 是 Java 中用来包装一个 Reader 对象的类,它提供了一个缓冲区,可以提高读取文本数据的效率。BufferedReader 通常用于逐行读取文本文件,因为它提供了 readLine() 方法,该方法一次读取一行文本。

以下是 BufferedReader 的一些常见用法:

创建 BufferedReader

要使用 BufferedReader,你首先需要创建一个实例。这通常是通过将现有的 Reader 对象(如 FileReader)传递给 BufferedReader 的构造函数来完成的。

FileReader fileReader = new FileReader("path/to/file.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);

读取文本

使用 readLine() 方法逐行读取文本:

String line;
while ((line = bufferedReader.readLine()) != null) {
    System.out.println(line);
}

关闭 BufferedReader

读取完成后,应该关闭 BufferedReader(以及它包装的 Reader),以释放系统资源。

bufferedReader.close();

其他读取方法

除了 readLine()BufferedReader 还提供了其他方法来读取文本:

  • read():读取单个字符。
  • read(char[] cbuf):将字符读入数组。
  • read(char[] cbuf, int off, int len):从缓冲区读取字符到数组的某个部分。

示例代码

下面是一个使用 BufferedReader 读取文件内容的完整示例:

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 实例,指定你想要写入数据的文件。

import java.io.FileOutputStream;
import java.io.File;

File saveFile = new File("path/to/your/file.txt");
FileOutputStream fileOut = new FileOutputStream(saveFile);

写入数据

使用 write() 方法将字节数据写入文件。

byte[] data = ...; // 这里是你要写入文件的数据
fileOut.write(data);

关闭 FileOutputStream

完成写入操作后,应该关闭 FileOutputStream 以释放系统资源。

fileOut.close();

示例代码

下面是一个完整的示例,演示如何使用 FileOutputStream 将字节数据写入文件:

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 中。

在你的代码示例中:

new PrintWriter(
    new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);

以下是各个部分的解释:

  1. **OutputStreamWriter**:

    • 它接受一个字节输出流(在这个例子中是 socketOut,即 Socket 的输出流)。
    • 它使用指定的字符集(这里是 StandardCharsets.UTF_8)将字节转换为字符。
  2. **PrintWriter**:

    • 它接受一个 Writer 对象(这里是 OutputStreamWriter 的实例)。
    • 第二个参数 true 表示使用自动刷新模式。这意味着每当缓冲区满了或者新的行分隔符被写入时,PrintWriter 会自动刷新其内部缓冲区。这对于网络应用程序很有用,因为它可以确保数据及时发送到网络上。

示例代码

下面是一个完整的示例,演示如何使用 PrintWriter 通过 Socket 发送字符串数据:

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字节数据返回字符

private BufferedReader getReader(Socket socket) throws IOException {
    //获得输入流缓冲区的地址
    InputStream socketIn = socket.getInputStream();
    //读取字节数据返回字符串
    return new BufferedReader(
        new InputStreamReader(socketIn, StandardCharsets.UTF_8));
}

关闭通信套接字

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

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

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

    // 服务器端发送完数据后
    socket.shutdownOutput();
  • 在客户端添加超时设置
    你可以为 Socket 设置读取超时,这样如果服务器在指定时间内没有发送数据,read() 方法将会超时并返回 -1。

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

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

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

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

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

设计思路

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

// 得到网络输出字节流地址,并封装成网络输出字符流
// 设置最后一个参数为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的数据(字节),直接使用字节类型数据写入到文件流对象(或者包装一下,但都是字节数据)

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

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

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

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

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

chapter05(线程池-多用户服务器)

教学与实践目的

  • 学会服务器支持多用户并发访问的程序设计技术。

多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等。

第二讲的TCPServer是单用户版本,每次只能和一个用户对话。(请仔细阅读TCPServer.java 程序,了解其中原理,找出关键语句),只有前一个用户 退出后,后面的用户才能完成服务器连接。

允许多个实例运行

image-20241010114553558

原因:服务器的主进程一次只能处理一个客户,其它已连接的客户等候在 监听队列中。

设计思路

  • 解决思路就是用多线程

服务器可能面临很多客户的并发连接,这种情况的方案一般是:主线程只负责监听客户请求和接受连接请求,用一个线程专门负责和一个客户对话,即一个客户请求成功后,创建一个新线程来专门负责该客户。

对于这种多用户的情况,用第三讲的方式new Thread创建线程,频繁创建大量线程需要消耗大量系统资源。

对于服务器,一般是使用线程池来管理和复用线程。线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

  • ExecutorService 代表线程池,其创建方式常见的有两种:
    • ExecutorService executorService = Executors.newFixedThreadPool(n);
    • ExecutorService executorService = Executors. newCachedThreadPool( );

创建后,就可以使用executorService.execute方法来取出一个线程执行, 该方法的参数就是Runnable接口类型。我们可以将和客户对话部分的代码抽 取到一个Runnable的实现类 Handler(见附录)的run方法中,然后丢给线程 池去执行。方便起见,Handler作为主程序的内部类是个不错的选择。

线程池

Java中的线程池是一种执行器(Executor),用于在一个后台线程中执行任务。线程池的主要目的是减少在创建和销毁线程时所产生的性能开销。通过重用已经创建的线程来执行新的任务,线程池提高了程序的响应速度,并且提供了更好的系统资源管理。

Java通过java.util.concurrent包中的Executor框架提供了线程池的实现。以下是线程池的一些关键概念:

  1. 核心线程数(Core Pool Size):线程池中始终保持的线程数量,即使它们处于空闲状态。

  2. 最大线程数(Maximum Pool Size):线程池中允许的最大线程数量。

  3. 工作队列(Work Queue):用于存放待执行任务的阻塞队列。

  4. 线程工厂(Thread Factory):用于创建新线程的工厂。

  5. 拒绝策略(Rejected Execution Handler):当任务太多,无法被线程池及时处理时,采取的策略。

  6. 保持活动时间(Keep Alive Time):非核心线程空闲时在终止前等待新任务的最长时间。

  7. 时间单位(Time Unit):保持活动时间的时间单位。

Java 提供了几种预定义的线程池:

  • FixedThreadPool:拥有固定数量线程的线程池。
  • CachedThreadPool:根据需要创建新线程的线程池,对于短生命周期的异步任务非常合适。
  • SingleThreadExecutor:只有一个线程的线程池,保证所有任务按顺序执行。
  • ScheduledThreadPool:用于延迟执行或定期执行任务的线程池。

创建线程池的一般方式是使用Executors工厂类:

// 创建一个拥有固定线程数量的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);

// 创建一个可根据需要创建新线程的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 创建一个单线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 创建一个可定时执行任务的线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);

使用线程池执行任务:

// 提交一个Runnable任务
fixedThreadPool.execute(new Runnable() {
    public void run() {
        // 任务代码
    }
});

// 提交一个Callable任务,并获取Future对象
Future<String> future = fixedThreadPool.submit(new Callable<String>() {
    public String call() {
        // 任务代码
        return "result";
    }
});

关闭线程池:

// 关闭线程池,不接受新任务,但已提交的任务会继续执行
fixedThreadPool.shutdown();

// 关闭线程池,不接受新任务,并且会尝试停止所有正在执行的任务
fixedThreadPool.shutdownNow();

线程池是Java并发编程中非常重要的一部分,合理使用线程池可以显著提高程序性能和资源利用率。

实现代码

TCPClientThreadFX.java

import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TCPClientThreadFX extends Application {

    private final Button btnCon = new Button("连接");
    private final Button btnExit = new Button("退出");
    private final Button btnSend = 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 TCPClient tcpClient;
    private Thread receiveThread;

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

    public void start(Stage primaryStage) {
        btnSend.setDisable(true);

        BorderPane mainPane = new BorderPane();
        VBox mainVBox = new VBox();

        HBox hBox = new HBox();
        hBox.setSpacing(10);
        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.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 = new TCPClient(ip, port);
                receiveThread = new Thread(() -> {
                    String msg;
                    while ((msg = tcpClient.receive()) != null) {
                        String msgTemp = msg;
                        Platform.runLater(() -> {
                            OutputArea.appendText(msgTemp + "\n");
                        });
                    }
                    Platform.runLater(() -> {
                        OutputArea.appendText("对话已关闭!\n");
                    });
                });
                receiveThread.start();
                btnSend.setDisable(false);
            } catch (Exception e) {
                OutputArea.appendText("服务器连接失败!" + e.getMessage() + "\n");
            }
        });

        btnExit.setOnAction(event -> {
            if (tcpClient != null) {
                tcpClient.send("bye");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tcpClient.close();
                btnSend.setDisable(true);
            }
            System.exit(0);
        });

        btnSend.setOnAction(event -> {
            String sendMsg = InputField.getText();
            tcpClient.send(sendMsg);
            InputField.clear();
            OutputArea.appendText("客户端发送:" + sendMsg + "\n");
        });

        hBox2.setAlignment(Pos.CENTER_RIGHT);
        hBox2.getChildren().addAll(btnSend, 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("8080");

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

TCPClient.java

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

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

    public TCPClient(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(主要修改线程池部分)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

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

    // 创建线程池
    private final ExecutorService executorService = Executors.newCachedThreadPool();

    public TCPServer() throws IOException {
        port = 8080; // 例如,使用8080作为默认端口
        serverSocket = new ServerSocket(port);
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

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

    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));
    }

    class ThreadHandler implements Runnable {
        private final Socket socket;

        public ThreadHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            //本地服务器控制台显示客户端连接的用户信息
            System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
            try {
                BufferedReader br = getReader(socket);//定义字符串输入流
                PrintWriter pw = getWriter(socket);//定义字符串输出流
                //客户端正常连接成功,则发送服务器的欢迎信息,然后等待客户发送信息
                pw.println("From 服务器:欢迎使用本服务!");
                String msg = null;
                //此处程序阻塞,每次从输入流中读入一行字符串
                while ((msg = br.readLine()) != null) {
                    //如果客户发送的消息为"bye",就结束通信
                    if (msg.equalsIgnoreCase("bye")) {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From服务器:服务器断开连接,结束服务!");
                        System.out.println("客户端离开");
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        break; //结束循环
                    }
                    pw.println("From服务器:" + msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (socket != null)
                        socket.close(); //关闭socket连接及相关的输入输出流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //单客户版本,即每一次只能与一个客户建立通信连接
    public void Service() {
        while (true) {
            Socket socket = null;
            try {
                //此处程序阻塞等待,监听并等待客户发起连接,有连接请求就生成一个套接字。
                socket = serverSocket.accept();
                executorService.execute(new ThreadHandler(socket));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

群组聊天功能

数据结构之Set集合

在 Java 中,Set 是一种集合类型,用于存储不重复的元素。Set 接口是 Java Collections Framework 的一部分,主要用于表示不允许重复元素的集合。Set 接口的主要实现类有:

  1. HashSet:最常用的 Set 实现,基于哈希表实现,具有较快的查找速度。它不保证元素的迭代顺序,可能会随时间而变化。

    Set<String> hashSet = new HashSet<>();
    hashSet.add("Apple");
    hashSet.add("Banana");
    hashSet.add("Orange");
  2. LinkedHashSet:继承自 HashSet,维护插入元素的顺序。这意味着在迭代元素时,会按照插入的顺序返回。

    Set<String> linkedHashSet = new LinkedHashSet<>();
    linkedHashSet.add("Apple");
    linkedHashSet.add("Banana");
    linkedHashSet.add("Orange");
  3. TreeSet:基于红黑树实现的 Set,它按自然顺序或通过构造函数提供的比较器进行排序。插入和删除操作的时间复杂度为 O(log n)。

    Set<String> treeSet = new TreeSet<>();
    treeSet.add("Apple");
    treeSet.add("Banana");
    treeSet.add("Orange");
特点
  • 集合中的元素是唯一的,不允许重复。
  • Set 不提供按索引访问元素的功能。
  • 可以使用迭代器遍历集合中的元素。
使用场景
  • 当需要存储不重复的元素时,如用户ID、唯一的商品代码等。
  • 常用于需要检索某个元素是否存在的情况。

在 Java 中,Set 接口提供了多种方法来操作集合。以下是一些常用的 Set 方法,以及它们的具体说明和示例:

常用方法
  1. add(E e): 将指定元素添加到集合中,如果集合中已存在该元素,则不做任何操作。

    Set<String> set = new HashSet<>();
    set.add("Apple");
    set.add("Banana");
    set.add("Apple"); // 不会重复添加
  2. remove(Object o): 从集合中移除指定的元素,如果成功移除则返回 true。

    set.remove("Banana"); // 移除 "Banana"
  3. contains(Object o): 检查集合是否包含指定的元素,如果包含返回 true。

    boolean hasApple = set.contains("Apple"); // 返回 true
  4. size(): 返回集合中的元素个数。

    int size = set.size(); // 返回 2,因为 "Banana" 已被移除
  5. isEmpty(): 检查集合是否为空,如果集合没有元素则返回 true。

    boolean isEmpty = set.isEmpty(); // 返回 false
  6. clear(): 移除集合中的所有元素。

    set.clear(); // 清空集合
  7. iterator(): 返回集合的迭代器,可以用于遍历集合中的元素。

    Iterator<String> iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
  8. addAll(Collection<? extends E> c): 将指定集合中的所有元素添加到当前集合中。

    Set<String> anotherSet = new HashSet<>();
    anotherSet.add("Cherry");
    anotherSet.add("Date");
    set.addAll(anotherSet); // 将 anotherSet 的元素添加到 set 中
  9. retainAll(Collection<?> c): 只保留当前集合中包含的指定集合中的元素,移除其他的元素。

    Set<String> keepSet = new HashSet<>();
    keepSet.add("Apple");
    set.retainAll(keepSet); // 只保留 "Apple"
  10. removeAll(Collection<?> c): 从当前集合中移除指定集合中的所有元素。

    set.removeAll(anotherSet); // 移除 anotherSet 中的所有元素
示例代码

以下是一个综合实例,展示如何使用 Java 中的 Set 和它的方法:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class SetExample {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();

        // 添加元素
        set.add("Apple");
        set.add("Banana");
        set.add("Cherry");

        // 检查是否包含
        System.out.println("Contains Apple: " + set.contains("Apple"));

        // 输出集合大小
        System.out.println("Size: " + set.size());

        // 遍历集合
        Iterator<String> iterator = set.iterator();
        System.out.println("Elements in the set:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 移除元素
        set.remove("Banana");
        System.out.println("After removing Banana, size: " + set.size());

        // 清空集合
        set.clear();
        System.out.println("After clearing, is empty: " + set.isEmpty());
    }
}

线程安全的集合CopyOnWriteArraySet

在Java中,CopyOnWriteArraySetjava.util 包下的一个线程安全的变体,它继承自 CopyOnWriteArrayList。这个集合类适用于读多写少的场景,因为每次修改(添加、删除等)都会复制整个底层数组,这可能会导致写操作变得非常昂贵,尤其是在集合元素很多的情况下。

CopyOnWriteArraySet 维护了一个无序的元素集合,并且不允许元素重复。由于它是基于 CopyOnWriteArrayList 实现的,所以它的方法和 CopyOnWriteArrayList 相似,只是它额外确保了元素的唯一性。

下面是一些使用 CopyOnWriteArraySet 的基本示例:

import java.util.concurrent.CopyOnWriteArraySet;

public class Example {
    public static void main(String[] args) {
        // 创建一个CopyOnWriteArraySet集合
        CopyOnWriteArraySet<Socket> sockets = new CopyOnWriteArraySet<>();

        // 添加元素
        sockets.add(new Socket(/* 参数 */));
        sockets.add(new Socket(/* 参数 */));

        // 迭代集合
        for (Socket socket : sockets) {
            // 做一些操作
        }

        // 删除元素
        sockets.remove(new Socket(/* 参数 */));

        // 检查集合是否包含某个元素
        boolean contains = sockets.contains(new Socket(/* 参数 */));

        // 获取集合的大小
        int size = sockets.size();
    }
}

请注意,由于 CopyOnWriteArraySet 的写操作性能开销较大,所以它通常适用于以下情况:

  1. 写操作非常少,而读操作非常多。
  2. 存储的数据量不大。
  3. 数据的实时性要求不高,因为读操作可能读取到旧的数据。

如果你的应用场景不满足上述条件,可能需要考虑使用其他的并发集合类,比如 ConcurrentHashMap 键集合或 Collections.synchronizedSet 包装的 HashSet

GroupServer.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

    // 创建线程池
    private final ExecutorService executorService = Executors.newCachedThreadPool();

    // 线程安全的set集合
    public static CopyOnWriteArraySet<Socket> socketset = new CopyOnWriteArraySet<>();

    public GroupServer() throws IOException {
        port = 8080; // 例如,使用8080作为默认端口
        serverSocket = new ServerSocket(port);
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

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

    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));
    }

    private void sendToAllMembers(String msg, String hostAddress) throws IOException {
        PrintWriter pw;
        OutputStream out;
        for (Socket tempSocket : socketset) {
            out = tempSocket.getOutputStream();
            pw = new PrintWriter(
                new OutputStreamWriter(out, "utf-8"), true);
            pw.println(hostAddress + " 发言:" + msg);
        }
    }


    class ThreadHandler implements Runnable {
        private final Socket socket;

        public ThreadHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            //本地服务器控制台显示客户端连接的用户信息
            System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
            try {
                BufferedReader br = getReader(socket);//定义字符串输入流
                PrintWriter pw = getWriter(socket);//定义字符串输出流
                //客户端正常连接成功,则发送服务器的欢迎信息,然后等待客户发送信息
                pw.println("From 服务器:欢迎使用本服务!");
                String msg;
                //此处程序阻塞,每次从输入流中读入一行字符串
                while ((msg = br.readLine()) != null) {
                    //如果客户发送的消息为"bye",就结束通信
                    if (msg.equalsIgnoreCase("bye")) {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From服务器:服务器断开连接,结束服务!");
                        System.out.println("客户端离开");
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        break; //结束循环
                    }
                    sendToAllMembers(msg, socket.getInetAddress().getHostAddress());
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close(); //关闭socket连接及相关的输入输出流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    //单客户版本,即每一次只能与一个客户建立通信连接
    public void Service() {
        while (true) {
            Socket socket = null;
            try {
                //此处程序阻塞等待,监听并等待客户发起连接,有连接请求就生成一个套接字。
                socket = serverSocket.accept(); // 从请求队列取一个socket请求
                socketset.add(socket);
                System.out.println("添加socket" + socket);
                Thread t = new Thread(new ThreadHandler(socket));
                executorService.execute(t);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

扩展练习一:自定义线程池

  • 前面提到OOM的问题,如果能提供自行确定最小值和最大值的动态调整的线程池会更满足要求,大家跟踪Executors. newCachedThreadPool()方法,观察其源代码,会发现非常简单,而且也会明白为什么会出现OOM错误(Out of Memory内存溢出)

  • 大家可以尝试将其实现代码拷贝出来稍作修改,封装一个自己版本的 myCachedThreadPool 方法来使用。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, 1000,60L, TimeUnit.SECONDS,
       new SynchronousQueue<Runnable>());
}

扩展练习二:简易聊天室设计

  • 设计聊天服务器ChatServer.java,客户端用学号-姓名的方式登录服务器,实现一对一、一对多私聊及群组广播聊天的功能;
  • 用户登录时,需要将用户上线的信息广播给所有在线用户;客户端发送特定命令,服务器要能够返回在线用户列表信息;
  • 程序设计中,要能显示发言者的学号姓名(例如20181111111-程旭),这种情况可以考虑使用线程安全的HashMap类型(自行搜索应该使用哪个类,这些线程安全的集合类型和普通的集合类型使用方式如出一辙);
  • 自行考虑如何设计服务端和客户端之间的交互约定(协议),可以在用户连上服务器时,即要求用户发送学号和姓名信息,并给用户发送相关的使用指南,约定发送指令的作用。
  • 编程中要小心处理好各种逻辑关系。

数据结构之HashMap

HashMap 是一种数据结构,属于哈希表的一种实现。它能够以键值对的形式存储数据,具有快速的查找、插入和删除操作。以下是一些关于 HashMap 的基本概念和特性:

  1. 键值对存储:HashMap 将数据以键值对的方式存储,每个键(Key)唯一对应一个值(Value)。
  2. 快速访问:通过哈希函数,HashMap 可以快速定位到存储在内部数组中的数据,具有平均 O(1) 的时间复杂度进行查找、插入和删除。
  3. 允许空值:HashMap 允许一个键为 null,且可以有多个值为 null。
  4. 不保证顺序:HashMap 并不保证键值对的顺序,因此在迭代时可能不按插入顺序返回元素。
  5. 线程不安全:HashMap 不是线程安全的,若在多线程环境下使用,可能需要使用 Collections.synchronizedMap 或使用 ConcurrentHashMap

在 Java 中,HashMap 的基本用法示例如下:

import java.util.HashMap;

public class Example {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();

        // 插入数据
        map.put("苹果", 1);
        map.put("香蕉", 2);
        map.put("橙子", 3);

        // 访问数据
        System.out.println("香蕉的数量: " + map.get("香蕉"));

        // 删除数据
        map.remove("橙子");

        // 遍历 HashMap
        for (String key : map.keySet()) {
            System.out.println(key + ": " + map.get(key));
        }
    }
}

线程安全的ConcurrentHashMap

ConcurrentHashMap 是 Java 中提供的一个线程安全的 HashMap 实现,它允许多个线程同时访问和修改,而不需要额外的同步控制。以下是一些基本的使用方法:

导入类

首先,你需要导入 ConcurrentHashMap 类:

import java.util.concurrent.ConcurrentHashMap;
创建 ConcurrentHashMap

创建一个 ConcurrentHashMap 实例非常简单,你可以像创建普通的 HashMap 一样创建它:

ConcurrentHashMap<KeyType, ValueType> map = new ConcurrentHashMap<>();
插入元素

ConcurrentHashMap 中插入元素,可以使用 putIfAbsent 方法,该方法只有在键不存在时才会插入元素,这有助于避免多线程环境下的冲突:

map.putIfAbsent(key, value);
获取元素

获取元素可以直接通过 get 方法:

ValueType value = map.get(key);
删除元素

删除元素可以使用 remove 方法:

map.remove(key);
遍历

遍历 ConcurrentHashMap 和普通的 HashMap 一样,但是要注意,遍历时对 ConcurrentHashMap 的修改操作可能会影响迭代器的行为:

for (Map.Entry<KeyType, ValueType> entry : map.entrySet()) {
    KeyType key = entry.getKey();
    ValueType value = entry.getValue();
    // 处理键值对
}
原子操作

ConcurrentHashMap 提供了一些原子操作,例如 putIfAbsentreplacecompute 等:

// 如果键存在,则替换旧值,否则插入新值
map.replace(key, oldValue, newValue);

// 如果键存在,则根据提供的函数计算新值
map.compute(key, (k, v) -> {
    // 计算新值的逻辑
    return newValue;
});
线程安全

ConcurrentHashMap 在多线程环境下不需要额外的同步措施,因为它内部已经处理了线程安全的问题。但是,当涉及到复合操作时(比如先检查某个键是否存在,然后基于这个检查结果执行一些操作),你可能需要使用 computeIfAbsentcomputeIfPresent 等原子方法来保证操作的原子性。

使用 ConcurrentHashMap 时,你不需要担心线程安全问题,但是要确保你的操作是线程安全的,比如不要在迭代过程中修改 ConcurrentHashMap,或者在迭代过程中对元素进行修改时,要确保这些修改是线程安全的。

TextArea添加滚轮修改字体大小

// 给文本区添加滚轮事件并且要按住Ctrl键增加字号
OutputArea.setOnScroll(event -> {
    if (event.isControlDown()) {
        if (event.getDeltaY() > 0) {
            OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() + 1) + "px;");
        } else {
            OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() - 1) + "px;");
        }
    }
});

实现代码

ChatServer
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ChatServer {
    private final int port;
    private final ServerSocket serverSocket;
    private final ExecutorService executorService = Executors.newCachedThreadPool();
    // public static ConcurrentHashMap<HashMap<String, String>, Socket> hashmap = new ConcurrentHashMap<>();
    public static ConcurrentHashMap<String, Socket> hashmap = new ConcurrentHashMap<>();

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

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

    private PrintWriter getWriter(Socket socket) throws IOException {
        OutputStream socketOut = socket.getOutputStream();
        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));
    }

    private void sendToAllMembers(String no_name, String msg, int message_type) throws IOException {
        PrintWriter pw;
        OutputStream out;
        for (Map.Entry<String, Socket> entry : hashmap.entrySet()) {
            if (entry.getKey().equals(no_name)) {
                continue;
            }
            Socket tempSocket = entry.getValue();
            out = tempSocket.getOutputStream();
            pw = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8), true);
            if (message_type == 0) { // 聊天消息
                pw.println(no_name + " 发言:" + msg);
            } else if (message_type == 1) { // 系统消息
                pw.println(msg);
            }

        }
    }

    private void listAllMembers(Socket socket) throws IOException {
        PrintWriter pr = getWriter(socket);
        for (Map.Entry<String, Socket> entry : hashmap.entrySet()) {
            pr.println(entry.getKey());
        }
    }

    class ThreadHandler implements Runnable {
        private final Socket socket;

        public ThreadHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
            try {
                PrintWriter pw = getWriter(socket);
                BufferedReader br = getReader(socket);
                pw.println("请输入用户名和学号,中间使用-分割");
                String no_name = br.readLine();
                // 正则的分割符是\|,所以这里要用\\|
                while (no_name.split("-").length < 2) {
                    pw.println("请输入正确的用户名和学号,中间使用-分割");
                    no_name = br.readLine();
                }
                // 为什么要加\\?因为|在正则表达式中有特殊含义,需要转义
                String name = no_name.split("-")[0];
                String no = no_name.split("-")[1];
                hashmap.put(no_name, socket);
                pw.println("clearScreen");
                pw.println("no_name:" + no_name);
                pw.println("From 服务器:已成功登录!");
                pw.println("From 服务器:默认是发送给全体用户的广播信息");
                pw.println("From 服务器:如果要发送私聊信息, 使用【学号1|学号2&私聊信息】方式给指定用户发送,例如发送【20181111111|20182222222&这是我发给你们的私聊信息】");
                pw.println("From 服务器:发送 #在线用户# 能获得所有在线用户的列表信息");

                // 处理消息
                String msg;
                while ((msg = br.readLine()) != null) {
                    if (msg.equalsIgnoreCase("bye")) {
                        pw.println("From服务器:服务器断开连接,结束服务!");
                        hashmap.remove(no_name);
                        System.out.println("客户端" + no_name + "离开");
                        sendToAllMembers(no_name, "系统消息:-------" + no_name + "离开-------", 1);
                        break;
                    } else if (msg.equals("#在线用户#")) {
                        listAllMembers(socket);
                        continue;
                    }
                    if (msg.matches("^【[0-9]*\\|[0-9]*&.*】$")) {
                        // 私聊消息
                        System.out.println("私聊消息:" + msg);
                        String[] split = msg.split("\\|");
                        String from_no = split[0].substring(1);
                        System.out.println("from_no:" + from_no);
                        if (!from_no.equals(no)) {
                            System.out.println(no);
                            pw.println("From服务器:学号1必须是自己的学号!");
                            continue;
                        }
                        String to_no = split[1].split("&")[0];
                        System.out.println("to_no:" + to_no);
                        String content = split[1].split("&")[1].substring(1, split[1].length() - 1);
                        System.out.println("content:" + content);
                        for (Map.Entry<String, Socket> entry : hashmap.entrySet()) {
                            if (entry.getKey().endsWith(to_no)) {
                                Socket tempSocket = entry.getValue();
                                System.out.println("Socket" + tempSocket);
                                PrintWriter tempPw = getWriter(tempSocket);
                                tempPw.println("From " + no_name + ":" + content);
                            }
                        }
                        continue;
                    }
                    sendToAllMembers(no_name, msg, 0);
                }

            } catch (
                IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    public void Service() {
        while (true) {
            Socket socket;
            try {
                socket = serverSocket.accept();
                System.out.println("添加socket" + socket);
                Thread t = new Thread(new ThreadHandler(socket));
                executorService.execute(t);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
TCPClient
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

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

    public TCPClient(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();
        }
    }
}
TCPClientThreadFx
import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class TCPClientThreadFX extends Application {

    private final Button btnCon = new Button("连接");
    private final Button btnExit = new Button("退出");
    private final Button btnSend = 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 TCPClient tcpClient;
    private Thread receiveThread;

    private String no_name;

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

    public void start(Stage primaryStage) {
        btnSend.setDisable(true);

        BorderPane mainPane = new BorderPane();
        VBox mainVBox = new VBox();

        HBox hBox = new HBox();
        hBox.setSpacing(10);
        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.setPadding(new Insets(10, 20, 10, 20));
        vBox.getChildren().addAll(new Label("信息显示区:"), OutputArea, new Label("信息输入区"), InputField);
        // setVgrow()方法用于设置组件的拉伸策略,在这里设置为ALWAYS,即组件将会填充整个区域
        VBox.setVgrow(OutputArea, Priority.ALWAYS);
        OutputArea.setEditable(false);
        OutputArea.setStyle("-fx-wrap-text: true; -fx-font-size: 16px;");

        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 = new TCPClient(ip, port);
                receiveThread = new Thread(() -> {
                    String msg;
                    while ((msg = tcpClient.receive()) != null) {
                        String msgTemp = msg;
                        if (msgTemp.equals("clearScreen")) {
                            OutputArea.clear();
                            continue;
                        } else if (msgTemp.startsWith("no_name:")) {
                            no_name = msgTemp.split(":")[1];
                            continue;
                        }
                        Platform.runLater(() -> {
                            OutputArea.appendText(msgTemp + "\n");
                        });
                    }
                    Platform.runLater(() -> {
                        OutputArea.appendText("对话已关闭!\n");
                    });
                });
                receiveThread.start();
                btnSend.setDisable(false);
            } catch (Exception e) {
                OutputArea.appendText("服务器连接失败!" + e.getMessage() + "\n");
            }
        });

        btnExit.setOnAction(event -> {
            if (tcpClient != null) {
                tcpClient.send("bye");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tcpClient.close();
                btnSend.setDisable(true);
            }
            System.exit(0);
        });

        btnSend.setOnAction(event -> {
            String sendMsg = InputField.getText();
            if (sendMsg.trim().isEmpty()) {
                return;
            }
            tcpClient.send(sendMsg);
            InputField.clear();
            // 获取本机ip
            String ip;
            try {
                ip = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                throw new RuntimeException(e);
            }
            // 添加窗口标题
            primaryStage.setTitle(ip + " [" + no_name + "]");
            OutputArea.appendText("Me: " + sendMsg + "\n");
        });

        // 给文本区添加滚轮事件并且要按住Ctrl键增加字号
        OutputArea.setOnScroll(event -> {
            if (event.isControlDown()) {
                if (event.getDeltaY() > 0) {
                    OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() + 1) + "px;");
                } else {
                    OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() - 1) + "px;");
                }
            }
        });
        // 文本自动换行
        OutputArea.setWrapText(true);

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

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

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

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

chapter06(UDPSocket)

UPD的特点

  1. UDP有独立的套接字(IP + PORT),与TCP使用相同端口号不会冲突。
  2. UDP在使用前不需要进行连接,没有流的概念。
  3. UDP通信类似于邮件通信:不需要实时连接,只需要目的地址。
  4. UDP通信前只需知道对方的IP地址和端口号即可发送信息。
  5. 基于用户数据报文(包)进行读写。
  6. UDP通信通常用于线路质量好的环境,如局域网。如果在互联网上,通常用于对数据完整性要求不高的场合,例如语音传送等。

UDP 编程关键Java类

  • DatagramSocket
  • DatagramPacket
  • MulticastSocket

1.创建 UDPClient.java 程序

UDP 客户端的主要步骤

  1. 创建 DatagramSocket 实例

    • 可以选择对本地地址和端口号进行设置,但一般不需要指定。
    • 不指定时程序将自动选择本地地址和可用的端口。
  2. 发送和接收数据

    • 使用 DatagramSocket 类来发送和接收 DatagramPacket 类的实例进行通信。
  3. 关闭套接字

    • 通信完成后,使用 DatagramSocket 类的 close() 方法销毁该套接字。

注意事项

  • Socket 类不同,创建 DatagramSocket 实例时并不需要指定目的地址,这也是 TCP 协议和 UDP 协议的最大不同点之一。

UDP 套接字类: DatagramSocket

概述

  • UDP通信没有客户套接字 (Socket用于通信) 和服务器套接字 (ServerSocket服务器端用于接收连接请求) 之分,UDP套接字只有一种:DatagramSocket
  • UDP套接字的角色类似于邮箱,可以从不同地址接收邮件,并向不同地址发送信息。
  • UDP编程不严格区分服务端和客户端,通常将固定IP和固定端口的机器视为服务器。

创建 UDP 套接字

DatagramSocket datagramSocket = new DatagramSocket();
  • 创建时不需要指定本地的地址和端口号。

UDP 套接字的重要方法

  1. 发送网络数据

    datagramSocket.send(DatagramPacket packet);
    • 发送一个数据包到由IP和端口号指定的地址。
  2. 接收网络数据

    datagramSocket.receive(DatagramPacket packet);
    • 接收一个数据包。如果没有数据,程序会在此调用处阻塞。
  3. 指定超时

    datagramSocket.setSoTimeout(int timeout);
    • timeout 是一个整数,表示毫秒数,用于指定 receive(DatagramPacket packet) 方法的最长阻塞时间。
    • 超过此时限后,如果没有响应,将抛出 InterruptedIOException 异常。

注意事项

  • 如果客户端通过 send 发送信息并等待响应,则可以设置超时,避免程序无限等待。
  • 如果采用类似TCP的设计,开启新线程接收信息,则不应使用超时设置,以避免在等待过程中导致超时错误。

UDP 数据报文类: DatagramPacket

概述

  • TCP发送数据是基于字节流的,而UDP发送数据是基于DatagramPacket报文。
  • 网络中传递的UDP数据都封装在自包含(self-contained)的报文中。

发送数据的过程

  • 创建UDP套接字时,没有指定远程通信方的IP和端口,而send方法的参数 (DatagramPacket packet) 是关键。
  • 每个数据报文实例除了包含要传输的信息外,还附加了IP地址和端口信息,这些信息的含义取决于数据报文是被发送还是被接收。

数据报文的创建

  1. 发送信息的构造方法
       DatagramPacket(byte[] data, int length, InetAddress remoteAddr, int remotePort);
    
    - 需要明确远程地址信息,以便将报文发送到目的地址。
    
    1. 接收信息的构造方法
    
    	```java
    	DatagramPacket(byte[] data, int length);
    • 不需要指定地址信息,length 表示要读取的数据长度,data 是用于存储报文数据的字节数组缓存。

UDP 数据报文的几个重要方法

  1. 获取目标主机IP地址

    InetAddress getAddress();
    • 如果是发送的报文,返回目标主机的IP地址;如果是接收的报文,返回发送该数据报文的主机IP地址。
  2. 获取目标主机端口

    int getPort();
    • 如果是发送的报文,返回目标主机的端口;如果是接收的报文,返回发送该数据报文的主机端口。
  3. 获取与报文相关联的数据

    byte[] getData();
    • 从报文中取出数据,返回与数据报文相关联的字节数组。

注意事项

  • 上述两个方法 (getAddress()getPort()) 主要供服务端使用,服务端可以通过这些方法获知客户端的地址信息。

2.创建UDPClientFX.java客户端窗体程序

创建 UDPServer.java 程序

概述

  • 类似TCP服务器,UDP服务器的工作是建立一个通信终端,并被动等待客户端发起连接。
  • 由于UDP是无连接的,因此没有TCP中建立连接的步骤。
  • UDP通信通过客户端的数据报文进行初始化。

典型的UDP服务器步骤

  1. 创建UDP套接字

    • 创建一个DatagramSocket实例,并指定一个本地端口(端口号范围在1024-65535之间选择)。
    DatagramSocket datagramSocket = new DatagramSocket(port);
  • 服务器准备好从任何客户端接收数据报文。
  • UDP服务器为所有客户端使用同一个套接字(与TCP不同,TCP服务器为每个成功的accept方法调用创建新的套接字)。
  1. 接收UDP报文

    • 使用DatagramSocket实例的receive方法接收一个DatagramPacket实例。
    datagramSocket.receive(datagramPacket);
    • receive方法返回时,数据报文将包含客户端的地址信息,从而使服务器知道该消息的来源,以便进行回复。
  2. 通信过程

    • 使用套接字的sendreceive方法来发送和接收DatagramPacket的实例进行通信。

注意事项

  • 服务端需要循环调用receive方法接收消息。

  • 如果使用同一个报文实例来接收消息,在下一个receive方法调用之前,需要调用报文实例的setLength(缓存数组.length)方法,以确保兼容性,避免数据丢失的BUG。

    datagramPacket.setLength(缓存数组.length);
  • 每次receive接收到的报文会修改内部消息的长度值。如果接收到的消息是10字节,下一次receive接收超出10字节的内容将会被丢弃。因此,务必重置长度值以防数据丢失。

UDP 服务器处理方法

注意事项

  • 与TCP不同,小负荷的UDP服务器通常不采用多线程方式
  • 由于UDP使用同一个套接字对应多个客户端,UDP服务器可以简单地使用顺序迭代的方式处理请求,而无需创建多个线程。

处理模式

  • UDP服务器的工作模式可以直接按照以下步骤进行:
// 省略...... 
byte[] buffer = new byte[MAX_PACKET_SIZE]; // 创建数据缓存区
DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length); // 创建接收数据报文
// 省略..... 

while (true) { 
    // 等待客户端请求
    serverSocket.receive(inPacket); // 阻塞等待,来了哪个客户端就服务哪个客户端 

    // 处理请求
    String receivedData = new String(inPacket.getData(), 0, inPacket.getLength()); // 读取客户端发送的数据
    System.out.println("收到来自客户端的消息: " + receivedData);

    // 发送响应数据
    String response = "服务器已收到: " + receivedData;
    byte[] responseData = response.getBytes();
    DatagramPacket outPacket = new DatagramPacket(responseData, responseData.length, inPacket.getAddress(), inPacket.getPort());
    serverSocket.send(outPacket); // 发送响应给客户端

    // 每次调用前,重置报文内部消息长度为缓冲区的实际长度
    inPacket.setLength(buffer.length); 
}

工作流程

  1. 创建缓冲区:在服务器启动时,创建一个字节数组作为数据缓存区,以存放接收到的UDP数据报文。
  2. 进入处理循环:服务器进入无限循环,等待客户端的请求。
  3. 接收数据:当客户端请求到达时,通过serverSocket.receive(inPacket)方法阻塞等待,直到有数据到达。
  4. 处理请求:从inPacket中读取客户端发送的数据,处理相应的业务逻辑。
  5. 发送响应:
    • 根据处理结果创建响应数据,并将其封装到新的DatagramPacket中。
    • 使用serverSocket.send(outPacket)将响应发送回客户端。
  6. 重置长度:在每次接收数据之前,调用inPacket.setLength(buffer.length),以确保能够正确接收下一次数据,避免出现数据丢失。

优势

  • 这种单线程顺序处理方法简单易懂,适用于负载较轻的场景,可以有效减少服务器资源的占用。
  • 与多线程相比,能避免上下文切换和线程管理带来的额外开销。

预习版本代码

UDPServer

package server;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Date;

public class UDPServer {
    private final int port = 8888;
    private DatagramSocket socket;

    public UDPServer() {
        try {
            socket = new DatagramSocket(port);
            System.out.println("Server started on port " + port);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void Service(){
        while (true) {
            byte[] buffer = new byte[1024];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            try {
                socket.receive(packet);
                String message = new String(packet.getData(), 0, packet.getLength());
                System.out.println("Received message: " + message);
                String response = "20221003174&徐彬&"+ new Date() + "&" + message;
                byte[] responseBytes = response.getBytes();
                // 返回响应
                DatagramPacket responsePacket = new DatagramPacket(responseBytes, responseBytes.length, packet.getAddress(), packet.getPort());
                socket.send(responsePacket);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new UDPServer().Service();
    }
}

UDPClient

package client;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

public class UDPClient {
    private final int remotePort;
    private final InetAddress remoteIP;
    private final DatagramSocket socket; // UDP套接字

    //用于接收数据的报文字节数组缓存最大容量,字节为单位
    private static final int MAX_PACKET_SIZE = 512;
    // private static final int MAX_PACKET_SIZE = 65507;

    public UDPClient(String remoteIP, String remotePort) throws IOException {
        this.remoteIP = InetAddress.getByName(remoteIP);
        this.remotePort = Integer.parseInt(remotePort);
        // 创建UDP套接字,系统随机选定一个未使用的UDP端口绑定
        socket = new DatagramSocket(); // 其实就是创建了一个发送datagram包的socket
        //设置接收数据超时
        // socket.setSoTimeout(30000);
    }

    public void send(String msg) {
        try {
            //将待发送的字符串转为字节数组
            byte[] outData = msg.getBytes(StandardCharsets.UTF_8);
            //构建用于发送的数据报文,构造方法中传入远程通信方(服务器)的ip地址和端口
            DatagramPacket outPacket = new DatagramPacket(outData, outData.length, remoteIP, remotePort);
            // 给UDPServer发送数据报文
            socket.send(outPacket);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 定义数据接收方法
    public String receive() {
        String msg = null;
        // 先准备一个空数据报文
        DatagramPacket inPacket = new DatagramPacket(new byte[MAX_PACKET_SIZE], MAX_PACKET_SIZE);
        try {
            //读取报文,阻塞语句,有数据就装包在inPacket报文中,装完或装满为止。
            socket.receive(inPacket);
            //将接收到的字节数组转为字符串
            msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return msg;
    }
}

UDPClientFx

package client;

import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;

public class UDPClientFx extends Application {

    private UDPClient client;
    private final Button btnInit = new Button("初始");
    private final Button btnExit = new Button("退出");
    private final Button btnSend = 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();


    public void start(Stage primaryStage) {
        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, btnInit);

        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: 18px;");


        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));

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

        mainVBox.getChildren().addAll(hBox, vBox, hBox2);

        mainPane.setCenter(mainVBox);
        VBox.setVgrow(vBox, Priority.ALWAYS);
        Scene scene = new Scene(mainPane, 800, 600);

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

        btnInit.setOnAction(event -> {
            try {
                String ip = IpAdd_input.getText().trim();
                String port = Port_input.getText().trim();
                client = new UDPClient(ip, port);
                client.send("Hello, Server!");
                new Thread(() -> {
                    while (true) {
                        String message = client.receive();
                        if (message != null && !message.isEmpty()) {
                            Platform.runLater(() -> OutputArea.appendText(message + "\n"));
                        }
                    }
                }).start(); // 启动接收线程
            } catch (IOException e) {
                e.printStackTrace();
                Platform.runLater(() -> OutputArea.appendText("连接服务器失败: " + e.getMessage() + "\n"));
            }
        });

        btnExit.setOnAction(event -> {
            //TODO 退出程序
            System.exit(0);
        });

        btnSend.setOnAction(event -> {
            //TODO 发送消息
            String message = InputField.getText().trim();
            if (message.isEmpty()) {
                return;
            }
            client.send(message);
            InputField.clear();
        });
        // 添加滚轮事件
        OutputArea.setOnScroll(event -> { // event滚轮事件,从底层的gestureEvent中继承,里面定义了controlDown变量,表示是否按下了ctrl键
            if (event.isControlDown()) {
                if (event.getDeltaY() > 0) {
                    OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() + 1) + "px;");
                } else {
                    OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() - 1) + "px;");
                }
            }
        });
        OutputArea.setWrapText(true);

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

    }

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

拓展练习一: 组播程序设计

  • 组播是指在一群用户范围内发送和接收信息,该信息具有共享性。UDP具有组播功能,而TCP不具有。

  • 组播地址范围为224.0.0.0 --- 239.255.255.255。组播地址号唯一标示一群用户(一定网络范围内,仅限于局域网络内或有些自治系统内支持组播)。但有很多组播地址默认已经被占用,建议在225.0.0.0到238.255.255.255之间随机选择一个组播地址使用。(默认组播只能同一网段,不能跨子网,除非设置了TTL值,且有配置组播路由器。另外同一个子网内如果也出现部分主机组播无效,可能是vmware的虚拟网卡影响,可先临时禁用这些命名为Vmnet*的虚拟网卡,并关闭防火墙)

  • 只要大家加入同一个组播地址,就能全体收取信息。在Java中,使用组播套接字MulticastSocket来组播数据,其是DatagramSocket 的一个子类,使用方式也与DatagramSocket 十分相似:将数据放在DatagramPacket对象中,然后通过MulticastSocket收发DatagramPacket对象。

组播套接字类MulticastSocket及其几个重要的方法:

路由器、交换机一般只转发和终端机一致IP地址和广播地址数据,终端机如何知道要接收组内信息?

  • 要先声明加入或退出某一组播组,其方法是:

    MulticastSocket ms = new MulticastSocket(8900);  
    ms.joinGroup(groupIP);

    该方法表示加入groupIP 组,groupIP 是 InetAddress 类型的组播地址。

    其作用是:告知自己的网络层该IP地址的包要收;转告上联的路由器这样的IP地址包要转发。

    ms.leaveGroup(groupIP);

    该方法表示退出 groupIP 组

  • 组内接收和发送信息的方法同UDP单播,也是以下两个方法:

    ms.send(DatagramPacket packet);
    ms.receive(DatagramPacket packet); 
  • 独立完成组播程序Multicast.java(供参考的源代码见附录)和窗体界面MulticastFX.java,组播套接字为225.0.0.1:8900,在组内发言要求以 “From IP 地址 学号 姓名:”为信息头。

  • 其效果如图6.3所示,要求每位同学都能看到组内其他同学的留言。

image-20241024200014700

Multicast.java

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.nio.charset.StandardCharsets;
public class Multicast {
    InetAddress groupIP;
    int port = 8900;
    MulticastSocket ms = null;
    byte[] inBuff = new byte[1024]; // 1MB数据
    byte[] outBuff = new byte[1024];

    public Multicast() throws IOException {
        groupIP = InetAddress.getByName("225.0.0.1");
        // 开启一个组播端口
        ms = new MulticastSocket(port);
        // 告诉网卡这样的 IP 地址数据包要接收
        ms.joinGroup(groupIP);
    }

    public void send(String msg) {
        try {
            outBuff = ("From/" + InetAddress.getLocalHost().toString() + " " + "20221003xxx xx" + msg).getBytes(StandardCharsets.UTF_8);
            DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, groupIP, port);
            ms.send(outPacket);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String receive() {
        try {
            DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
            ms.receive(inPacket);
            String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);
            return "From " + inPacket.getAddress().getHostAddress() + " " + msg + "\n";
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    public void close() {
        try {
            ms.leaveGroup(groupIP);
            ms.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

MulticastFx.java

import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class MulticastFx extends Application {

    private Multicast multicast;
    private final Button btnExit = new Button("退出");
    private final Button btnSend = new Button("发送");

    private final TextArea OutputArea = new TextArea();

    private final TextField InputField = new TextField();


    public void start(Stage primaryStage) throws IOException {

        BorderPane mainPane = new BorderPane();
        VBox mainVBox = new VBox();

        //内容显示区域
        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: 18px;");

        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));

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

        mainVBox.getChildren().addAll(vBox, hBox2);

        mainPane.setCenter(mainVBox);
        VBox.setVgrow(vBox, Priority.ALWAYS);
        Scene scene = new Scene(mainPane, 800, 600);

        multicast = new Multicast();

        Thread receiveThread = new Thread(() -> {
            while (true) {
                try {
                    String msg = multicast.receive();
                    Platform.runLater(() -> {
                        OutputArea.appendText(msg + "\n");
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "receiveThread");

        receiveThread.start();

        btnExit.setOnAction(event -> {
            //TODO 退出程序
            System.exit(0);
        });

        btnSend.setOnAction(event -> {
            //TODO 发送消息
            String message = InputField.getText().trim();
            if (message.isEmpty()) {
                return;
            }
            multicast.send(message);
            try {
                OutputArea.appendText("From/" + InetAddress.getLocalHost().toString() + " " + "20221003xxx xx" + message + "\n");
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
            InputField.clear();
        });
        // 添加滚轮事件
        OutputArea.setOnScroll(event -> { // event滚轮事件,从底层的gestureEvent中继承,里面定义了controlDown变量,表示是否按下了ctrl键
            if (event.isControlDown()) {
                if (event.getDeltaY() > 0) {
                    OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() + 1) + "px;");
                } else {
                    OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() - 1) + "px;");
                }
            }
        });
        OutputArea.setWrapText(true);

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

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

扩展练习二:UDP局域网聊天程序

  • 与TCP不同,UDP其实不真正区分服务端和客户端,一个程序其实可以身兼二职,尝试写一个不区分服务端和客户端UDP局域网聊天程序UDPChatFX.java

  • 为了能够彼此通信,使用一个约定的固定端口号,例如9527,界面可参考图 6.4。在同一局域网网段的机器运行该程序,可以互相

    发消息及发送广播消息。 程序应该提供一个下拉组合框来显示在线的用户IP地址,选中地址即可以给该用户发送消息;如果下拉组

    合框的内容为空,则给所有用户发送广播消息。发送广播消息可以简单的给广播地址”255.255.255.255”发送报文来实现。

image-20241024200608062

  • 下拉组合框可以使用泛型方式的private ComboBox ipComboBox = new ComboBox<>()ipComboBox.setEditable(true)将组合框设置成可编辑,ipComboBox.getValue()可获取组合框中选定的内容, ipComboBox.getItems().add(ipString)可以添加 IP 地址到组合框,ipComboBox.getItems().clear()可以清空组合框,具体其他用法可以自行搜索查询。
  • 关于在线IP地址列表的获得方法,可以给广播地址发送一个约定的探测信息,收到该特定探测信息的用户就回发一个约定的信息报文,这样就可以从该报文中取出IP地址,加入到下拉组合框中。例如可以约定:点击”刷新在线用户”按钮时,向广播地址”255.255.255.255”发送特定的字符串”detect”,而收到”detect”信息时,回发”echo”。通过这种统一的约定就可以找到在线用户。

UDPChat.java

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class UDPChat {
    private final int port = 8118;
    private DatagramSocket socket;
    public InetAddress broadcastAddress; // 广播地址

    private Thread refreshThread; // 接收线程
    byte[] inBuff = new byte[512]; // 512字节 = 512B
    byte[] outBuff = new byte[512];

    // 创建一个数组
    private final HashSet<String> onlineUsers = new HashSet<>();

    public UDPChat() {
        try {
            socket = new DatagramSocket(port);
            socket.setBroadcast(true);
            broadcastAddress = InetAddress.getByName("255.255.255.255");
            //            startRefreshThread(onlineUsers);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void send(String msg, int type, InetAddress address) {
        try {
            if (type == 1) { // 群播
                outBuff = ("From/" + InetAddress.getLocalHost().toString() + " " + "20221003xxx xx " + msg).getBytes(StandardCharsets.UTF_8);
                DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, broadcastAddress, port);
                socket.send(outPacket);
            } else if (type == 2) { // 单播
                System.out.println("单播消息:" + msg);
                outBuff = ("单播消息:" + msg).getBytes(StandardCharsets.UTF_8);
                DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, address, port);
                socket.send(outPacket);
            } else if (type == 3) { // 刷新`在线用户`
                outBuff = (msg).getBytes(StandardCharsets.UTF_8);
                DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, InetAddress.getByName("255.255.255.255"), port);
                socket.send(outPacket);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String receive() {
        try {
            DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
            socket.receive(inPacket);
            String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);
            if (msg.equals("detect")) {
                // 发送检测请求
                send("echo", 3, null);
                return "detect";
            }
            return "receive: " + msg;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public boolean isClosed() {
        return socket.isClosed();
    }

    public void close() {
        // 关闭套接字
        socket.close();
        System.out.println("Socket closed.");
    }

    //    public void startRefreshThread(HashSet<String> onlineUsers) {
    //        // 接收刷新在线用户响应
    //       this.refreshThread = new Thread(() -> {
    //            while (true) {
    //                try {
    //                    DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
    //                    socket.receive(inPacket);
    //                    String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);
    //                    if (msg.equals("echo")) {
    //                        String usrAddr = inPacket.getAddress().toString().substring(1);
    //                        onlineUsers.add(usrAddr);
    //                    }
    //                } catch (Exception e) {
    //                    e.printStackTrace();
    //                } finally {
    //                    // 加入适当的休眠时间
    //                    try {
    //                        Thread.sleep(5000); // 100毫秒
    //                    } catch (InterruptedException e) {
    //                        Thread.currentThread().interrupt(); // 恢复中断状态
    //                    }
    //                }
    //            }
    //        }, "RefreshThread");
    //        refreshThread.start();
    //    }
    public void RefreshUsers( HashSet<String> onlineUsers ) {
        long startTime = System.currentTimeMillis();
        long endTime = startTime + 5000; // 设置结束时间为当前时间加上5秒

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<?> future = executor.submit(() -> {
            while (System.currentTimeMillis() < endTime) {
                try {
                    DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
                    socket.receive(inPacket);
                    String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);
                    if ("echo".equals(msg)) {
                        String usrAddr = inPacket.getAddress().toString().substring(1);
                        onlineUsers.add(usrAddr);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            // 等待任务完成或者超时
            future.get(5, java.util.concurrent.TimeUnit.SECONDS);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdownNow(); // 尝试立即停止所有正在执行的任务
            try {
                if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
                    executor.shutdownNow(); // 再次尝试强制停止
                }
            } catch (InterruptedException ex) {
                executor.shutdownNow();
                Thread.currentThread().interrupt(); // 恢复中断状态
            }
        }
    }
    public HashSet<String> refreshOnlineUsers() {
        // 发送刷新在线用户请求
        send("detect", 3, null);
        // 刷新在线用户
        RefreshUsers(onlineUsers);
        return onlineUsers;
    }
}

UDPChatFx.java

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;

import static javafx.scene.input.KeyCode.ENTER;

public class UDPChatFx extends Application {
    private final UDPChat chat = new UDPChat();
    private final TextArea Output = new TextArea();
    private final TextField Input = new TextField();
    private final Button refreshButton = new Button("刷新在线用户");
    private final Button sendButton = new Button("发送");
    private final Button closeButton = new Button("关闭");
    private final ComboBox<String> ipComboBox = new ComboBox<>();

    public void start(Stage primaryStage) {
        BorderPane mainPane = new BorderPane();
        mainPane.setPadding(new Insets(10));

        ipComboBox.resize(150, 20);
        ipComboBox.setEditable(true);
        ipComboBox.getItems().add("所有用户");
        ipComboBox.getSelectionModel().select("所有用户");

        // 设置对话框区域
        VBox vbox = new VBox(10);
        vbox.getChildren().addAll(new Label("对话框"), Output);
        VBox.setVgrow(Output, Priority.ALWAYS);
        Output.setEditable(false);

        // 设置输入区域
        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        HBox.setHgrow(Input, Priority.ALWAYS);
        hbox.getChildren().addAll(ipComboBox, refreshButton, Input, sendButton, closeButton);

        VBox mainVBox = new VBox(10);
        mainVBox.getChildren().addAll(vbox, hbox);
        mainPane.setCenter(mainVBox);
        VBox.setVgrow(vbox, Priority.ALWAYS);

        Thread ReceiveThread = new Thread(() -> {
            // 退出线程
            while (!chat.isClosed()) {
                String msg = chat.receive();
                System.out.println("接收到消息: " + msg);
                Platform.runLater(() -> Output.appendText(msg + "\n"));
            }
        }, "ReceiveThread");
        ReceiveThread.start();
        // 设置关闭按钮事件
        closeButton.setOnAction(e -> {
            chat.close();
            System.exit(0);
        });

        // 设置刷新按钮事件
        refreshButton.setOnAction(e -> {
            System.out.println("刷新在线用户列表");
            HashSet<String> onlineUsers = chat.refreshOnlineUsers();
            ipComboBox.getItems().clear();
            ipComboBox.getItems().add("所有用户");
            ipComboBox.getItems().addAll(onlineUsers);
            ipComboBox.getSelectionModel().select("所有用户");
        });

        Input.setOnKeyPressed(e -> {
            if (e.getCode() == ENTER) {
                sendButton.fire();
            }
        });
        // 设置发送按钮事件
        sendButton.setOnAction(e -> {
            String msg = Input.getText();
            if (msg.isEmpty()) {
                return;
            }
            if (ipComboBox.getSelectionModel().getSelectedItem().equals("所有用户")) {
                System.out.println("群发: " + msg);
                chat.send(msg, 1, null); // 默认群发
            } else {
                InetAddress ip = null;
                try {
                    ip = InetAddress.getByName(ipComboBox.getSelectionModel().getSelectedItem());
                } catch (UnknownHostException ex) {
                    throw new RuntimeException(ex);
                }
                chat.send(msg, 2, ip); // 指定用户群发
            }
            Output.appendText("我: " + msg + "\n");
            Input.clear();
        });

        primaryStage.setScene(new Scene(mainPane, 760, 450));
        primaryStage.setTitle("UDP Chat Application");
        primaryStage.show();
    }

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

chapter07(HTTP程序设计)

使用Java的Socket类时,你只需要知道服务器的域名(或IP地址)和端口号就可以建立连接。Java的网络库会处理域名解析的过程,即将域名转换为IP地址。

import java.io.*;
import java.net.*;

public class SocketExample {
    public static void main(String[] args) {
        String hostname = "www.example.com"; // 服务器域名
        int port = 80; // 端口号

        try {
            // 创建Socket连接
            Socket socket = new Socket(hostname, port);
            System.out.println("连接到服务器: " + hostname + " 在端口: " + port);

            // 发送请求
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            out.println("GET / HTTP/1.1");
            out.println("Host: " + hostname);
            out.println("Connection: Close");
            out.println();

            // 接收响应
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String responseLine;
            while ((responseLine = in.readLine()) != null) {
                System.out.println(responseLine);
            }

            // 关闭连接
            in.close();
            out.close();
            socket.close();
        } catch (IOException e) {
            System.err.println("连接失败: " + e.getMessage());
        }
    }
}

关于HTTP协议的基本信息

你可以自行查阅相关资料,也可以查看我的博客(十分基础)python网络编程 | 0zxm

一、概述

HTTP 系统包括客户端软件(浏览器)和服务器软件(HTTP服务器)。早期的客户端软件,其主要工作可理解为文件下载和文件显示。

实际上现代的HTTP客户端比文件下载要复杂得多,它包括网页文件的下载、跨平台的本地显示,参数的传递,动态网页的实现,以及交互等功能。

HTTP系统程序设计包括:

  1. 客户端软件(web浏览器软件如Edge浏览器、360浏览器);
  2. 服务器软件(web服务器软件如IIS、Nginx、Tomcat等)。

HTTP系统客户端的工作过程是:

  1. 客户端软件和服务器建立连接(TCP的三次握手);
  2. 发送HTTP头格式协议;
  3. 接收网页文件;
  4. 显示网页。

HTTP系统服务端的工作过程:

  1. 服务器软件开启80端口;
  2. 响应客户的要求、完成TCP连接;
  3. 检查客户端的HTTP头格式发送客户请求的网页文件(含动态网页)。

image-20241024201749193

二、第一步:基于TCP套接字的网页下载程序设计

利用TCP客户套接字Socket和HTTP服务器进行信息交互,将网页的原始内容下载显示在图形界面中,具体工作如下:

  1. 新建一个包chapter08,将第3讲的TCPClient.java复制到此包下, 重命名为 HTTPClient.java;

  2. 创建 HTTPClientFX.java 程序,界面如图8.2所示,网页地址输入 www.baidu.com 进行测试,端口为 80,在“连接”按钮

    类似以往的编码方式, 放置连接服务器的代码和启动输入流 “读线程”;在“网页请求”按钮中发送 HTTP 请求头标准格式(关于

    HTTP请求头的更多信息可查阅互联网);在“退出”按钮中,相对之前章节的代码,不是发送 “bye” 表示结束,而是

    send("Connection:close" +"\r\n") 表示结束连接。

image-20241024210926344

具体来说,“网页请求”按钮中,我们的程序可以按顺序发送以下HTTP请求头:

GET / HTTP/1.1  //访问默认网页,注意GET后的 / 前后要留有空格 
HOST: address  //address 指服务器的 IP 或域名 
Accept: */* 
Accept-Language: zh-cn 
User-Agent: User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64)  
Connection: Keep-Alive

将上述的HTTP头格式构成一整个字符串,一致发送到HTTP服务器。

(注意:Windows系统下各个请求头字符串用\r\n分隔区分,不同操作系统的 User-Agent 也要相应修改;通常用StringBuffer类的append方法来拼接这些字符串,其toString()方法可将内容转换为字符串)。

HTTP状态码

如果网页信息显示区返回的第一条信息是“HTTP/1.1 200 OK”,则说明访问正常。现在将网页地址改为www.gdufs.edu.cn,其它不变,再次连接并点击网页请求,结果如图8.3所示:

image-20241024203013117

HTTPClient

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

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

    public HTTPClient(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;
    }
    public boolean isConnected(){
        return socket.isConnected();
    }

    // 实现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();
        }
    }
}

HTTPClientFx

import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;

public class HTTPClientFx extends Application {
    private final Button connectBtn = new Button("连接");
    private final Button requestBtn = new Button("网页请求");
    private final Button clearBtn = new Button("清空");
    private final Button quitBtn = new Button("退出");
    private final TextField urlField = new TextField();
    private final TextField portField = new TextField();
    private final TextArea responseArea = new TextArea();

    private HTTPClient httpc;

    private Thread receiveThread;

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

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("HTTP Client");
        BorderPane mainPane = new BorderPane();

        HBox topBox = new HBox();
        topBox.setSpacing(10);
        topBox.setPadding(new Insets(10));
        topBox.getChildren().addAll(new Label("网页地址: "), urlField, new Label("端口: "), portField, connectBtn);
        topBox.setAlignment(Pos.CENTER);

        VBox centerBox = new VBox(10);
        centerBox.setPadding(new Insets(10, 20, 10, 20));
        centerBox.getChildren().addAll(new Label("网页信息显示区: "), responseArea);
        centerBox.setAlignment(Pos.CENTER);

        HBox bottomBox = new HBox(10);
        bottomBox.setPadding(new Insets(10, 10, 10, 10));
        bottomBox.getChildren().addAll(requestBtn, clearBtn, quitBtn);
        bottomBox.setAlignment(Pos.CENTER_RIGHT);

        VBox mainBox = new VBox();
        mainBox.getChildren().addAll(topBox, centerBox, bottomBox);
        mainBox.setAlignment(Pos.CENTER);


        VBox.setVgrow(responseArea, Priority.ALWAYS);
        VBox.setVgrow(centerBox, Priority.ALWAYS);

        mainPane.setCenter(mainBox);

        Scene scene = new Scene(mainPane, 720, 450);

        // 按钮事件
        quitBtn.setOnAction(event -> {
            this.httpc.close();
            receiveThread.interrupt();
            httpc.send("Connection:close" + "\r\n");
            System.exit(0);
        });

        // 连接按钮事件
        connectBtn.setOnAction(event -> {
            try {
                String url = urlField.getText().trim();
                httpc = new HTTPClient(url, portField.getText().trim());
                if (httpc.isConnected()) {
                    this.responseArea.appendText("连接成功!\n");
                }
                receiveThread = new Thread(() -> {
                    while (true) {
                        String response = httpc.receive();
                        if (response == null) {
                            break;
                        }
                        Platform.runLater(() -> {
                            this.responseArea.appendText(response);
                        });
                    }
                }, "ReceiveThread");
                receiveThread.start();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });

        clearBtn.setOnAction(event -> {
            this.responseArea.clear();
        });

        requestBtn.setOnAction(event -> {
            httpc.send("GET / HTTP/1.1");
            httpc.send("Host: " + this.urlField.getText().trim());
            httpc.send("Connection: Close");
            httpc.send("\r\n");
        });


        urlField.setText("www.baidu.com");
        portField.setText("80");

        this.responseArea.setEditable(false);
        this.responseArea.setWrapText(true);

        // 添加滚轮事件
        responseArea.setOnScroll(event -> {
            if (event.isControlDown()) {
                if (event.getDeltaY() > 0) {
                    responseArea.setStyle("-fx-font-size: " + (responseArea.getFont().getSize() + 1) + "px;");
                } else {
                    responseArea.setStyle("-fx-font-size: " + (responseArea.getFont().getSize() - 1) + "px;");
                }
            }
        });

        // 界面显示
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

第二步: 基于SSLSocket的网页下载程序设计

HTTP协议是一种基于普通套接字的不安全的协议,要访问HTTPS站点,就不能用普通的客户端套接字,而是使用SSL套接字。Java 安全套接字扩展(Java Secure Socket Extension,JSSE)为基于 SSL 和 TLS 协议的Java网络应用程序提供了Java API以及参考实现,相关的类都定义在 javax.net.ssl 包下面,我们这里只使用其客户端的SSLSocket套接字。

SSL/TLS协议是在TCP连接的基础上实现的,通常用于提高数据传输的安全性。这意味着SSL套接字实际上是建立在TCP套接字之上的。其工作流程如下:

  1. TCP连接建立:首先,客户端和服务器通过TCP建立一个可靠的连接。这个过程包括TCP的三次握手。
  2. SSL握手:一旦TCP连接建立,接下来进行SSL/TLS握手。握手过程包括以下几个步骤:
    • 客户端发送一个“客户端问候”消息,包含支持的TLS版本、加密算法等信息。
    • 服务器响应“服务器问候”消息,选择TLS版本和加密算法,并发送其数字证书
    • 客户端验证服务器的证书,确保服务器的身份。
    • 双方生成共享密钥,用于加密后续的通信。
  3. 安全数据传输:握手完成后,客户端和服务器就可以使用生成的密钥进行加密数据的传输,确保数据在传输过程中是安全的。
  4. 连接关闭:通信完成后,双方可以按照TLS协议关闭连接,确保安全地结束会话。

SSLSocket相对之前学习的客户端套接字,只是创建方法不同,SSLSocket对象 由SSLSocketFactory创建

HTTPSClient

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class HTTPSClient {
    // 定义SSL套接字
    private final SSLSocket sslSocket;
    // 定义SSL套接字工厂
    private final SSLSocketFactory sslSocketFactory;
    private final PrintWriter pw; // 定义字符输出流
    private final BufferedReader br; // 定义字符输入流

    public HTTPSClient(String ip, String port) throws IOException {
        // 创建工厂对象()使用静态方法getDefault()获取默认的SSL套接字工厂
        sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        // 创造SSL套接字对象
        sslSocket = (SSLSocket) sslSocketFactory.createSocket(ip, Integer.parseInt(port));
        pw = new PrintWriter(new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.UTF_8),true);
        br = new BufferedReader(new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.UTF_8));
        // 开始SSL握手
        sslSocket.startHandshake();
    }

    public void send(String message) {
        pw.println(message);
    }

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

    public boolean isConnected() {
        return sslSocket.isConnected();
    }

    // 实现close方法以关闭socket连接及相关的输入输出流
    public void close() {
        try {
            if (pw != null) {
                pw.close(); // 关闭PrintWriter会先flush再关闭底层流
            }
            if (br != null) {
                br.close(); // 关闭BufferedReader
            }
            if (sslSocket != null) {
                sslSocket.close(); // 关闭Socket连接
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

HTTPSClientFx

import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;

public class HTTPSClientFx extends Application {
    private final Button connectBtn = new Button("连接");
    private final Button requestBtn = new Button("网页请求");
    private final Button clearBtn = new Button("清空");
    private final Button quitBtn = new Button("退出");
    private final TextField urlField = new TextField();
    private final TextField portField = new TextField();
    private final TextArea responseArea = new TextArea();

    private HTTPSClient httpsc;

    private Thread receiveThread;

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

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("HTTPS Client");
        BorderPane mainPane = new BorderPane();

        HBox topBox = new HBox();
        topBox.setSpacing(10);
        topBox.setPadding(new Insets(10));
        topBox.getChildren().addAll(new Label("网页地址: "), urlField, new Label("端口: "), portField, connectBtn);
        topBox.setAlignment(Pos.CENTER);

        VBox centerBox = new VBox(10);
        centerBox.setPadding(new Insets(10, 20, 10, 20));
        centerBox.getChildren().addAll(new Label("网页信息显示区: "), responseArea);
        centerBox.setAlignment(Pos.CENTER);

        HBox bottomBox = new HBox(10);
        bottomBox.setPadding(new Insets(10, 10, 10, 10));
        bottomBox.getChildren().addAll(requestBtn, clearBtn, quitBtn);
        bottomBox.setAlignment(Pos.CENTER_RIGHT);

        VBox mainBox = new VBox();
        mainBox.getChildren().addAll(topBox, centerBox, bottomBox);
        mainBox.setAlignment(Pos.CENTER);


        VBox.setVgrow(responseArea, Priority.ALWAYS);
        VBox.setVgrow(centerBox, Priority.ALWAYS);

        mainPane.setCenter(mainBox);

        Scene scene = new Scene(mainPane, 720, 450);

        // 按钮事件
        quitBtn.setOnAction(event -> {
            httpsc.send("Connection:close" + "\r\n");
            this.httpsc.close();
            receiveThread.interrupt();
            System.exit(0);
        });

        // 连接按钮事件
        connectBtn.setOnAction(event -> {
            try {
                String url = urlField.getText().trim();
                httpsc = new HTTPSClient(url, portField.getText().trim());
                if (httpsc.isConnected()) {
                    this.responseArea.appendText("连接成功!\n");
                }
                receiveThread = new Thread(() -> {
                    while (true) {
                        String response = httpsc.receive();
                        if (response == null) {
                            break;
                        }
                        Platform.runLater(() -> {
                            this.responseArea.appendText(response);
                        });
                    }
                }, "ReceiveThread");
                receiveThread.start();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });

        clearBtn.setOnAction(event -> {
            this.responseArea.clear();
        });

        requestBtn.setOnAction(event -> {
            httpsc.send("GET / HTTP/1.1");
            httpsc.send("Host: " + this.urlField.getText().trim());
            httpsc.send("Accept: */*");
            httpsc.send("Accept-Language: zh-cn");
            httpsc.send("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
            httpsc.send("Connection: Keep-Alive");
            httpsc.send("\r\n");
        });


        urlField.setText("www.gdufs.edu.cn");
        portField.setText("443");

        this.responseArea.setEditable(false);
        this.responseArea.setWrapText(true);

        // 添加滚轮事件
        responseArea.setOnScroll(event -> {
            if (event.isControlDown()) {
                if (event.getDeltaY() > 0) {
                    responseArea.setStyle("-fx-font-size: " + (responseArea.getFont().getSize() + 1) + "px;");
                } else {
                    responseArea.setStyle("-fx-font-size: " + (responseArea.getFont().getSize() - 1) + "px;");
                }
            }
        });

        // 界面显示
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

第三步:基于URL类的网页下载程序设计

前面直接发送http请求的程序,对于复杂的地址,例如指向具体页面的地址,无法完成网页下载任务,我们可以使用基于URL类的解决方案。

URL(Uniform Resource Locator)中文名为统一资源定位符,用于表示资源地址,资源如网页或者FTP地址等。

URL 的格式为 protocol://资源地址,protocol可以是HTTP、HTTPS、FTP 和 File,资源地址中可以带有端口号及查询参数,具体可以自行搜索URL的知识。

java.net包中定义了URL类,该类用来处理有关URL的内容。并且其封装有一个InputStream返回类型的openStream()方法,我们的程序就可以读取 这个字节输入流来获得对应内容。

URLClientFx

  1. 创建程序URLClientFX.java,界面布局可参见如图8.5所示:

image-20241025143505985

URLClient

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class URLClient {
    private URL url;
    private BufferedReader br;

    public URLClient(String urlString) throws IOException {
        url = new URL(urlString);
        //获得url的字节流输入
        InputStream in = url.openStream();
        br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));

    }
    public String readLine() throws IOException {
        return br.readLine();
    }

    public void close() throws Exception {
        br.close();
    }
}

URLClientFx

import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class URLClientFx extends Application {
    private final Button sendBtn = new Button("发送");
    private final Button quitBtn = new Button("退出");
    private final TextField urlField = new TextField();
    private final TextArea responseArea = new TextArea();
    private URLClient urlClient;

    private Thread receiveThread;

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

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("URL Downloader");
        BorderPane mainPane = new BorderPane();

        VBox centerBox = new VBox(10);
        centerBox.setPadding(new Insets(10, 20, 10, 20));
        centerBox.getChildren().addAll(new Label("网页信息显示区: "), responseArea);
        centerBox.setAlignment(Pos.CENTER);

        VBox urlBox = new VBox(10);
        urlBox.setPadding(new Insets(10, 20, 10, 20));
        urlBox.getChildren().addAll(new Label("输入URL地址: "), urlField);
        urlBox.setAlignment(Pos.CENTER_LEFT);

        HBox bottomBox = new HBox(10);
        bottomBox.setPadding(new Insets(10, 20, 10, 20));
        bottomBox.getChildren().addAll(sendBtn, quitBtn);
        bottomBox.setAlignment(Pos.CENTER_RIGHT);

        VBox mainBox = new VBox();
        mainBox.getChildren().addAll(centerBox, urlBox, bottomBox);
        mainBox.setAlignment(Pos.CENTER);


        VBox.setVgrow(responseArea, Priority.ALWAYS);
        VBox.setVgrow(centerBox, Priority.ALWAYS);

        mainPane.setCenter(mainBox);

        Scene scene = new Scene(mainPane, 720, 450);

        // 按钮事件处理
        quitBtn.setOnAction(event -> System.exit(0));
        sendBtn.setOnAction(
                event -> {
                    try {
                        String url = this.urlField.getText().trim();
                        try {
                            this.urlClient = new URLClient(url);
                        } catch (MalformedURLException e) {
                            responseArea.appendText("URL格式错误!\n");
                            return;
                        }
                        Thread receiveThread = new Thread(() -> {
                            while (true) {
                                try {
                                    String line = this.urlClient.readLine();
                                    if (line == null) {
                                        break;
                                    }
                                    Platform.runLater(
                                            () -> {
                                                this.responseArea.appendText(line);
                                            }
                                    );
                                } catch (IOException e) {
                                    throw new RuntimeException(e);
                                }
                            }
                        }, "receiveThread");
                        receiveThread.start();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }

                }
        );
        urlField.setText("http://www.baidu.com");
        responseArea.setEditable(false);
        responseArea.setWrapText(true);
        // 界面显示
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

chapter08(邮箱程序设计)

教学与实践目的:学会网络邮件发送的程序设计技术。

1.SMTP协议

  • 邮件传输协议包括 SMTP(简单邮件传输协议,RFC821)及其扩充协议 MIME;

  • 邮件接收协议包括 POP3 和功能更强大的 IMAP 协议。 服务邮件发送的服务器其端口为 25(如果开启 ssl 一般使用 465 端口,目前QQ邮箱已经强制必须使用加密连接方式,所以以下实验使用 465 端口), 服务邮件接收的服务器端口为 110(如果开启 SSL 一般使用 995 端口)。

SMTP协议解读以及如何使用SMTP协议发送电子邮件 - 一只会铲史的猫 - 博客园

  1. SMTP(简单邮件传输协议)

    • 命令有序性:SMTP的命令需要按照特定的顺序执行,以完成邮件的发送任务。每个命令都有其特定的作用,并且必须按照正确的顺序组合使用。
    • 请求应答模式:SMTP遵循请求应答式协议,客户端发送命令后,服务器会返回相应的响应。这种模式确保了命令的执行和结果的确认。
    • 响应格式:SMTP的响应格式通常包括一个三位数字的响应码,后面跟着响应描述,这与HTTP协议的响应格式相似。
  2. POP3(邮局协议第三版)

    • 命令独立性:POP3的命令如LIST、STAT、UIDL、TOP、RETR、DELE等,都可以独立使用,每个命令都有其特定的功能,如查看邮件列表、获取邮件内容等。
    • 数据流处理:在接收邮件时,POP3需要以流的方式处理数据,因为邮件数据可能不是一次性完整发送,而是分批次到达。
  3. Socket编程中的差异

    • 发送数据的简单性:在socket编程中,发送数据(如SMTP)相对简单,因为发送方可以按照自己的节奏发送数据,不需要考虑接收方的状态。
    • 接收数据的复杂性:接收数据(如POP3)需要判断数据是否完全接收,处理数据流的完整性,这比发送数据要复杂。
  4. 请求应答式协议

    • SMTP和HTTP:SMTP和HTTP都是请求应答式协议,客户端发送请求后,服务器返回响应。
    • HTTP的Keep-Alive:HTTP协议在设置为Keep-Alive时,可以进行多次请求和响应的交互,否则通常只有一次交互机会。

2.第三方邮箱设置

邮箱设置一定要开启 smtp/pop3 服务(以 QQ邮箱为例,在[邮箱设置]中 的[账户]中开启相关服务(获取授权码)

image-20241106102750149

3.BASE64编码

Base64编码是一种广泛使用的编码方法,主要用于在不同的系统和应用程序之间传输二进制数据。

  1. 数据兼容性:Base64编码可以将二进制数据转换为纯文本格式,这样就可以在只支持文本传输的系统中传输二进制数据,例如电子邮件、网页等。

  2. 数据隐藏:虽然Base64编码不是加密方法,但它可以隐藏原始数据的内容,使得数据在传输过程中不易被识别。

  3. 数据完整性:Base64编码可以确保数据在传输过程中不会因为某些字符被错误处理而损坏,例如某些字符在URL中可能有特殊含义。

  4. 国际化:Base64编码可以处理任何语言的字符,因此它在国际化的应用中非常有用。

  5. 存储二进制数据:在某些情况下,需要在文本文件或数据库中存储二进制数据,Base64编码提供了一种方便的方式来实现这一点。

  6. 数据传输:在网络协议中,如HTTP,Base64编码常用于传输非ASCII数据,例如在HTTP响应头中传输图片或文件。

  7. 兼容性和标准化:Base64编码是一种标准化的编码方式,这意味着不同系统和应用程序可以使用相同的方法来解码数据,确保了互操作性。

  8. 安全性:虽然Base64编码本身不提供安全性,但它常用于加密数据的前处理步骤,使得加密过程更加方便。

尽管Base64编码有许多优点,但它也有缺点,比如编码后的数据体积会增加约33%,因此在需要传输大量数据时可能会影响效率。此外,Base64编码的数据可以被解码,因此它不适用于需要严格保密的数据。

这是一段base64的c++实现代码

#ifndef BASE64_H
#define BASE64_H

#include <string>

using namespace std;

// Valid Base64 Characters
const char base64_chars[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    "abcdefghijklmnopqrstuvwxyz"
    "0123456789+/";

// Return true of given character is valid Base64 Character
bool is_base64(unsigned char c);

// Return Encoded Base64 String
string base64_encode(const string &input);

// Return Decoded Base64 String
string base64_decode(const string &input);

#endif
/*
GitHub: @farhanalioffical
Free to use
*/
bool is_base64(unsigned char c)
{
    return (isalnum(c) || (c == '+') || (c == '/'));
}

inline string base64_encode(const string &input)
{
    string output;
    int i = 0;
    int j = 0;
    unsigned char char_array_3[3];
    unsigned char char_array_4[4];

    int input_len = input.length();
    while (input_len--)
    {
        char_array_3[i++] = input[j++];
        if (i == 3)
        {
            char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
            char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
            char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
            char_array_4[3] = char_array_3[2] & 0x3f;

            for (i = 0; i < 4; i++)
                output += base64_chars[char_array_4[i]];
            i = 0;
        }
    }

    if (i)
    {
        for (j = i; j < 3; j++)
            char_array_3[j] = '\0';

        char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
        char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
        char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
        char_array_4[3] = char_array_3[2] & 0x3f;

        for (j = 0; j < i + 1; j++)
            output += base64_chars[char_array_4[j]];

        while (i++ < 3)
            output += '=';
    }

    return output;
}

inline string base64_decode(const string &input)
{
    string output;
    int i = 0;
    int j = 0;
    int in_ = 0;
    unsigned char char_array_4[4], char_array_3[3];

    int input_len = input.length();
    while (input_len-- && (input[in_] != '=') && is_base64(input[in_]))
    {
        char_array_4[i++] = input[in_];
        in_++;
        if (i == 4)
        {
            for (i = 0; i < 4; i++)
                char_array_4[i] = string(base64_chars).find(char_array_4[i]);

            char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

            for (i = 0; i < 3; i++)
                output += char_array_3[i];
            i = 0;
        }
    }

    if (i)
    {
        for (j = i; j < 4; j++)
            char_array_4[j] = 0;

        for (j = 0; j < 4; j++)
            char_array_4[j] = string(base64_chars).find(char_array_4[j]);

        char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
        char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

        for (j = 0; j < i - 1; j++)
            output += char_array_3[j];
    }

    return output;
}

4.openssl手动体验smtp协议

注意:由于 QQ邮箱 的 smtp 服务已经强制使用 SSL 连接,不能使用 telnet 来进行明文连接,可以使用 OpenSSL 代替 telnet,通过交互方式体验 SMTP 协议—openssl下载地址

将其解压到任意一个盘的根目录,例如解压到D盘,其执行文件 openssl.exe 位于 libressl 下的 bin 目录中,可以使用右键菜单进入终端,或者 win+R 运行 cmd 进入终端,再通过 cd 命令进入该目录

4.1 使用 OpenSSL 连接 QQ 邮箱的 SMTP 服务器

在终端中输入以下命令,使用 SSL 方式连接 QQ 邮箱的 SMTP 服务器:

openssl s_client -quiet -connect smtp.qq.com:465

连接成功后,会有以 220 开头的一串返回信息,然后就以交互方式完成如下的对话过程。其中加粗加亮字体是 SMTP 协议支持的命令,必须输入一致,其余内容根据具体情况填写,双斜杠内容是注释,不用输入:

  • HELO hostname // hostname 可以是 IP 或其他随意别名

  • AUTH LOGIN // 在输入 auth login 回车后,先粘贴 base64 程序编码的完整邮箱名并回车;再粘贴 base64 编码的授权码并回车,成功会有 Authentication successful 提示信息

  • MAIL FROM:<your mail address> // 尖括号中填写完整邮箱名,用于发送邮件(注意冒号后面别有空格)

  • RCPT TO:<your mail address> // 接收方的邮箱,尖括号中暂时填写和上面一样的邮箱地址,即自己发送邮件给自己,验证是否成功

  • DATA // 在输入 data 回车后,接下来开始设置邮件头,邮件头除了邮件标题,还包括发件人地址和收件人地址

  • From:xxx@qq.com // 发件人的邮箱地址,要和 MAIL FROM 一致

  • Subject: this is simple mail // 邮件的标题

  • To:xxx@qq.com // 会显示在邮件接收者栏上,和 RCPT TO 内容一致

  • 在这里回车再多发送一行空行,来分隔邮件内容,下面就是邮件正文内容。即在输入过程中标题和正文内容之间多空了一行

邮件正文

Hello! 很好!
  • . //在邮件正文发送完毕后,单独用一行输入一个英文的小圆点,作为结束标志,然后回车
  • QUIT //结束通信

知识点:以上基于命令行交互方式的邮件发送方法,可以让你不需要其他专用软件的帮助就可以完成简单邮件的发送。以上是以QQ邮箱为例,如果使用的是其它邮箱,则需要对应修改连接的 smtp 服务器。

5.窗口程序实现自动发邮件

  • 设置如图7.3所示的邮件发送窗口(界面仅供参考,多行文本域 TextArea 控件用于输入邮件正文内容),并命名为TCPMailClientFX2.java,将手动操作的 SMTP 过程封装到程序中,实现邮件自动发送

  • 注意不要短时间频繁发送,会被邮件服务器拒绝 smtp 服务(特别是不要上一个邮件还没发完, 又点击发送按钮,很容易被拒绝服务)

image-20241106111118466

注意:邮件命令需要一行一行地发送,使用程序自动发送过程中需要设置一定的睡眠等待时间。

可以使用Thread.sleep()方法

public void send(String msg) {
    //输出字符流,由 Socket 调用系统底层函数,经网卡发送字节流
    pw.println(msg);
    try {
        //进行邮件交互、发送 smtp 指令之间应该暂停一段时间
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

6.优化TCPMailClientFx_2.java

问题描述

TCPMailclientFX2 在发送邮件时无法看到服务器的反馈信息,这不利于了解发送状态和排查错误。例如,语法错误提示信息、邮件服务器的 502 错误(发送次数过多导致发送失败)或 503 错误(Send command HELO/EHL0 first)等。

优化目标

  • 添加一个文本域(TextArea 控件)用于显示邮件服务器的反馈信息。
  • 解决同时发送邮件和显示服务器信息造成的卡顿问题。

优化方案

  1. 使用线程池:为了避免资源浪费和接收信息混乱,使用固定线程数为2的线程池。
  2. 多线程处理:在发送按钮的代码中,开启两个新线程:
    • 一个线程用于显示服务器的反馈信息。
    • 另一个线程用于执行邮件发送操作。
  3. 线程执行顺序:先执行显示服务器信息的线程,暂停100毫秒后再执行发送线程,确保发送线程后执行。

注意事项

  • 在上一个邮件发送的对话还没关闭前,不要再次点击发送按钮,以免被邮件服务器拒绝服务。

优化后的程序界面

参考界面如图7.4所示(此处无法显示图片,需在实际文档中查看)。

image-20241106111438671

注意:使用socket获取输出流时,一定设置自动刷新autoFlush=True

pw = new PrintWriter(new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.UTF_8), true);

7.最终代码

BASE64.java

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class BASE64 {
    public static void main(String[] args) {
        String usrName = "aaaaa@163.com";
        String passwd = "Mn6tQtYHKZEpLN";


        String encodedUsrName = encode(usrName);
        String encodedPasswd = encode(passwd);

        System.out.println("Encoded Username: " + encodedUsrName);
        System.out.println("Encoded Password: " + encodedPasswd);
    }
    public static String encode(String str) {
//        return new sun.misc.BASE64Encoder().encode(str.getBytes());
        Base64.Encoder encoder = Base64.getEncoder();
        String encodedMmsg = null;
        try {
            encodedMmsg = encoder.encodeToString(str.getBytes(StandardCharsets.UTF_8));
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        return encodedMmsg;
    }
}

TCPMailClient.java

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class TCPMailClient {
    // 定义SSL套接字
    private final SSLSocket sslSocket;
    private final SSLSocketFactory sslSocketFactory;
    private final PrintWriter pw; // 定义字符输出流
    private final BufferedReader br; // 定义字符输入流

    public TCPMailClient(String ip, String port) throws IOException {
        // 创建工厂对象()使用静态方法getDefault()获取默认的SSL套接字工厂
        // 定义SSL套接字工厂
        sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        // 创造SSL套接字对象
        sslSocket = (SSLSocket) sslSocketFactory.createSocket(ip, Integer.parseInt(port));
        //注意:这里一定要设置自动刷新
        pw = new PrintWriter(new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.UTF_8), true);
        br = new BufferedReader(new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.UTF_8));
        // 开始SSL握手
        // sslSocket.startHandshake();
    }

    public void send(String message) {
        try {
            pw.println(message);
        } catch (Exception e) {
            System.out.println("发送消息失败");
            e.printStackTrace();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }


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

    public boolean isConnected() {
        return sslSocket.isConnected();
    }

    // 实现close方法以关闭socket连接及相关的输入输出流
    public void close() {
        try {
            if (pw != null) {
                pw.close(); // 关闭PrintWriter会先flush再关闭底层流
            }
            if (br != null) {
                br.close(); // 关闭BufferedReader
            }
            if (sslSocket != null) {
                sslSocket.close(); // 关闭Socket连接
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

TCPMailClientFx_1.java

import javafx.application.Application;
import javafx.application.Platform;
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 TCPMailClientFx_1 extends Application {

    private final Button btnCon = new Button("连接");

    private final Button btnSend = new Button("发送");

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

    private final TextField input = new TextField();

    private TCPMailClient tcpmail;

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

    public void start(Stage primaryStage) {

        primaryStage.setTitle("邮件测试");
        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(10);
        vBox.setPadding(new Insets(10, 10, 10, 10));
        vBox.getChildren().addAll(textArea);
        vBox.setAlignment(Pos.CENTER);


        HBox inputBox = new HBox(10);
        inputBox.setPadding(new Insets(10, 10, 10, 10));
        inputBox.getChildren().addAll(input, btnSend);
        inputBox.setAlignment(Pos.CENTER);
        HBox.setHgrow(input, Priority.ALWAYS);
        // 获取加密后的邮箱账号密码
        String usrName = "aaa@163.com";
        String passwd = "MHKNYtEwZpLn6ttQ";

        String encodedUsrName = BASE64.encode(usrName);
        String encodedPasswd = BASE64.encode(passwd);

        // 设置按钮的交互效果
        btnCon.setOnAction(event -> {
            String ip = IpAdd_input.getText().trim();
            String port = Port_input.getText().trim();
            if (ip.isEmpty() || port.isEmpty()) {
                return;
            }
            try {
                tcpmail = new TCPMailClient(ip, (port));
                Thread recieveThread = new Thread(() -> {
                    while (true) {
                        String msg = tcpmail.receive();
                        if (msg == null) {
                            continue;
                        }
                        Platform.runLater(() -> {
                            textArea.appendText(msg + "\n");
                        });
                    }
                }, "recieveThread");
                recieveThread.start();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        btnSend.setOnAction(event -> {
            String text = input.getText();
            if (text.isEmpty()) {
                return;
            }
            if (!tcpmail.isConnected()) {
                System.out.println("服务器未连接");
                return;
            }
            try {
                tcpmail.send(text + "\r\n");
                input.clear();
                Platform.runLater(() -> {
                    textArea.appendText("已发送:" + text + "\n");
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        input.setOnKeyPressed((event) -> {
            if (event.getCode() == KeyCode.ENTER) {
                btnSend.fire();
            }
        });

        Port_input.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
            if (event.getCode() == KeyCode.ENTER) {
                btnCon.fire();
            }
        });

        mainVBox.getChildren().addAll(hBox, vBox, inputBox);

        mainPane.setCenter(mainVBox);
        Scene scene = new Scene(mainPane);

        IpAdd_input.setText("smtp.163.com");
        Port_input.setText("465");

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

TCPMailClientFx_2.java

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static javafx.scene.layout.Priority.ALWAYS;

public class TCPMailClientFx_2 extends Application {
    private static final String TITLE = "TCP邮箱客户端";

    private final Button btnSend = new Button("发送");
    private final Button btnQuit = new Button("退出");
    private final TextField mailServerAdd = new TextField(" 邮件服务器地址 ");
    private final TextField mailServerPort = new TextField(" 邮件服务器端口 ");
    private final TextField mailFrom = new TextField(" 发件人地址 ");
    private final TextField mailTo = new TextField(" 收件人地址 ");
    private final TextField mailSubject = new TextField(" 邮件主题 ");
    private final TextArea mailContent = new TextArea(" 邮件内容 ");
    private final TextArea responseArea = new TextArea();
    public ExecutorService service = Executors.newFixedThreadPool(2);
    private TCPMailClient tcpMailClient;

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

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle(TITLE);

        HBox hbox1 = new HBox(10);
        hbox1.getChildren().addAll(new Label("邮件服务器地址:"), mailServerAdd, new Label("邮件服务器端口:"), mailServerPort);
        // 设置输入框自动拉伸
        mailServerAdd.setPrefColumnCount(15);
        mailServerPort.setPrefColumnCount(15);
        hbox1.setAlignment(javafx.geometry.Pos.CENTER);

        HBox hbox2 = new HBox(10);
        hbox2.getChildren().addAll(new Label("发件人地址:"), mailFrom, new Label("收件人地址:"), mailTo);
        mailFrom.setPrefColumnCount(15);
        mailTo.setPrefColumnCount(15);
        hbox2.setAlignment(javafx.geometry.Pos.CENTER);

        HBox hbox3 = new HBox(10);
        hbox3.getChildren().addAll(new Label("邮件主题:"), mailSubject);
        HBox.setHgrow(mailSubject, ALWAYS);
        hbox3.setAlignment(javafx.geometry.Pos.CENTER);

        HBox showBox = new HBox(10);
        responseArea.setEditable(false);
        responseArea.setWrapText(true);
        showBox.getChildren().addAll(this.mailContent, responseArea);

        HBox hbox4 = new HBox(10);
        hbox4.getChildren().addAll(btnSend, btnQuit);

        VBox mainBox = new VBox(10);
        mainBox.setPadding(new javafx.geometry.Insets(10));
        mainBox.getChildren().addAll(hbox1, hbox2, hbox3, showBox, hbox4);

        VBox.setVgrow(this.mailContent, ALWAYS);
        VBox.setVgrow(showBox, ALWAYS);

        primaryStage.setScene(new javafx.scene.Scene(mainBox, 700, 600));
        primaryStage.show();

        // 添加默认文字
        mailServerAdd.setText("smtp.163.com");
        mailServerPort.setText("465");
        mailFrom.setText("m15813109801@163.com");
        mailTo.setText("m15813109801@163.com");
        mailSubject.setText("测试邮件");
        mailContent.setText("这是一封测试邮件。");

        btnQuit.setOnAction(event -> {
            // 退出程序代码
            System.exit(0);
        });

        btnSend.setOnAction(event -> {
            // 发送邮件代码
            String smtpAddr = mailServerAdd.getText().trim();
            String smtpPort = mailServerPort.getText().trim();
            try {
                tcpMailClient = new TCPMailClient(smtpAddr, smtpPort);
                // 启动两个线程,一个负责接收服务器响应,一个负责发送邮件
                service.execute(new ReceiveHandler());
                service.execute(new SendHandler());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

    public class SendHandler implements Runnable {
        public SendHandler() {
            System.out.println("SendHandler created.");
        }

        @Override
        public void run() {
            tcpMailClient.send("HELO myfriend");

            tcpMailClient.send("AUTH LOGIN");

            String userName = "aaa@163.com";
            String authCode = "MfaffafafggtQN";

            String msg = BASE64.encode(userName);
            tcpMailClient.send(msg);

            msg = BASE64.encode(authCode);
            tcpMailClient.send(msg);

            msg = "MAIL FROM:<" + mailFrom.getText().trim() + ">";
            tcpMailClient.send(msg);

            msg = "RCPT TO:<" + mailTo.getText().trim() + ">";
            tcpMailClient.send(msg);

            msg = "DATA";
            tcpMailClient.send(msg);

            msg = "FROM:" + mailFrom.getText().trim();
            tcpMailClient.send(msg);

            msg = "SUBJECT:" + mailSubject.getText().trim();
            tcpMailClient.send(msg);

            msg = "TO:" + mailTo.getText().trim();

            tcpMailClient.send(msg);

            // 发送空行,隔开邮件正文和内容
            tcpMailClient.send("");

            msg = "这是一封测试邮件。";
            tcpMailClient.send(msg);

            msg = ".";
            tcpMailClient.send(msg);

            tcpMailClient.send("QUIT");
        }
    }

    private class ReceiveHandler implements Runnable {
        public ReceiveHandler() {
            System.out.println("ReceiveHandler created.");
        }

        @Override
        public void run() {
            String msg = null;
            while ((msg = tcpMailClient.receive()) != null) {
                String tmpMsg = msg;
                Platform.runLater(() -> {
                    responseArea.appendText(tmpMsg + "\n");
                });
            }
        }
    }
}

扩展练习一、验证POP3电子邮件接收协议

可以使用 OpenSSL 或 TCPMailClientFX1.java 来验证,这里以终端运行 OpenSSL 为例:

  • openssl s_client -quiet -connect pop.qq.com:995 // 建立连接

  • USER 完整邮箱名 // 不需要 base64 编码

  • PASS 授权码 // 不需要 base64 编码

  • STAT 或 LIST // stat 统计当前邮箱数量和占用空间,list 列出所有邮件 信息,如果邮件特别多,list 会比较耗时

  • RETR 2 // 显示第 2 个邮件,注意,显示的都是编码的内容,正常无法阅读

  • QUIT // 退出

扩展练习二、JavaMail API 程序设计

为了简化邮件客户程序的开发,Oracle公司制定了 JavaMail API,它封装了按照各种邮件通信协议,如 IMAP、POP3 和 SMTP 等与邮件服务器通信的细节,为 Java 应用程序提供了收发电子邮件的公共接口,用于开发邮件收发客户端,有兴趣的同学可进一步学习

JavaMail API 使用指南

对于加入外部 jar,在项目开发中经常使用 maven 来进行配置,但对于我们的练习项目,完全可以手动下载 jar,添加到项目中。

1. 下载和配置 JavaMail API 类库
  • 下载:从 JavaMail GitHub Releases 下载 javax.mail.jar 文件。
  • 添加到项目:在 IntelliJ IDEA 项目中,创建一个 lib 文件夹,并将下载的 .jar 文件放入其中。
  • 配置项目:按照图 7.6 的方式,将 lib 文件夹下的 .jar 文件导入到项目中。

image-20241106112829974

2. 阅读理解 JavaMailAPIClient.java
  • 程序功能:该程序使用 JavaMail API 实现邮件的发送和接收功能,以 QQ 邮箱服务器为例。
  • 变量设置:将程序中的邮箱服务器地址、端口、用户名和授权码等变量修改为自己的邮箱信息。
3. 运行及调试
  • 发送邮件:使用 sendMail 方法,输入发送者邮箱地址、授权码、接收者邮箱地址、邮件主题和内容。
  • 接收邮件:使用 receiveMail 方法,输入接收邮箱地址和授权码,程序将连接到邮箱服务器并读取邮件。

JavaMailAPIClient.java 程序说明

import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.text.SimpleDateFormat;
import java.util.Properties;

public class JavaMailAPIClient {
    // Session 类表示邮件会话,是 JavaMail API 的最高层入口类。
    protected Session session;
    // 定义接收邮件服务器
    private String receiveHost = "pop.qq.com";
    // 定义发送邮件服务器
    private String sendHost = "smtp.qq.com";
    // 定义接收邮件服务器的端口
    private String popPort = "995";
    // 定义发送邮件服务器的端口
    private String smtpPort = "465";

    public JavaMailAPIClient() {}

    public JavaMailAPIClient(String receiveHost, String popPort, String sendHost, String smtpPort) {
        this.receiveHost = receiveHost;
        this.popPort = popPort;
        this.sendHost = sendHost;
        this.smtpPort = smtpPort;
    }

    /**
     * init()方法负责邮箱服务器相关的初始配置,例如服务器地址、协议、端口号、SSL 配置
     */
    public void init() {
        Properties props = new Properties();
        // 发送邮件服务器相关设置
        props.setProperty("mail.transport.protocol", "smtp"); // 发送邮件使用的协议
        props.setProperty("mail.smtp.host", sendHost); // SMTP 服务器地址
        // SSL 相关配置
        props.setProperty("mail.smtp.ssl.enable", "true"); // 显式启用 SSL
        props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.setProperty("mail.smtp.socketFactory.fallback", "false");
        props.setProperty("mail.smtp.socketFactory.port", smtpPort); // 设置 SSL smtp 端口
        // 认证设置
        props.setProperty("mail.smtp.auth", "true");
        // 接收邮件服务器相关设置
        props.setProperty("mail.store.protocol", "pop3"); // 使用 pop3 协议
        props.setProperty("mail.pop3.host", receiveHost); // pop3 服务器地址
        // SSL 相关配置
        props.setProperty("mail.pop3.ssl.enable", "true");
        props.setProperty("mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.setProperty("mail.pop3.socketFactory.fallback", "false");
        props.setProperty("mail.pop3.socketFactory.port", popPort);
        // 创建 session 对象,加载配置
        session = Session.getDefaultInstance(props);
        // 开启 Debug 模式查看详细的发送 log,调试正常后可注释关闭
        // session.setDebug(true);
    }

    /**
     * 发送邮件
     *
     * @param fromAddr 发送邮件的邮箱地址
     * @param authCode 发送邮件的邮箱的授权码
     * @param toAddr   接收邮件的邮箱地址
     * @param subject  发送邮件的主题
     * @param content  发送邮件的正文内容
     */
    public void sendMail(String fromAddr, String authCode, String toAddr, String subject, String content) throws MessagingException {
        MimeMessage message = new MimeMessage(session);
        message.setFrom(new InternetAddress(fromAddr));
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(toAddr));
        message.setSubject(subject);
        message.setText(content);
        Transport.send(message, fromAddr, authCode);
    }

    /**
     * 接收邮件
     *
     * @param emailAddress 接收来自这个邮箱的邮件
     * @param authCode     该邮箱的授权码
     * @throws Exception
     */
    public void receiveMail(String emailAddress, String authCode) throws Exception {
        Store store = session.getStore("pop3");
        store.connect(receiveHost, emailAddress, authCode);
        Folder folder = store.getFolder("inbox");
        if (folder == null) {
            throw new Exception("inbox邮件夹不存在");
        }
        folder.open(Folder.READ_ONLY);
        System.out.println("你的邮箱里有: " + folder.getMessageCount() + " 封邮件");
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH:mm:ss");
        Message[] messages = folder.getMessages();
        for (int i = messages.length - 1; i >= 0; i--) {
            System.out.println("-------第" + (i + 1) + "封邮件--------");
            InternetAddress[] addr = (InternetAddress[]) messages[i].getFrom();
            System.out.println("其发送者是: " + addr[0].getAddress());
            System.out.print("发送的标题为:");
            String subject = messages[i].getSubject();
            System.out.println(subject);
            System.out.print("发送日期为:");
            System.out.println(dateFormat.format(messages[i].getSentDate()));
            System.out.println("邮件内容为: ");
            System.out.println(messages[i].getContent().toString());
        }
        folder.close(false);
        store.close();
    }

    public static void main(String[] args) throws Exception {
        JavaMailAPIClient mailClient = new JavaMailAPIClient();
        mailClient.init();
        String subject = "测试邮件";
        String content = "今天是个好天气\n一起 happy!";
        // 替换相关邮箱资料,发送邮件
        mailClient.sendMail("发送者@qq.com", "发送者邮箱授权码", "接收者@qq.com", subject, content);
        // 替换相关邮箱资料,查收邮件,注意收到新邮件需要一定时间
        mailClient.receiveMail("需要查收的邮箱@qq.com", "该邮箱授权码");
    }
}

注意事项

  • 确保在发送邮件前,已经正确配置了邮箱服务器的地址、端口、用户名和授权码。
  • 在调试过程中,可以开启 Debug 模式以查看详细的发送日志,帮助排查问题。
  • 收到新邮件可能需要一定时间,确保在测试接收邮件功能时给予足够的等待时间。

chapter09(网络扫描程序)

教学与实践目的:学会基本的网络扫描程序设计技术。

目标主机扫描是网络功防的基础和前提,扫描探测一台目标主机包括:确定该目标主机是否活动、目标主机的操作系统、正在使用哪些端口、对外提供了哪些服务、相关服务的软件版本等等,对这些内容的探测就是为了”对症下药”,为攻防提供参考信息。

对主机的探测工具非常多,比如大名鼎鼎的nmap、netcat、superscan, 以及国内的x-scanner等等。我们自己动手做简单扫描软件或工具,用于加深对网络编程的理解。

知识点

  • new Socket()
  • Process类
  • ICMP报文

主机扫描(远程主机探测)

通过指定的IP地址范围,发现该范围中活跃的主机,例如指定 192.168.0.15-192.168.1.100 范围。

新建Java包,命名chapter09,创建主机扫描程序HostScannerFX.java, 窗口界面如图9.1所示,并在”主机扫描”按钮中设置主机探测关键代码,例如类似代码:

InetAddress addr = InetAddress.getByName(host);//host 为 IP 地址 
boolean status=addr.isReachable(timeOut);// timeOut 为等待时间(毫秒为单位,例如可以设置为500,如果网络环境拥塞或网速不够,适当增加该值,以免错将活跃主机测试为无效主机)

image-20241110150018675

指定ip地址范围之间的遍历

更好的一种思路就是将ip地址转换为整数形式,一个网段就是0-255之间,刚好就是1个字节的范围,四个网段表示就需要四个字节。ip地址范围的遍历就转为在两个数字之间for循环遍历,循环体中将每一个数字转换回ip地址进行处理即可。所以关键就在于实现ip与数字之间的互相转换,基本原理阐述如下:

假设ip地址为 192.168.234.3: 每个网段都用二进制表示: 192(10) = 11000000 (2) ; 168(10) = 10101000 (2) ; 234(10) = 11101010 (2) ; 3(10) =00000011 (2) ;所以连在一起就是:11000000101010001110101000000011,对应的十进制数字就是3232295427。具体实现的算法分析:

image-20241110150402339

这些操作需要用到java的位运算操作,例如左移<<右移>>位与&位或|;将上述转换后的数字转换回ip地址,其实就是上述过程的逆过程(在具体运算中要用到和0xff与的操作,用于将高位置0)。

另外要注意的是,四个字节刚好是int类型,但Java不像C语言,没有无符号整数,最高位是符号位,当最左边的ip地址部分从128开始就变成了负数 (例如上面的192.168.234.3,192转为二进制是11000000,在java中,最高位1是为符号位使用,1就表示是负数了,所以使用Java的int类型,转换后的结果就不是3232295427,而是-1062671869),为了转换的数字能够大小连续变化,可以换成long类型来存储ip转换的数字,就不会出现负数的情况。

public long ipToLong(String ip) {
    String[] ipArray = ip.split("\\.");
    long num = 0;
    for (int i = 0; i < ipArray.length; i++) {

        long valueOfSection = Long.parseLong(ipArray[i]);
        num = (valueOfSection << 8 * (3 - i)) | num;
    }
    return num;
}

/**
* 长整型转ip
*/
public String longToIp(long i) {
    //右移,并将高位置0 18
    return ((i >> 24) & 0xFF) + "." +
        ((i >> 16) & 0xFF) + "." +
        ((i >> 8) & 0xFF) + "." +
        (i & 0xFF);
}

注意:主机扫描是个耗时操作,为了避免程序失去响应,”主机扫描”按钮中的代码应该放在一个新线程中执行

在Java中执行其它应用程序

可以使用包括PING、TRACERT等命令来实现ICMP相关扫描等功能,当然还有其他有关的命令行程序,可以在我们的程序中实现执行其他外部 程序的功能,这样就可以把一些功能集成到本程序。在Java中执行其它应用程序,要用到Process类和Runtime类,大家可以自行查找其相关用法,部分核心代码如下:

在Java中,确实可以使用 Process 类和 Runtime 类来执行外部程序,包括但不限于 pingtracert(在Windows中)或 traceroute(在Unix/Linux系统中)等命令。这些命令通常用于网络诊断,比如检测网络连接、路由路径等。

以下是一些基本的示例代码,展示如何在Java程序中执行这些外部命令:

使用 Runtime 类执行外部命令
try {
    // 在Windows上执行ping命令
    String os = System.getProperty("os.name").toLowerCase();
    if (os.contains("win")) {
        Runtime.getRuntime().exec("ping example.com");
    } else {
        // 在Unix/Linux上执行ping命令
        Runtime.getRuntime().exec("ping -c 4 example.com");
    }
} catch (Exception e) {
    e.printStackTrace();
}
使用 ProcessBuilder 类执行外部命令并获取输出

ProcessBuilder 类提供了更灵活的方式来执行外部命令,并且可以更容易地获取命令的输出。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ExternalCommandExample {
    public static void main(String[] args) {
        try {
            // 使用ProcessBuilder执行ping命令
            ProcessBuilder processBuilder = new ProcessBuilder("ping", "example.com");
            processBuilder.redirectErrorStream(true); // 合并标准输出和错误输出

            Process process = processBuilder.start();

            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }

            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("Command executed successfully");
            } else {
                System.out.println("Command execution failed");
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
注意事项
  1. 安全性:执行外部命令时,如果命令或参数来自不可信的源,可能会引发安全问题。确保对输入进行适当的验证和清理。

  2. 平台依赖性:不同的操作系统可能有不同的命令和参数。在上面的例子中,我们根据操作系统的不同选择了不同的命令参数。

  3. 错误处理:执行外部命令时,应该妥善处理可能发生的异常,比如 IOExceptionInterruptedException

  4. 输出处理:使用 ProcessBuilder 可以更容易地获取命令的输出,这对于诊断和日志记录非常有用。

同样,很多命令行程序执行时间较长,例如netstat命令,所以"命令执行"按钮中的代码也需要放在新线程中执行。

HostScannerFx.java

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;

public class HostScannerFx extends Application {

    private final TextArea showArea = new TextArea(); // 显示扫描结果的TextArea组件

    private final TextField startField = new TextField(); // 输入起始地址的TextField组件
    private final TextField endField = new TextField(); // 输入结束地址的TextField组件
    private final Button scanButton = new Button("主机扫描"); // 扫描按钮
    private final TextField inputcmd = new TextField(); // 输入命令的TextField组件
    private final Button execButton = new Button("执行命令"); // 执行命令按钮

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        // 设置窗口标题
        primaryStage.setTitle("Host Scanner");

        BorderPane mainPane = new BorderPane(); // 创建BorderPane作为主容器

        VBox vbox1 = new VBox(10);
        vbox1.setPadding(new Insets(10, 10, 10, 10));
        vbox1.getChildren().addAll(new Label("扫描结果:"), showArea);
        VBox.setVgrow(vbox1, Priority.ALWAYS);
        VBox.setVgrow(showArea, Priority.ALWAYS);

        HBox hbox1 = new HBox(10);
        hbox1.setPadding(new Insets(10, 10, 10, 10));
        hbox1.getChildren().addAll(new Label("起始地址:"), startField, new Label("结束地址:"), endField, scanButton);


        HBox hbox2 = new HBox(10);
        hbox2.setPadding(new Insets(10, 10, 10, 10));
        hbox2.getChildren().addAll(new Label("输入命令:"), inputcmd, execButton);
        HBox.setHgrow(hbox2, Priority.ALWAYS);
        HBox.setHgrow(inputcmd, Priority.ALWAYS);

        VBox mainVBox = new VBox(10);
        mainVBox.setPadding(new Insets(10, 10, 10, 10));
        mainVBox.getChildren().addAll(vbox1, hbox1, hbox2);
        mainPane.setCenter(mainVBox); // 设置主容器的中心为mainVBox

        showArea.setEditable(false); // 设置TextArea不可编辑
        showArea.setWrapText(true); // 设置TextArea自动换行

        Scene scene = new Scene(mainPane, 800, 600); // 创建Scene对象,设置大小
        primaryStage.setScene(scene);
        startField.setText("192.168.128.1"); // 设置默认起始地址
        endField.setText("192.168.128.254"); // 设置默认结束地址
        // 显示窗口
        primaryStage.show();


        scanButton.setOnAction(event -> {
            String start = startField.getText();
            String end = endField.getText();
            showArea.clear();

            Thread scanThread = new Thread(() -> {
                String startip = startField.getText();
                String endip = endField.getText();
                long startnum = ipToLong(startip);
                long endnum = ipToLong(endip);
                for (long i = startnum; i <= endnum; i++) {
                    String ip = longToIp(i);
                    try {
                        InetAddress address = InetAddress.getByName(ip);
                        boolean status = address.isReachable(5000);// timeOut 为等待时间
                        Platform.runLater(() -> showArea.appendText(ip + " : " + ((status) ? "Reachable" : "Unreachable") + "\n"));
                    } catch (Exception e) {
                        Platform.runLater(() -> showArea.appendText(ip + " : " + "Unknown host" + "\n"));
                    }
                }
            }, "Host Scanner");
            scanThread.start();
        });

        execButton.setOnAction(event -> {
            String cmd = inputcmd.getText();
            showArea.clear();
            Thread execThread = new Thread(() -> {
                try {
                    Process process = Runtime.getRuntime().exec(cmd);
//                    process.waitFor(); // 等待命令执行完成
                    InputStream inputStream = process.getInputStream(); // 获取输入流
                    BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "GBK"));
                    String msg = null;
                    while ((msg = br.readLine()) != null) {
                        String  msgTmp = msg;
                        Platform.runLater(() -> showArea.appendText(msgTmp + "\n"));
                    }
                } catch (Exception e) {
                    Platform.runLater(() -> showArea.appendText("命令执行失败"));
                }
            }, "Command Executor");
            execThread.start();
        });
    }

    public long ipToLong(String ip) {
        String[] ipArray = ip.split("\\.");
        long num = 0;
        for (int i = 0; i < ipArray.length; i++) {

            long valueOfSection = Long.parseLong(ipArray[i]);
            num = (valueOfSection << 8 * (3 - i)) | num;
        }
        return num;
    }

    /**
     * 长整型转ip
     */
    public String longToIp(long i) {
//右移,并将高位置0 18
        return ((i >> 24) & 0xFF) + "." +
                ((i >> 16) & 0xFF) + "." +
                ((i >> 8) & 0xFF) + "." +
                (i & 0xFF);
    }
}

端口扫描

扫描目的主机开放的端口是常见操作:攻(寻找目的主机开放的端口)与 防(检测本机异常开放的端口)。基本的端口扫描可使用的技术:

  • New Socket(host, port);
  • TCP Connect 端口扫描;

创建端口扫描程序PortScannerFX.java,参考主界面如图9.2所示:

image-20241110152324705

PortScannerFx.java

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.scene.control.Button;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class PortScanner extends Application {
    private final TextArea resultArea = new TextArea();
    private final TextField ipField = new TextField();
    private final TextField startPortField = new TextField();
    private final TextField endPortField = new TextField();

    private final Button scanButton = new Button("扫描");
    private final Button quickScanButton = new Button("快速扫描");
    // 多线程扫描
    private final Button multiScanButton = new Button("多线程扫描");
    private final Button quitButton = new Button("退出");
    private static AtomicInteger portCount = new AtomicInteger(0);//portCount用于统计已扫描的端口数

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("端口扫描程序");

        BorderPane mainPane = new BorderPane();

        VBox resultBox = new VBox(10);
        resultBox.setPadding(new Insets(10, 10, 10, 10));
        resultBox.getChildren().addAll(new Label("端口扫描结果:" + "\n"), resultArea);
        resultArea.setEditable(false);
        resultArea.setWrapText(true);
        VBox.setVgrow(resultArea, Priority.ALWAYS);
        VBox.setVgrow(resultBox, Priority.ALWAYS);

        HBox settingBox = new HBox(10);
        settingBox.setPadding(new Insets(10, 10, 10, 10));
        settingBox.getChildren().addAll(new Label("目标主机IP:"), ipField, new Label("起始端口:"), startPortField, new Label("结束端口:"), endPortField);


        HBox buttonBox = new HBox(20);
        buttonBox.setPadding(new Insets(10, 10, 10, 10));
        buttonBox.getChildren().addAll(scanButton, quickScanButton, multiScanButton, quitButton);
        buttonBox.setAlignment(Pos.CENTER);

        VBox mainBox = new VBox(10);
        mainBox.getChildren().addAll(resultBox, settingBox, buttonBox);

        mainPane.setCenter(mainBox);

        Scene scene = new Scene(mainPane, 700, 500); // 创建Scene对象,设置大小
        primaryStage.setScene(scene);

        ipField.setText("127.0.0.1");
        startPortField.setText("1");
        endPortField.setText("1000");
        primaryStage.show();

        this.scanButton.setOnAction(event -> {
            Thread scanThread = new Thread(() -> {
                String ip = ipField.getText();
                int startPort = Integer.parseInt(startPortField.getText());
                int endPort = Integer.parseInt(endPortField.getText());
                for (int port = startPort; port <= endPort; port++) {
                    try {
                        Socket socket = new Socket(ip, port);
                        socket.close();
                        int finalPort = port;
                        Platform.runLater(() -> resultArea.appendText("端口" + finalPort + ":开放\n"));
                    } catch (Exception e) {
                        // 端口关闭
                        int closedPort = port;
                        Platform.runLater(() -> resultArea.appendText("端口" + closedPort + ":关闭\n"));
                    }
                }
            }, "scanThread");
            scanThread.start();
        });
        this.quickScanButton.setOnAction(event -> {
            Thread quickScanThread = new Thread(() -> {
                String ip = ipField.getText();
                int startPort = Integer.parseInt(startPortField.getText());
                int endPort = Integer.parseInt(endPortField.getText());
                for (int port = startPort; port <= endPort; port++) {
                    try {
                        Socket socket = new Socket();
                        socket.connect(new InetSocketAddress(ip, port), 300);
                        socket.close();
                        int finalPort = port;
                        Platform.runLater(() -> resultArea.appendText("端口" + finalPort + ":开放\n"));
                    } catch (Exception e) {
                        // 端口关闭
                        int closedPort = port;
                        Platform.runLater(() -> resultArea.appendText("端口" + closedPort + ":关闭\n"));
                    }
                }
            }, "quickScanThread");
            quickScanThread.start();
        });
        this.quitButton.setOnAction(
                event -> {
                    Platform.exit();
                    System.exit(0);
                }
        );
        this.multiScanButton.setOnAction(event -> {
            int startPort = Integer.parseInt(startPortField.getText());
            int endPort = Integer.parseInt(endPortField.getText());
            ExecutorService executor = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                int start = startPort + i * (endPort - startPort + 1) / 10;
                int end = startPort + (i + 1) * (endPort - startPort + 1) / 10;
                executor.execute(new MultiThreadScannerHandler(ipField.getText(), i, 10, start, end));
            }
        });
    }

    class MultiThreadScannerHandler implements Runnable {
        private int totalThreadNum;//用于端口扫描的总共线程数,默认为10
        private int threadNo;//线程号,表示第几个线程
        private final String host; //扫描的主机ip
        private int startPort; //扫描的起始端口
        private int endPort; //扫描的结束端口

        public MultiThreadScannerHandler(String host, int threadNo, int startPort, int endPort) {
            this.totalThreadNum = 10;
            this.host = host;
            this.threadNo = threadNo;
            this.startPort = startPort;
            this.endPort = endPort;
        }

        public MultiThreadScannerHandler(String host, int threadNo, int totalThreadNum, int startPort, int endPort) {
            this.totalThreadNum = totalThreadNum;
            this.host = host;
            this.threadNo = threadNo;
            this.startPort = startPort;
            this.endPort = endPort;

        }

        @Override
        public void run() {
            // startPort和endPort为外部类的成员变量,表示需要扫描的起止端口
            for (int port = startPort + threadNo; port <= endPort; port += totalThreadNum) {
                try {
                    Socket socket = new Socket();
                    socket.connect(new InetSocketAddress(host, port), 1000);
                    socket.close();
                    String msg = host + "端口 " + port + " is open\n";
                    Platform.runLater(() -> {
                        resultArea.appendText(msg);
                    });
                } catch (IOException e) {
                    // 不输出关闭的端口信息
                    /*
                    String msg = host + "端口 " + port + " is closed\n";
                    Platform.runLater(() -> {
                        taDisplay.appendText(msg);
                    });
                    */
                }
                portCount.incrementAndGet(); // 扫描的端口数 +1
            }

                /* 如果全部扫描完成,端口扫描数量置0,并提示扫描结束
                   注意,如果使用下面这种方式,在多线程下不能保证操作的原子性
                   if (portCount.get() == (endPort - startPort + 1)) {
                       portCount.set(0);
                       ......
                   }
                */

            if (portCount.compareAndSet(endPort - startPort + 1, 0)) {
                Platform.runLater(() -> {
                    resultArea.appendText("----------------多线程扫描结束--------------\n");
                });
            }
        }
    }
}

拓展练习

优雅的停止多余线程

  1. 使用线程技术,使得按钮执行不会卡死主界面,但如果用户再次点击按钮执行新任务,前面的没有完成的线程任务输出就会和后面任务的输出内容混杂显示在一起,请改进这两个程序,使得每次点击按钮时,先关闭之前执行的扫描线程。注意,不要使用Thread.stop()方法,该方法不安全,已经被废弃。大家搜索有关ThreadGroup类、Thread.currentThread()、线程对象的 interrupt()、isInterrupted()等用法,实现以上需求;
ThreadGroup 类用法

ThreadGroup 是 Java 中用来表示线程组的类,可以对线程进行分组管理。每个线程都属于一个线程组,线程组可以包含线程和其他线程组,形成树形结构。您可以创建一个线程组,并将线程添加到该组中,以便于管理。 2

public class ThreadGroupTest {
    public static void main(String[] args) {
        ThreadGroup rootThreadGroup = new ThreadGroup("root线程组");
        Thread thread0 = new Thread(rootThreadGroup, new MRunnable(), "线程A");
        Thread thread1 = new Thread(rootThreadGroup, new MRunnable(), "线程B");
        thread0.start();
        thread1.start();
    }
}

class MRunnable implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("线程名: " + Thread.currentThread().getName() + ", 所在线程组: " + Thread.currentThread().getThreadGroup().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Thread.currentThread() 用法

Thread.currentThread() 方法用于获取当前执行的线程对象。这在多线程环境中非常有用,尤其是在需要获取当前线程状态或者名称时。

interrupt()isInterrupted() 方法用法

interrupt() 方法用于请求终止线程,而 isInterrupted() 方法用于检查线程是否被中断。这两个方法配合使用可以实现线程的优雅停止。

public class ThreadDemo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread!!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程 t 工作完毕");
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
        System.out.println("让 t 线程结束");
    }
}
安全停止线程的实践

在实际应用中,我们可以通过设置一个标志位来控制线程的运行,结合 interrupt() 方法来安全地停止线程。

public class Test {
    public static void main(String[] args) {
        SystemMonitor sm = new SystemMonitor();
        sm.start();
        try {
            Thread.sleep(10 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("监控任务启动 10 秒后,停止...");
        sm.stop();
    }
}

class SystemMonitor {
    private Thread t;
    private volatile boolean stop = false;

    void start() {
        t = new Thread(() -> {
            while (!stop) {
                System.out.println("正在监控系统...");
                try {
                    Thread.sleep(3 * 1000L);
                    System.out.println("任务执行 3 秒");
                    System.out.println("监控的系统正常!");
                } catch (InterruptedException e) {
                    System.out.println("任务执行被中断...");
                    Thread.currentThread().interrupt();
                }
            }
        });
        t.start();
    }

    void stop() {
        stop = true;
        t.interrupt();
    }
}

TextField组件的宽度设置

在JavaFX中,设置TextField的宽度可以通过几种不同的方法来实现:

  1. 使用CSS样式
    您可以为TextField设置CSS样式来指定宽度。例如:

    .text-field {
        -fx-min-width: 200px; /* 最小宽度 */
        -fx-max-width: 200px; /* 最大宽度 */
        -fx-width: 200px; /* 固定宽度 */
    }

    然后在Java代码中应用这个样式:

    TextField textField = new TextField();
    textField.getStyleClass().add("text-field");
  2. 使用setPrefColumnCount方法
    TextField有一个setPrefColumnCount方法,您可以设置期望的列数,JavaFX会根据列数和字体大小自动计算宽度。

    TextField textField = new TextField();
    textField.setPrefColumnCount(20); // 设置期望的列数
  3. 使用setPrefWidth方法
    您可以直接设置TextField的首选宽度:

    TextField textField = new TextField();
    textField.setPrefWidth(200); // 设置首选宽度为200像素
  4. 使用布局约束
    如果您使用的是JavaFX的布局管理器(如VBoxHBoxGridPane等),您可以在将TextField添加到布局时设置宽度约束。

    例如,在GridPane中:

    GridPane gridPane = new GridPane();
    TextField textField = new TextField();
    gridPane.add(textField, 0, 0);
    GridPane.setConstraints(textField, 0, 0, 1, 1); // 设置位置
    GridPane.setColumnSpan(textField, 2); // 设置列跨度
    GridPane.setHgrow(textField, Priority.ALWAYS); // 设置水平生长优先级

    HBoxVBox中,您可以使用HBox.setHgrowVBox.setVgrow方法来设置:

    HBox hbox = new HBox();
    TextField textField = new TextField();
    hbox.getChildren().add(textField);
    HBox.setHgrow(textField, Priority.ALWAYS); // 设置水平生长优先级
  5. 使用setMaxWidthsetMinWidth方法
    您还可以设置TextField的最大宽度和最小宽度:

    TextField textField = new TextField();
    textField.setMaxWidth(Double.MAX_VALUE); // 设置最大宽度
    textField.setMinWidth(100); // 设置最小宽度

HostAndPortScanner.java

package chapter09;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.text.DecimalFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class HostAndPortScanner extends Application {

  private final TextArea showArea = new TextArea();
  private final TextField startField = new TextField();
  private final TextField endField = new TextField();
  private final Button scanButton = new Button("扫描");
  private final TextField startPort = new TextField();
  private final TextField endPort = new TextField();
  private final TextField inputcmd = new TextField();
  private final Button execButton = new Button("执行命令");
  private final Button stopButton = new Button("停止");
  private Label lblProgress = new Label("");
  private ProgressBar bar = new ProgressBar();

  private AtomicInteger hostCount = new AtomicInteger(0);
  private ExecutorService executorService;
  private volatile boolean isRunning = false;

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

  @Override
  public void start(Stage primaryStage) throws Exception {
    primaryStage.setTitle("Better Scanner");
    BorderPane mainPane = new BorderPane();

    VBox vbox1 = new VBox(10);
    vbox1.setPadding(new Insets(10, 10, 10, 10));
    vbox1.getChildren().addAll(new Label("扫描结果:"), showArea);
    VBox.setVgrow(vbox1, Priority.ALWAYS);
    VBox.setVgrow(showArea, Priority.ALWAYS);

    //进度条的界面布局
    HBox hBoxBar = new HBox();
    hBoxBar.setSpacing(10);
    hBoxBar.setPadding(new Insets(10, 0, 10, 0));
    hBoxBar.setAlignment(Pos.CENTER);
    //lblProgress 和 bar 分别为Label和ProgressBar 类型的成员变量
    hBoxBar.getChildren().addAll(lblProgress, bar);
//以下下两条语句使得进度条水平扩展
    bar.setMaxWidth(Double.MAX_VALUE);
    HBox.setHgrow(bar, Priority.ALWAYS);

    HBox hbox1 = new HBox(10);
    hbox1.setPadding(new Insets(10, 10, 10, 10));
    startField.setPrefColumnCount(8);
    endField.setPrefColumnCount(8);
    startPort.setPrefColumnCount(5);
    endPort.setPrefColumnCount(5);
    hbox1.getChildren().addAll(new Label("起始地址:"), startField, new Label("结束地址:"), endField, new Label("起始port:"), startPort, new Label("结束port"), endPort, scanButton);

    HBox hbox2 = new HBox(10);
    hbox2.setPadding(new Insets(10, 10, 10, 10));
    hbox2.getChildren().addAll(new Label("输入命令:"), inputcmd, execButton);
    HBox.setHgrow(hbox2, Priority.ALWAYS);
    HBox.setHgrow(inputcmd, Priority.ALWAYS);

    VBox mainVBox = new VBox(10);
    mainVBox.setPadding(new Insets(10, 10, 10, 10));
    mainVBox.getChildren().addAll(vbox1, hbox1, hbox2,hBoxBar, stopButton);
    mainPane.setCenter(mainVBox);

    showArea.setEditable(false);
    showArea.setWrapText(true);

    Scene scene = new Scene(mainPane, 900, 550);
    primaryStage.setScene(scene);
    startField.setText("192.168.128.1");
    endField.setText("192.168.128.254");
    primaryStage.show();


    scanButton.setOnAction(event -> {
      if (isRunning) {
        stopButton.fire();
      }
      isRunning = true;
      showArea.clear();
      executorService = Executors.newFixedThreadPool(10);
      long starthost = ipToLong(startField.getText());
      long endhost = ipToLong(endField.getText());
      for (int i = 0; i < 10; i++) {
        long startnum = starthost + (long) Math.ceil(i * 1.0 * (endhost - starthost + 1) / 10);
        long endnum = (i == 9) ? endhost : starthost + (long) Math.ceil((i + 1) * 1.0 * (endhost - starthost + 1) / 10) - 1;
        executorService.execute(new MultiThreadHostScanner(startnum, endnum));
      }
    });

    stopButton.setOnAction(event -> {
      isRunning = false;
      if (executorService != null) {
        executorService.shutdownNow();
      }
    });

    execButton.setOnAction(event -> {
      if (isRunning) {
        stopButton.fire();
      }
      isRunning = true;
      showArea.clear();
      Thread execThread = new Thread(() -> {
        try {
          Process process = Runtime.getRuntime().exec(inputcmd.getText());
          InputStream inputStream = process.getInputStream();
          BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "GBK"));
          String msg;
          while ((msg = br.readLine()) != null) {
            String finalMsg = msg;
            Platform.runLater(() -> showArea.appendText(finalMsg + "\n"));
          }
        } catch (Exception e) {
          Platform.runLater(() -> showArea.appendText("命令执行失败\n"));
        }
      }, "Command Executor");
      execThread.start();
    });
  }


  public long ipToLong(String ip) {
    String[] ipArray = ip.split("\\.");
    long num = 0;
    for (int i = 0; i < ipArray.length; i++) {
      long valueOfSection = Long.parseLong(ipArray[i]);
      num = (valueOfSection << 8 * (3 - i)) | num;
    }
    return num;
  }

  public String longToIp(long i) {
    return ((i >> 24) & 0xFF) + "." +
      ((i >> 16) & 0xFF) + "." +
      ((i >> 8) & 0xFF) + "." +
      (i & 0xFF);
  }

  class MultiThreadHostScanner implements Runnable {
    private long startHost;
    private long endHost;

    public MultiThreadHostScanner(long startHost, long endHost) {
      this.startHost = startHost;
      this.endHost = endHost;
    }
    private void progressChange() {
      hostCount.incrementAndGet(); // 扫描的端口数+1
      double progress = 1.0 * hostCount.get() / (endHost - startHost + 1);
      Platform.runLater(() -> {
        bar.setProgress(progress);
        DecimalFormat df = new DecimalFormat("0%");
        lblProgress.setText(df.format(progress));
      });
    }

    @Override
    public void run() {
      if (!isRunning) return;
      StringBuilder output = new StringBuilder();
      StringBuilder portOutput = new StringBuilder();
      for (long host = startHost; host <= endHost; host++) {
        if (!isRunning) return;
        try {
          InetAddress address = InetAddress.getByName(longToIp(host));
          boolean status = address.isReachable(1000);
          if (status) {
            int startport = Integer.parseInt(startPort.getText());
            int endport = Integer.parseInt(endPort.getText());
            ExecutorService portExecutorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
              int start = startport + i * (endport - startport + 1) / 10;
              int end = startport + (i + 1) * (endport - startport + 1) / 10;
              long finalHost = host;
              portExecutorService.execute(() -> {
                if (!isRunning) return;
                for (int port = start; port < end; port++) {
                  Socket socket = new Socket();
                  try {
                    socket.connect(new InetSocketAddress(longToIp(finalHost), port), 1000);
                    socket.close();
                    synchronized (portOutput) {
                      portOutput.append(longToIp(finalHost)).append(":").append(port).append(" is open\n");
                    }
                  } catch (IOException e) {
                    synchronized (portOutput) {
                      portOutput.append(longToIp(finalHost)).append(":").append(port).append(" is closed\n");
                    }
                  }
                }
              });
            }
            portExecutorService.shutdown();
            while (!portExecutorService.isTerminated()) {
              if (!isRunning) {
                portExecutorService.shutdownNow();
                break;
              }
            }
          }
          String statusMessage = status ? "Reachable" : "Unreachable";
          output.append(longToIp(host)).append(" : ").append(statusMessage).append("\n");
        } catch (Exception e) {
          output.append(longToIp(host)).append(" : Exception occurred\n");
        }
      }
      Platform.runLater(() -> {
        synchronized (showArea) {
          showArea.appendText(output.toString());
          showArea.appendText(portOutput.toString());
        }
      });
      progressChange();
    }
  }
}

chapter10(网络发包与抓包)

基于Java_使用Jpcap进行网络抓包并分析(6千字保姆级教程)-CSDN博客

教学与实践目的:此实验基于Windows系统,学习网络抓包及发送特定网络包的程序设计技术。

通常情况下网卡(工作在链路层)在接收数据时往网络层传递3类包:广播包、与自己IP地址一致的单播包、已加入某组的组播包,在这种情况下,高层应用只能收到上述3类数据包。

我们前几讲的程序设计教学中,包括基于TCP Socket的网络应用(网络对话、FTP、Email及HTTP),以及基于UDP Socket的网络通信应用中,网卡只往网络层传递与自己IP地址一致的单播包。

抓包的思想是:流经网卡所有的有效包都要抓,所以抓包程序都是工作在链路层或网络层(不考虑应用层的端口参数问题)。网络抓包软件或工具,也叫网络嗅探器,目前网上有许多免费的嗅探软件。java 语言虽然在TCP/UDP传输方面给予了良好的定义,但无法直接访问底层网络数据,对于网络层以下的控制无能为力。Jpcap扩展包弥补了这一 点,Jpcap 是一个可以让 java 工作在链路层的类库,Jpcap实际上并非真正实现了对数据链路层的控制,而是一个中间件,Jpcap通过调用WinPcap(Npcap),给java语言提供一个公共的接口,从而实现了平台无关性。这两个教学单元掌握基于Jpcap类库包(第三方软件包)自己动手开发抓包发包工具软件。

1. JPacp简介

  1. Jpcap 的工作环境需求

    (1)安装JDK;

    (2)安装WinPcap

    Windows 7平台,需要安装WinPcap4.1.3(点击下载)

    Windows 10、11平台:WinPcap4.1.3对Windows 10的系统支持不好, 需要管理员模式下运行net start npf解决兼容问题。所以在windows 10 平台下推荐用Npcap(点击下载)代替(安装的时候勾选 “Install Npcap in WinPcap API-compatiable Mode”,其它选项保持默认);

  2. Jpcap 的类库结构

    Jpcap0.7版本共有15个类(接口),如图10.1所示,分别简介如下:

    image-20241116131341190

JpcapCaptor 类:继承了java.lang.Object,它是 Jpcap 的核心类,一个 JpcapCaptor 对象实例可以看作是个网络数据包捕获器,通过对JpcapCaptor对象的调用,实现网络数据包的抓取;它提供了一系列静态方法调用如:获取网卡列表、获取某个网卡上的JpcapCaptor对象;

Packet 类:是所有被捕获的数据包的基类,继承了java.lang.Object;

ARPPacket 类:描述了ARP/RARP包,继承了Packet类;

IPPacket 类:描述了IP包,继承了Packet类,支持IPv4和IPv6;

TCPPacket 类:描述TCP包,继承了IPPacket类;

UDPPacket 类:描述了UDP包,继承了IPPacket类;

ICMPPacket 类:描述了ICMP包,继承了IPPacket类;

DatalinkPacket 类:抽象类,描述了数据链路层的包,它继承了 java.lang.Object;

EthernetPacket 类:描述了以太帧包,继承DatalinkPacket 类;

IPAddress 类:继承了java.lang.Object,描述了 IPv4 和 IPv6 地址,包含了将IP地址转换为域名的方法;

IPv6Option 类:继承了java.lang.Object,描述了 IPv6 选项报头;

JpcapSender 类 :用来发送可以用来发送IPPacket及其子类,继承了 java.lang.Object;

JpcapWriter 类 :用来将一个被捕获的数据包保存到文件,继承了 java.lang.Object;

PacketReceiver 接口:数据包处理器接口定义,要处理收到的数据包,必须编写这个接口的实现类,在JpcapCaptor对象的loopPacket或processPacket 方法中调用。

关于Jpcap的API详细索引,可下载Jpcap API Doc参考文档(下载解压后,打开index.html)

2. Jpcap的安装

登录OC课堂资源网站,从”助软件资源”中下载jpcap32_64.zip,压缩包中有32位和64位版本的两个文件夹,分别对应jdk32位和jdk64位使用 (每个文件夹下都有Jpcap.dll和Jpcap.jar文件。Jpcap.dll要和jdk类型对应, 32位对应32位,64位对应64位,不能混用)。

针对Windows 环境:

  1. 复制”Jpcap.dll”到jdk安装目录下的bin目录中,即[jdk目录]/bin,对于安装了多个jdk版本的系统,复制到你项目使用的那个jdk目录 下的bin中(如果嫌麻烦,可以使用本课程提供的idea2018绿色版,并使用绿色版中内置的jdk,其中已经内置了对应版本的Jpcap.dll);
  2. 将”jpcap.jar”导入到自己项目中,idea添加外部jar的方法参见第7讲的扩展练习二;

注意F&Q: 上述工作完成后,如果后续编写的程序运行不正常,查找是否出现下列问题:

  1. 若错误提示是”no dependence libray”, WinPcap(Npcap)没有安装或未装好, 重装;
  2. 如果出现”no Jpcap in …java library.path”或”no ipcap.dll in jdk/bin……”等原因是没有拷贝Jpcap.dll到正确的位置,或者32位和64位版本没对应,或者jpcap.jar没有导入到项目中;
  3. 若出现”不是有效的Win32应用程序”,可能是WinPcap(Npcap)的版本问题,应下载一个64/32位兼容的版本。
  4. 程序能运行,但检测不到可用的网卡,可能是运行在windows 10系统中,错用了WinPcap,应该选择Npcap代替(或者管理员模式下的cmd 中,运行net start npf

3. Jpcap基本使用

1. 获取网卡列表及相关信息

要进行抓包,第一步工作就是要获取机器中可用的网卡列表,JpcapCaptor.getDeviceList()方法可返回NetworkInterface类型的数组。NetworkInterface对象包含了对应网卡的信息,对我们有用的主要是网卡的GUID值、网卡的描述信息、IP、MAC等。

import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;
import jpcap.NetworkInterfaceAddress;
import jpcap.PacketReceiver;
import jpcap.packet.Packet;
import jpcap.packet.TCPPacket;

NetworkInterface[] devices = JpcapCaptor.getDeviceList();
for (int i = 0; i < devices.length; i++) {
    // 打印 GUID information 和 description
    System.out.println(i + ": " + devices[i].name + " " + devices[i].description);
    
    // 打印 MAC address,各段用":"隔开
    String mac = "";
    for (byte b : devices[i].mac_address) {
        // mac 地址6段,每段是8位,而int转换的十六进制是4个字节,所以和0xff 相与,这样就只保留低8位
        mac = mac + Integer.toHexString(b & 0xff) + ":";
    }
    System.out.println("MAC address:" + mac.substring(0, mac.length() - 1));
    
    // print out its IP address, subnet mask 和 broadcast address
    for (NetworkInterfaceAddress addr : devices[i].addresses) {
        System.out.println(" address:" + addr.address + " " + addr.subnet + " " + addr.broadcast);
    }
}

2. 打开网卡接口

获得网卡接口列表后,打开一个可用的网卡返回JpcapCaptor对象,用于捕获数据。假设以上测试代码中,devices[0]获取的是真实可用的物理网卡:

JpcapCaptor jpcapCaptor = JpcapCaptor.openDevice(devices[0], 1514, true, 20);

openDevice方法有四个参数:

  • 第一个参数表示NetworkInterface对象;
  • 第二个参数表示捕获的数据字节数,一般最小不小于68,避免连首部数据都抓不全,一般可设置为1514(以太网帧MTU=1500)就够用了,特殊情况还可设置更大的值(巨型帧的情况);
  • 第三个参数表示是否开启网卡的混杂模式;
  • 第四个参数是毫秒为单位的超时设置。

3. 数据抓包

获得JpcapCaptor的对象实例jpcapCaptor后,就可以用来进行抓包操作。

(1)单步抓包方式:
Packet packet = jpcapCaptor.getPacket();
System.out.println(packet);
jpcapCaptor.close();

每执行一次,捕获一个数据包。

(2)自动回调方式:

首先需要实现PacketReceiver接口:

class PacketHandler implements PacketReceiver {
    @Override
    public void receivePacket(Packet packet) {
        System.out.println(packet); // 输出抓取的包的原始信息
        // 其它相关处理代码
    }
}

然后将PacketHandler的对象实例作为参数传递给jpcapCaptor.loopPacket(int n, PacketReceiver packetHandler)jpcapCaptor.processPacket(int n, PacketReceiver packetHandler),这样就会在有数据包被捕获的时候,自动执行receivePacket()方法中的代码。

以上两个方法中的参数int n,表示需要捕获的数据包的个数,-1表示无限制地捕获。注意,打开网络接口的方法openDevice中的超时设置,只对processPacket方法有效。loopPacketprocessPacket方法很相似,区别主要是:前者是阻塞方法,没有包被捕获就一直阻塞;而后者是非阻塞方法,支持超时设置。

(3)数据包信息的显示

捕获的数据包为Packet或其子类型(TCPPacketUDPPacket等),直接System.out.println(packet);就可以输出抓包的原始信息,显示结果类似:

1603870741:133888 /192.168.1.106->/14.215.177.38 protocol(6) priority(0) hop(128) offset(0) ident(9507) TCP 2913 > 443 seq(1890118981) win(1024) ack 3804762716 S

以上信息其实就是IP数据报及TCP报头的一些相关信息,例如S表示SYN=1的标识(表示这是一个连接请求或连接接受报文),seq(1890118981)中的数字表示序号,ack 3804762716中的数字表示确认号。

如果要显示抓包内的数据部分,可以通过packet.data返回字节数组内容,例如这样构造一个字符串来显示:

new String(packet.data, 0, packet.data.length, "utf-8");

(当然,数据部分可能根本不是明文字符串,例如是http返回的gzip压缩格式,显示出来的会是一堆不可读的乱字符)

(4)数据包过滤

捕获结果内容过多,如何只显示感兴趣的内容?可以使用jpacpCaptor.setFilter()方法,例如,只捕获网易站点的TCP/IPv4数据包:

jpcapCaptor.setFilter("ip and tcp and host www.163.com", true);

(更多的过滤规则可参考Designing Capture Filters for Ethereal / Wireshark

(5)关闭捕获器,释放资源
if (jpcapCaptor != null)
    jpcapCaptor.close();

4. ConfigDialog.java

package chapter10;

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;

public class ConfigDialog {
  private JpcapCaptor jpcapCaptor; // 用于返回给主窗体
  //网卡列表
  private NetworkInterface[] devices = JpcapCaptor.getDeviceList();
  private Stage stage = new Stage();//对话框窗体

  //parentStage表示抓包主程序(PacketCaptureFX)的stage,传值可通过这种构造方法参数的方式
  public ConfigDialog(Stage parentStage) {
    //设置该对话框的父窗体为调用者的那个窗体
    stage.initOwner(parentStage);
    // 设置为模态窗口,不关闭则不能切换焦点
    stage.initModality(Modality.WINDOW_MODAL);
    stage.setResizable(false);  // 禁止尺寸变化
    stage.setTitle("选择网卡并设置参数");

    // 窗体主vbox
    VBox mainvbox = new VBox(10);
    mainvbox.setPadding(new Insets(10, 30, 10, 30));

    // 下拉条
    ComboBox<String> comboBox = new ComboBox<>();
    comboBox.setMaxWidth(800);
    for (int i = 0; i < devices.length; i++) {
      comboBox.getItems().add(i + " : " + devices[i].description);
    }
    //默认选择第一项
    comboBox.getSelectionModel().selectFirst();

    //设置抓包过滤
    TextField tfFilter = new TextField();

    //设置抓包大小(一般建议在68-1514之间,默认1514)
    TextField tfSize = new TextField("1514");

    //是否设置混杂模式
    CheckBox cb = new CheckBox("是否设置为混杂模式");
    cb.setSelected(true); //默认选中

    //底部确定和取消按钮
    HBox hBoxBottom = new HBox(10);
    hBoxBottom.setPadding(new Insets(10, 30, 10, 30));
    hBoxBottom.setAlignment(Pos.CENTER_RIGHT);

    Button btnConfirm = new Button("确定");
    Button btnCancel = new Button("取消");
    hBoxBottom.getChildren().addAll(btnConfirm, btnCancel);

    //将各组件添加到主容器
    mainvbox.getChildren().addAll(new Label("请选择网卡:"), comboBox,
      new Label("设置抓包过滤器(例如 ip and tcp):"), tfFilter,
      new Label("设置抓包大小(建议介于68~1514之间):"), tfSize, cb,
      new Separator(), hBoxBottom);

    Scene scene = new Scene(mainvbox);
    stage.setScene(scene);
//    stage.show();//不要显示对话框,由主窗体调用显示

    //**************事件响应部分***************************

    //确定按钮
    btnConfirm.setOnAction(event -> {
      try {
        int index = comboBox.getSelectionModel().getSelectedIndex();
        // 选择网卡的接口
        NetworkInterface networkInterface = devices[index];
        // 抓包大小
        int snapLen = Integer.parseInt(tfSize.getText().trim());
        // 是否混杂模式
        boolean promisc = cb.isSelected();
        jpcapCaptor = JpcapCaptor.openDevice(networkInterface, snapLen, promisc, 20);
        jpcapCaptor.setFilter(tfFilter.getText().trim(), true);
        stage.hide();
      } catch (Exception e) {
        e.printStackTrace();
        new Alert(Alert.AlertType.ERROR, e.getMessage()).showAndWait();
      }
    });

    //取消按钮
    btnCancel.setOnAction(event->{
      stage.hide();
    });
  }

  public JpcapCaptor getJpcapCaptor(){
    return jpcapCaptor;
  }

  public void showAndWait(){
    stage.showAndWait();
  }

  public void show() {
    stage.show();
  }
}

5. PacketCapture.java

package chapter10;

import javafx.application.Application;
import javafx.application.Platform;
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.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import jpcap.JpcapCaptor;
import jpcap.PacketReceiver;
import jpcap.packet.Packet;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

public class PacketCaptureFX extends Application {
  private TextArea infoArea = new TextArea();
  private Button startBtn = new Button("开始抓包");
  private Button stopBtn = new Button("停止抓包");
  private Button clearBtn = new Button("清除内容");
  private Button settingBtn = new Button("设置参数");
  private Button quitBtn = new Button("退出");
  private ConfigDialog configDialog;
  private JpcapCaptor jpcapCaptor;
  private Thread packetCaptureThread;

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

  @Override
  public void start(Stage primaryStage) {
    configDialog = new ConfigDialog(primaryStage); // 设置父窗口

    VBox showvbox = new VBox(10);
    showvbox.setPadding(new Insets(10, 20, 10, 20));
    showvbox.getChildren().addAll(new Label("抓包信息:"), infoArea);
    VBox.setVgrow(infoArea, Priority.ALWAYS);
    VBox.setVgrow(showvbox, Priority.ALWAYS);
    infoArea.setEditable(false);
    infoArea.setWrapText(true);

    HBox buttonsBox = new HBox(10);
    buttonsBox.setPadding(new Insets(10, 10, 10, 10));
    buttonsBox.setAlignment(Pos.CENTER);
    buttonsBox.getChildren().addAll(startBtn, stopBtn, clearBtn, settingBtn, quitBtn);

    VBox mainBox = new VBox(10);
    mainBox.getChildren().addAll(showvbox, buttonsBox);

    Scene scene = new Scene(mainBox, 700, 500);
    primaryStage.setScene(scene);
    primaryStage.show();//不要显示对话框,由主窗体调用显示

    clearBtn.setOnAction(event -> {
      infoArea.clear();
    });

    quitBtn.setOnAction(event -> {
      this.packetCaptureThread.interrupt();
      System.exit(0);
    });

    settingBtn.setOnAction(event -> {
      if (configDialog == null) {
        configDialog = new ConfigDialog(primaryStage);
      }
      configDialog.showAndWait();
      //获取设置后的JpcapCaptor对象实例
      jpcapCaptor = configDialog.getJpcapCaptor();
    });

    // 开始抓包 按钮动作事件
    startBtn.setOnAction(event -> {
      //还没有jpcapCaptor对象实例,则打开设置对话框
      if (jpcapCaptor == null) {
        settingBtn.fire();
        return;
      }
      //停止还没结束的抓包线程
      if (packetCaptureThread != null) {
        interrupt("captureThread");
      }
      //开线程名为"captureThread"的新线程进行抓包  
      packetCaptureThread =
        new Thread(() -> {
          while (true) {
            //如果声明了本线程被中断,则退出循环
            if (Thread.currentThread().isInterrupted())
              break;
            //每次抓一个包,交给内部类PacketHandler的实例处理
            // PacketHandler为接口PacketReceiver的实现类
            jpcapCaptor.processPacket(1, new PacketHandler());
          }
        }, "captureThread");
      //将当前的主线程优先级提高
      Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
      //降低抓包线程的优先级,避免抓包线程卡住资源
      packetCaptureThread.setPriority(Thread.MIN_PRIORITY);
      packetCaptureThread.start();
    });

    stopBtn.setOnAction(event -> {
      interrupt("captureThread");
    });
  }


  /**
   * 循环遍历指定线程名的线程列表,并声明关闭,需要关闭的线程需要在构造时指定线
   * 程名,作为参数传入
   */
  private void interrupt(String threadName) {
    ThreadGroup currentGroup =
      Thread.currentThread().getThreadGroup();
    //获取当前线程的线程组及其子线程组中活动线程数量
    int noThreads = currentGroup.activeCount();
    Thread[] lstThreads = new Thread[noThreads];
    currentGroup.enumerate(lstThreads);//将活动线程复制到线程数组
    //遍历这些活动线程,符合指定线程名的则声明关闭
    for (int i = 0; i < noThreads; i++) {
      if (lstThreads[i].getName().equals(threadName)) {
        lstThreads[i].interrupt();//声明线程关闭
      }
    }
  }

  private class PacketHandler implements PacketReceiver {
    @Override
    public void receivePacket(Packet packet) {
      Platform.runLater(() -> {
        // 在显示区显示抓包原始信息
        String msg = new String(packet.data, 0, packet.data.length, StandardCharsets.UTF_8);
        infoArea.appendText(msg);
      });
    }
  }
}

6. 拓展练习

超链接组件HyperLink的使用

  • 创建超链接
Hyperlink hyperlink = new Hyperlink("https://oc.gdufs.edu.cn/filter");
  • 使用setOnAction方法为超链接添加一个事件处理器,当用户点击超链接时触发
hyperlink.setOnAction(event -> {
    try {
        Desktop.getDesktop().browse(new URI("https://www.openai.com"));
    } catch (IOException | URISyntaxException e) {
        e.printStackTrace();
    }
});
  • Desktop.getDesktop().browse(new URI("https://www.openai.com"));

使用Desktop.getDesktop().browse方法来打开默认浏览器访问这个链接,需要导入java.awt库和java.net库

Java操作注册表

在选择网卡界面中,可能不只一个网卡,例如有线网卡、无线网卡,如果有蓝牙设备、安装了Vmware、VirtualBox等配置的虚拟网卡,那么网卡列表就更长。一般大家都知道自己使用的是哪个网卡,但部分电脑从下拉表中却不容易找出自己使用的那块网卡,例如这台笔记本的网卡列表(图10.4):

image-20241120141149359

这个和网卡生产商有关,有些没有这个问题,有些却显示一些同名的名称。在windows系统中,Jpcap的 NetworkInerface的 description方法得到的是注册表中设备的 ProviderName 信息,而更准确的信息是注册表中的 DriverDesc 信息,有些设备这两个信息一致,所以显示正常,但不少设备的 ProviderName 都是 Microsoft,就无从区分网卡设备,如图10.5的注册表截图:

image-20241120141509893

为了解决windows系统下的这个问题,可以从注册表入手,NetworkInterface类的name方法中,包含一个guid值,就是图10.5中的NetCfgInstanceId 的值,可以通过搜索注册表特定位置下子目录中的该值,匹配成功后再获取对应的 DriverDesc 值,就可以得到真实的设备信息

例如程序优化后网卡下拉列表 就是如图10.6所示(如果DriverDesc和 ProvideName不同,则将 ProviderName 放在后面小括号中),可以发现列表中第2个设备才是可用的网卡。

image-20241120141626175

解决方案概述:
  1. 获取网络接口列表
    使用JpcapCaptor.getDeviceList()方法获取可用的网络接口数组,并遍历这些接口以提取GUID值。

  2. 访问Windows注册表
    使用一个可以读取注册表的Java库(如WinRegistry),从特定位置开始搜索NetCfgInstanceId的值,并匹配之前提取的GUID。

    注册表实际就像有五个根节点的树,关键就是要知道在注册表哪个位置开始搜索NetCfgInstanceId键对应的值,毕竟注册表太大,从最顶层根节点开始查找太慢。建议从这个位置开始搜索:HKEY_LOCAL_MACHINE \SYSTEM\CurrentControlSet\Control\Class

  3. 获取DriverDesc值
    一旦找到了对应的NetCfgInstanceId,在同一目录下获取DriverDesc的值。

示例代码:

下面是实现这一过程的一个简化示例:

import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// 假设你已经有一个可用的WinRegistry类,可以读取注册表信息
import somepackage.WinRegistry;

public class NetworkInfoRetriever {

    public void retrieveNetworkInfo() {
        try {
            // (1) 获取网络接口
            NetworkInterface[] devices = JpcapCaptor.getDeviceList();
            for (NetworkInterface device : devices) {
                String deviceName = device.name;
                String guid = extractGUID(deviceName);
                
                // (2) 从注册表搜索NetCfgInstanceId
                String netCfgInstanceId = searchRegistryForNetCfgId(guid);

                if (netCfgInstanceId != null) {
                    // (3) 获取DriverDesc值
                    String driverDesc = retrieveDriverDesc(netCfgInstanceId);
                    System.out.println("Driver Description: " + driverDesc);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private String extractGUID(String deviceName) {
        // 使用正则表达式提取GUID
        Pattern pattern = Pattern.compile("\\{(.*?)\\}");
        Matcher matcher = pattern.matcher(deviceName);
        return matcher.find() ? matcher.group(0) : null;
    }

    private String searchRegistryForNetCfgId(String guid) {
        // 示例代码,使用WinRegistry类读取注册表中的值
        String registryPath = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Class";
        String netCfgInstanceId = WinRegistry.readString(registryPath, "NetCfgInstanceId");

        // 这里可以根据需要进行更进一步的比较和匹配
        return netCfgInstanceId != null && netCfgInstanceId.contains(guid) ? netCfgInstanceId : null;
    }

    private String retrieveDriverDesc(String netCfgInstanceId) {
        String registryPath = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Class\\" + netCfgInstanceId;
        return WinRegistry.readString(registryPath, "DriverDesc");
    }

    public static void main(String[] args) {
        NetworkInfoRetriever retriever = new NetworkInfoRetriever();
        retriever.retrieveNetworkInfo();
    }
}
注意事项:
  1. 依赖库
    • 请确保你已正确添加jpcap库的依赖。
    • 查找并下载适合的WinRegistry类库以读取Windows注册表。
  2. 权限问题
    • 运行此程序时可能需要管理员权限以访问注册表中的某些部分。
    • 注意,对注册表进行操作,可能会出现如下信息:warning:Could not open/create prefs root node Software\JavaSoft\Prefs at root 0x80000002. Windows RegCreateKeyEx(…) returned error code 5
  3. 异常处理
    • 代码中应包含更详尽的异常处理,以处理可能出现的各种情况,如权限不足、接口不可用等。
  4. 正则表达式
    • extractGUID()方法使用正则表达式提取GUID,请根据具体的设备名称格式进行调整。

TCP三次握手示例图

image-20241120142328776

优化后的ConfigDialog.java

package chapter10.PacketCapture;

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.TextField;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;

import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;

public class ConfigDialog {
  private JpcapCaptor jpcapCaptor; // 用于返回给主窗体
  //网卡列表
  private NetworkInterface[] devices = JpcapCaptor.getDeviceList();
  private Stage stage = new Stage();//对话框窗体
  private Hyperlink hyperlink = new Hyperlink("https://oc.gdufs.edu.cn/filter");

  //parentStage表示抓包主程序(PacketCaptureFX)的stage,传值可通过这种构造方法参数的方式
  public ConfigDialog(Stage parentStage) throws InvocationTargetException, IllegalAccessException {
    //设置该对话框的父窗体为调用者的那个窗体
    stage.initOwner(parentStage);
    // 设置为模态窗口,不关闭则不能切换焦点
    stage.initModality(Modality.WINDOW_MODAL);
    stage.setResizable(false);  // 禁止尺寸变化
    stage.setTitle("选择网卡并设置参数");

    ArrayList<String> detailDevices = this.getDetailDeviceName();

  // 窗体主vbox
    VBox mainvbox = new VBox(10);
    mainvbox.setPadding(new Insets(10, 30, 10, 30));

  // 下拉条
    ComboBox<String> comboBox = new ComboBox<>();
    comboBox.setMaxWidth(800);
  // 假设 devices 是一个 NetworkInterface 数组或者其他类似的设备数组
    for (int i = 0; i < devices.length; i++) {
      // 确保 detailDevices 和 devices 列表是同步的
      if (i < detailDevices.size()) {
        comboBox.getItems().add(i + " : " + "(" + detailDevices.get(i) + ")" + " - " + devices[i].description);
      } else {
        comboBox.getItems().add(i + " : " + devices[i].description);
      }
    }
    //默认选择第一项
    comboBox.getSelectionModel().selectFirst();

    //设置抓包过滤
    TextField tfFilter = new TextField();

    //设置抓包大小(一般建议在68-1514之间,默认1514)
    TextField tfSize = new TextField("1514");

    //是否设置混杂模式
    CheckBox cb = new CheckBox("是否设置为混杂模式");
    cb.setSelected(true); //默认选中

    //底部确定和取消按钮
    HBox hBoxBottom = new HBox(10);
    hBoxBottom.setPadding(new Insets(10, 30, 10, 30));
    hBoxBottom.setAlignment(Pos.CENTER_RIGHT);

    Button btnConfirm = new Button("确定");
    Button btnCancel = new Button("取消");
    hBoxBottom.getChildren().addAll(btnConfirm, btnCancel);

    //将各组件添加到主容器
    mainvbox.getChildren().addAll(new Label("请选择网卡:"), comboBox,
      new Label("设置抓包过滤器(例如 ip and tcp):"), hyperlink, tfFilter,
      new Label("设置抓包大小(建议介于68~1514之间):"), tfSize, cb,
      new Separator(), hBoxBottom);

    Scene scene = new Scene(mainvbox);
    stage.setScene(scene);
//    stage.show();//不要显示对话框,由主窗体调用显示

    //**************事件响应部分***************************

    //确定按钮
    btnConfirm.setOnAction(event -> {
      try {
        int index = comboBox.getSelectionModel().getSelectedIndex();
        // 选择网卡的接口
        NetworkInterface networkInterface = devices[index];
        // 抓包大小
        int snapLen = Integer.parseInt(tfSize.getText().trim());
        // 是否混杂模式
        boolean promisc = cb.isSelected();
        jpcapCaptor = JpcapCaptor.openDevice(networkInterface, snapLen, promisc, 20);
        jpcapCaptor.setFilter(tfFilter.getText().trim(), true);
        stage.hide();
      } catch (Exception e) {
        e.printStackTrace();
        new Alert(Alert.AlertType.ERROR, e.getMessage()).showAndWait();
      }
    });

    //取消按钮
    btnCancel.setOnAction(event -> {
      stage.hide();
    });

    try {
      hyperlink.setOnAction(
        (ActiveEvent) -> {
          try {
            Desktop.getDesktop().browse(new URI(hyperlink.getText()));
          } catch (Exception e) {
            e.printStackTrace();
          }
        });
    } catch (Exception e) {
      e.printStackTrace();
    }
  }


  public JpcapCaptor getJpcapCaptor() {
    return jpcapCaptor;
  }

  public void showAndWait() {
    stage.showAndWait();
  }

  public void show() {
    stage.show();
  }

  public ArrayList<String> getDetailDeviceName() throws InvocationTargetException, IllegalAccessException {
    ArrayList<String> detailDevices = new ArrayList<>();
    int DRIVER_CLASS_ROOT = WinRegistry.HKEY_LOCAL_MACHINE;
    String DRIVER_CLASS_PATH =
      "SYSTEM\\CurrentControlSet\\Control\\Class";
    String NETCFG_INSTANCE_KEY = "NetCfgInstanceId";//这个值就是JPcap 返回的device.name 包含的guid值


    for (NetworkInterface networkInterface : this.devices) {
      System.out.println(networkInterface.name);
      String[] nameParts = networkInterface.name.split("[{}]", -1);
      if (nameParts.length > 1) {
        String guid = "{" + nameParts[1] + "}"; // 使用正则表达式划分{和},参数-1表示保留分割后全部内容,split返回的是一个数组

        // 从device.name 中提取出guid值
        //在注册表中循环遍历搜索对应的guid值
        for (String driverClassSubkey :
          WinRegistry.readStringSubKeys(DRIVER_CLASS_ROOT, DRIVER_CLASS_PATH,
            0)) {
          for (String driverSubkey :
            WinRegistry.readStringSubKeys(DRIVER_CLASS_ROOT, DRIVER_CLASS_PATH +
              "\\" + driverClassSubkey, 0)) {
            String path = DRIVER_CLASS_PATH + "\\" + driverClassSubkey +
              "\\" + driverSubkey;
            String netCfgInstanceId =
              WinRegistry.readString(DRIVER_CLASS_ROOT, path, NETCFG_INSTANCE_KEY,
                0);
            if (netCfgInstanceId != null &&
              netCfgInstanceId.equalsIgnoreCase(guid)) {
              String theDriverName =
                trimOrDefault(WinRegistry.readString(DRIVER_CLASS_ROOT, path, "DriverDesc", 0), "");
              detailDevices.add(theDriverName);
              break;
            }
          }
        }
      } else {
        System.out.println("Invalid device name format: " + networkInterface.name);
      }
    }

    for (String detailDevice : detailDevices) {
      System.out.println(detailDevice);
    }

    return detailDevices;
  }

  /**
   * @param str A string.
   * @param def A default string.
   * @return Returns def if str is null or empty (after trim),
   * otherwise returns str, trimmed.
   */
  private final static String trimOrDefault(String str, String def) {
    str = (str == null) ? "" : str.trim();
    return str.isEmpty() ? def : str;
  }
}

chapter11(网络发包与抓包二)

1. 完善设置对话框ConfigDialog.java

上一讲的抓包程序显示的是抓包的原始信息,我们可以完善程序,当TCP包中的数据包含特定的关键词,就将TCP包中的数据部分显示出来,对于那些明文传输的数据,就能够进行捕获分析。

如图11.1所示,增加设置数据关键字的功能,希望捕获的多个关键字用空格隔开,多个关键字建议使用“或”的关系。

image-20241130161459445

  • 在ConfigDialog.java 中,增加一个 getKeyData()方法,用于给主程序 PacketCaptureFX 返回用户输入的关键字信息

  • 修改主程序PacketCaptureFX中的内部类 PacketHandler,使得当TCP包中的数据部分包含关键字,则将该包中的数据部分在显示区显示

package chapter11.PacketCapture;

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.TextField;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;

import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;

public class ConfigDialog {
  private JpcapCaptor jpcapCaptor; // 用于返回给主窗体
  //网卡列表
  private NetworkInterface[] devices = JpcapCaptor.getDeviceList();
  private Stage stage = new Stage();//对话框窗体
  private Hyperlink hyperlink = new Hyperlink("https://oc.gdufs.edu.cn/filter");
  private TextField keydata = new TextField();

  //parentStage表示抓包主程序(PacketCaptureFX)的stage,传值可通过这种构造方法参数的方式
  public ConfigDialog(Stage parentStage) throws InvocationTargetException, IllegalAccessException {
    //设置该对话框的父窗体为调用者的那个窗体
    stage.initOwner(parentStage);
    // 设置为模态窗口,不关闭则不能切换焦点
    stage.initModality(Modality.WINDOW_MODAL);
    stage.setResizable(false);  // 禁止尺寸变化
    stage.setTitle("选择网卡并设置参数");

    ArrayList<String> detailDevices = this.getDetailDeviceName();

  // 窗体主vbox
    VBox mainvbox = new VBox(10);
    mainvbox.setPadding(new Insets(10, 30, 10, 30));

  // 下拉条
    ComboBox<String> comboBox = new ComboBox<>();
    comboBox.setMaxWidth(800);
  // 假设 devices 是一个 NetworkInterface 数组或者其他类似的设备数组
    for (int i = 0; i < devices.length; i++) {
      // 确保 detailDevices 和 devices 列表是同步的
      if (i < detailDevices.size()) {
        comboBox.getItems().add(i + " : " + "(" + detailDevices.get(i) + ")" + " - " + devices[i].description);
      } else {
        comboBox.getItems().add(i + " : " + devices[i].description);
      }
    }
    //默认选择第一项
    comboBox.getSelectionModel().selectFirst();

    //设置抓包过滤
    TextField tfFilter = new TextField();

    //设置抓包大小(一般建议在68-1514之间,默认1514)
    TextField tfSize = new TextField("1514");

    //是否设置混杂模式
    CheckBox cb = new CheckBox("是否设置为混杂模式");
    cb.setSelected(true); //默认选中

    //底部确定和取消按钮
    HBox hBoxBottom = new HBox(10);
    hBoxBottom.setPadding(new Insets(10, 30, 10, 30));
    hBoxBottom.setAlignment(Pos.CENTER_RIGHT);

    Button btnConfirm = new Button("确定");
    Button btnCancel = new Button("取消");
    hBoxBottom.getChildren().addAll(btnConfirm, btnCancel);

    //将各组件添加到主容器
    mainvbox.getChildren().addAll(new Label("请选择网卡:"), comboBox,
      new Label("设置抓包过滤器(例如 ip and tcp):"), hyperlink, tfFilter,
      new Label("包中数据包含的关健字,匹配则显示数据内容(多个关健字为or关系,用空格隔开):"),keydata,
      new Label("设置抓包大小(建议介于68~1514之间):"), tfSize, cb,
      new Separator(), hBoxBottom);

    Scene scene = new Scene(mainvbox);
    stage.setScene(scene);
//    stage.show();//不要显示对话框,由主窗体调用显示

    //**************事件响应部分***************************

    //确定按钮
    btnConfirm.setOnAction(event -> {
      try {
        int index = comboBox.getSelectionModel().getSelectedIndex();
        // 选择网卡的接口
        NetworkInterface networkInterface = devices[index];
        // 抓包大小
        int snapLen = Integer.parseInt(tfSize.getText().trim());
        // 是否混杂模式
        boolean promisc = cb.isSelected();
        jpcapCaptor = JpcapCaptor.openDevice(networkInterface, snapLen, promisc, 20);
        jpcapCaptor.setFilter(tfFilter.getText().trim(), true);
        stage.hide();
      } catch (Exception e) {
        e.printStackTrace();
        new Alert(Alert.AlertType.ERROR, e.getMessage()).showAndWait();
      }
    });

    //取消按钮
    btnCancel.setOnAction(event -> {
      stage.hide();
    });

    try {
      hyperlink.setOnAction(
        (ActiveEvent) -> {
          try {
            Desktop.getDesktop().browse(new URI(hyperlink.getText()));
          } catch (Exception e) {
            e.printStackTrace();
          }
        });
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public String getKeyData(){
      return this.keydata.getText();
  }

  public JpcapCaptor getJpcapCaptor() {
    return jpcapCaptor;
  }

  public void showAndWait() {
    stage.showAndWait();
  }

  public void show() {
    stage.show();
  }

  public ArrayList<String> getDetailDeviceName() throws InvocationTargetException, IllegalAccessException {
    ArrayList<String> detailDevices = new ArrayList<>();
    int DRIVER_CLASS_ROOT = WinRegistry.HKEY_LOCAL_MACHINE;
    String DRIVER_CLASS_PATH =
      "SYSTEM\\CurrentControlSet\\Control\\Class";
    String NETCFG_INSTANCE_KEY = "NetCfgInstanceId";//这个值就是JPcap 返回的device.name 包含的guid值


    for (NetworkInterface networkInterface : this.devices) {
      System.out.println(networkInterface.name);
      String[] nameParts = networkInterface.name.split("[{}]", -1);
      if (nameParts.length > 1) {
        String guid = "{" + nameParts[1] + "}"; // 使用正则表达式划分{和},参数-1表示保留分割后全部内容,split返回的是一个数组

        // 从device.name 中提取出guid值
        //在注册表中循环遍历搜索对应的guid值
        for (String driverClassSubkey :
          WinRegistry.readStringSubKeys(DRIVER_CLASS_ROOT, DRIVER_CLASS_PATH,
            0)) {
          for (String driverSubkey :
            WinRegistry.readStringSubKeys(DRIVER_CLASS_ROOT, DRIVER_CLASS_PATH +
              "\\" + driverClassSubkey, 0)) {
            String path = DRIVER_CLASS_PATH + "\\" + driverClassSubkey +
              "\\" + driverSubkey;
            String netCfgInstanceId =
              WinRegistry.readString(DRIVER_CLASS_ROOT, path, NETCFG_INSTANCE_KEY,
                0);
            if (netCfgInstanceId != null &&
              netCfgInstanceId.equalsIgnoreCase(guid)) {
              String theDriverName =
                trimOrDefault(WinRegistry.readString(DRIVER_CLASS_ROOT, path, "DriverDesc", 0), "");
              detailDevices.add(theDriverName);
              break;
            }
          }
        }
      } else {
        System.out.println("Invalid device name format: " + networkInterface.name);
      }
    }

    for (String detailDevice : detailDevices) {
      System.out.println(detailDevice);
    }

    return detailDevices;
  }

  /**
   * @param str A string.
   * @param def A default string.
   * @return Returns def if str is null or empty (after trim),
   * otherwise returns str, trimmed.
   */
  private final static String trimOrDefault(String str, String def) {
    str = (str == null) ? "" : str.trim();
    return str.isEmpty() ? def : str;
  }
}
package chapter11.PacketCapture;

import javafx.application.Application;
import javafx.application.Platform;
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.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import jpcap.JpcapCaptor;
import jpcap.PacketReceiver;
import jpcap.packet.Packet;

import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;

public class PacketCaptureFX extends Application {
  private TextArea infoArea = new TextArea();
  private Button startBtn = new Button("开始抓包");
  private Button stopBtn = new Button("停止抓包");
  private Button clearBtn = new Button("清除内容");
  private Button settingBtn = new Button("设置参数");
  private Button quitBtn = new Button("退出");
  private ConfigDialog configDialog;
  private JpcapCaptor jpcapCaptor;
  private Thread packetCaptureThread;

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

  @Override
  public void start(Stage primaryStage) throws InvocationTargetException, IllegalAccessException {
    configDialog = new ConfigDialog(primaryStage); // 设置父窗口

    VBox showvbox = new VBox(10);
    showvbox.setPadding(new Insets(10, 20, 10, 20));
    showvbox.getChildren().addAll(new Label("抓包信息:"), infoArea);
    VBox.setVgrow(infoArea, Priority.ALWAYS);
    VBox.setVgrow(showvbox, Priority.ALWAYS);
    infoArea.setEditable(false);
    infoArea.setWrapText(true);

    HBox buttonsBox = new HBox(10);
    buttonsBox.setPadding(new Insets(10, 10, 10, 10));
    buttonsBox.setAlignment(Pos.CENTER);
    buttonsBox.getChildren().addAll(startBtn, stopBtn, clearBtn, settingBtn, quitBtn);

    VBox mainBox = new VBox(10);
    mainBox.getChildren().addAll(showvbox, buttonsBox);

    Scene scene = new Scene(mainBox, 700, 500);
    primaryStage.setScene(scene);
    primaryStage.show();

    clearBtn.setOnAction(event -> {
      infoArea.clear();
    });

    quitBtn.setOnAction(event -> {
      if (this.packetCaptureThread != null) {
        this.packetCaptureThread.interrupt();
      }
      System.exit(0);
    });

    settingBtn.setOnAction(event -> {
      if (configDialog == null) {
        try {
          configDialog = new ConfigDialog(primaryStage);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
      configDialog.showAndWait();
      //获取设置后的JpcapCaptor对象实例
      jpcapCaptor = configDialog.getJpcapCaptor();
    });

    // 开始抓包 按钮动作事件
    startBtn.setOnAction(event -> {
      //还没有jpcapCaptor对象实例,则打开设置对话框
      if (jpcapCaptor == null) {
        settingBtn.fire();
        return;
      }
      //停止还没结束的抓包线程
      if (packetCaptureThread != null) {
        interrupt("captureThread");
      }
      String keyData = this.configDialog.getKeyData();
      //开线程名为"captureThread"的新线程进行抓包  
      packetCaptureThread =
        new Thread(() -> {
          while (true) {
            //如果声明了本线程被中断,则退出循环
            if (Thread.currentThread().isInterrupted())
              break;
            //每次抓一个包,交给内部类PacketHandler的实例处理
            // PacketHandler为接口PacketReceiver的实现类
            jpcapCaptor.processPacket(1, new PacketHandler(keyData));
          }
        }, "captureThread");
      //将当前的主线程优先级提高
      Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
      //降低抓包线程的优先级,避免抓包线程卡住资源
      packetCaptureThread.setPriority(Thread.MIN_PRIORITY);
      packetCaptureThread.start();
      this.startBtn.setDisable(true);
    });

    stopBtn.setOnAction(event -> {
      if (jpcapCaptor != null) {
        jpcapCaptor.close();
      }
      interrupt("captureThread");
      startBtn.setDisable(false);
    });
  }


  /**
   * 循环遍历指定线程名的线程列表,并声明关闭,需要关闭的线程需要在构造时指定线
   * 程名,作为参数传入
   */
  private void interrupt(String threadName) {
    ThreadGroup currentGroup =
      Thread.currentThread().getThreadGroup();
    //获取当前线程的线程组及其子线程组中活动线程数量
    int noThreads = currentGroup.activeCount();
    Thread[] lstThreads = new Thread[noThreads];
    currentGroup.enumerate(lstThreads);//将活动线程复制到线程数组
    //遍历这些活动线程,符合指定线程名的则声明关闭
    for (int i = 0; i < noThreads; i++) {
      if (lstThreads[i].getName().equals(threadName)) {
        lstThreads[i].interrupt();//声明线程关闭
      }
    }
  }

  private class PacketHandler implements PacketReceiver {
    private String keyData;

    public PacketHandler(String keyData) {
      this.keyData = keyData;
    }

    public String getKeyData() {
      return keyData;
    }

    public void setKeyData(String keyData) {
      this.keyData = keyData;
    }

    @Override
    public void receivePacket(Packet packet) {
        // 在显示区显示抓包原始信息
//        String msg = packet.toString() + "\n";
        if (keyData == null || keyData.equalsIgnoreCase("")) {
          return;
        }
        //将keyData按空格切分出包含多个关键词的字符串数组
        //提示,split方法也可以使用正则表达式,\s+表示匹配一个或多个空白
        String[] keyDataList = keyData.split("\\s+");
        String msg = new String(packet.data, 0, packet.data.length, StandardCharsets.UTF_8);
        for (String key: keyDataList) {
          if(msg.toUpperCase().contains(key.toUpperCase())){
            Platform.runLater(()->{infoArea.appendText(msg);});
          }
          break;
        }
    }
  }
}

2. 使用Jpcp发包的基本步骤

Java 自带的类库对TCP/IP协议做了很好的封装,通过不同的Socket来处理TCP、UDP协议。但Java标准库并不能处理原始套接字(raw Socket,可以 用来自行组装IP数据包,然后将数据包发送到其他终端。即允许直接发送/接收IP协议数据包而不需要任何传输层协议格式)。

有时需要发送一个或多个IP包、TCP包等特定的构造包,以实现特定的目的,如网络安全检测、隐蔽扫描、网络泛洪攻击等,标准的Java套接字就无能为力。这时就可以使用 Jpcap 开发包中提供的JpcapSender类。其发送TCP包的顺序大致为构造TCP包设置其IP报头填充TCP数据构造以太网帧发送包

构造包和发送包的详细过程如下:

  1. 获取JpcapSender 对象实例

    先按照上一讲的知识,获取网络接口参数,然后选择有效网卡接口获得 JpcapSender 对象实例:

    //open a network interface to send a packet   
    NetworkInterface[] devices = JpcapCaptor.getDeviceList();  
    JpcapSender sender = JpcapSender.openDevice(devices[0]);  
    //或者用 jpcapCaptor.getJpcapSenderInstance();获得对象实例
  2. 构造TCP包 TCP包的构造方法为:

    public TCPPacket(int src_port,int dst_port,long sequence,long ack_num, boolean urg,boolean ack,boolean psh,boolean rst, boolean syn,boolean fin,boolean rsv1,boolean rsv2, int window,int urgent);

    构造方法中的参数实际就是TCP报头的参数。我们就可以按实际需求构造TCP包,例如:

    //create a TCP packet with specified port numbers, flags, and other parameters 
    TCPPacket tcp = new TCPPacket;    
    (8000,80,56,78,false,false,false,false,true,false,true,true,200,10); 
  3. 设置IPv4头参数 使用setIPv4Parameter 方法来设置Ipv4报头参数,该方法的方法头定义如下:

    public void setIPv4Parameter(int priority, boolean d_flag, boolean  t_flag,  boolean r_flag, int rsv_tos, boolean rsv_frag, boolean  dont_frag,  boolean more_frag, int offset, int ident, int ttl, int  protocol, InetAddress src, InetAddress dst)

    例如,我们就可以这样设置参数:

    //specify IPv4 header parameters  
    tcp.setIPv4Parameter(0,false,false,false,0,false,false,false,0,1010101,100, IPPacket.IPPROTO_TCP, InetAddress.getByName(IP地址), InetAddress.getByName (目的IP地址));
  4. 填充TCP中的数据 tcp.data = "填充的数据".getBytes("utf-8");//字节数组型的填充数据

  5. 构造以太网数据包(帧)

    //create an Ethernet packet (frame)  
    EthernetPacket ether = new EthernetPacket();  
    
    //set frame type as IP  
    ether.frametype = EthernetPacket.ETHERTYPE_IP;  
    
    //set the datalink frame of the tcp packet as ether  
    tcp.datalink = ether;  
    
    //set source and destination MAC addresses 
    //MAC 地址要转换成十进制,ipconfig /all 查看本机的MAC地址  
    //源地址是自己机器的MAC地址,以下仅为用法示例
    ether.src_mac = new  byte[]{(byte)00,(byte)27,(byte)185,(byte)177,(byte)74,(byte)70}; 
    
    //根据实际情况设置目的MAC地址(默认网关), arp -a 可以查看相关的MAC地址 
    ether.dst_mac = new  byte[]{(byte)00,(byte)17,(byte)93,(byte)157,(byte)128,(byte)00};
  6. 发送特定的TCP包

    sender.sendPacket(tcp);  ...... //省略  
    sender.close();

3. 创建网卡对话框

如果没有网卡选择对话框,而是将选择的网卡接口写死在程序中,在不同机器上运行就非常不方便,而且容易忘记修改指定的网卡接口。

NetworkChoiceDialog 类中提供一个getSender()方法,返回给主程序 JpcapSender 对象实例。

image-20241130163457570

4. 创建PacketSender.java

在 PacketSender 类中

  • 提供一个静态方法sendTCPPacket用于发送报文
public static void sendTCPPacket(JpcapSender sender, int srcPort, int dstPort, String srcHost, String dstHost, String data, String srcMAC, String dstMAC, boolean syn, boolean ack, boolean rst, boolean fin) {
    try {
      //构造一个TCP包
      TCPPacket tcp = new TCPPacket(srcPort,dstPort,56,78,false,ack,false,rst,syn,fin,true,true,200,10);

      //设置IPv4报头参数,ip地址可以伪造
      tcp.setIPv4Parameter(0,false,false,false,0,false,false,false,0,1010101,100, IPPacket.IPPROTO_TCP, InetAddress.getByName(srcHost),
        InetAddress.getByName (dstHost));

      //填充TCP包中的数据
      tcp.data = data.getBytes(StandardCharsets.UTF_8);//字节数组型的填充数据


      //构造相应的MAC帧
      //create an Ethernet packet (frame)
      EthernetPacket ether = new EthernetPacket();
      //set frame type as IP
      ether.frametype = EthernetPacket.ETHERTYPE_IP;
      //set the datalink frame of the tcp packet as ether
      tcp.datalink = ether;

      ether.src_mac = convertMacFormat(srcMAC);
      ether.dst_mac = convertMacFormat(dstMAC);
      if (ether.src_mac == null || ether.dst_mac == null)
        throw new Exception("MAC地址输入错误");

      sender.sendPacket(tcp);

      System.out.println("发包成功!");
    } catch (Exception e) {
      System.err.println(e.getMessage());
      //重新抛出异常,调用者可以捕获处理
      throw new RuntimeException(e);
    }
  }
  • 另外由于JpcapSender中使用的MAC地址是字节数组,每次转换会很麻烦,所以在该类中提供一个静态方法convertMacFormat,用于将常见的MAC地址形式转为Jpcap可用的字节数组。

coverMacFormat.java示例代码

package chapter11.PacketSender;
public class Test {
  public static void main(String[] args){
        String mac = "00:22:33:44:55:66";
        byte[] bytes = convertMacFormat(mac);
    for (byte b: bytes
         ) {
      System.out.println(b);
    }
  }
  public static byte[] convertMacFormat(String MAC) {
    if (MAC.contains("-") || MAC.contains(":")) {
      // 使用正则表达式同时匹配":"和"-"
      String[] macList = MAC.split(":");
      byte[] macBytes = new byte[6]; // 字节基本类型,取值范围-128 ~ 127
      int index = 0;
      for (String str : macList) {
        // 将每两个十六进制字符转换为一个字节
//        macBytes[index] = Byte.parseByte(str, 16);
        macBytes[index] = (byte) Integer.parseInt(str, 16); //强制类型转换  "22"当作十六进制转成十进制是2*16^0 + 2*16^1 = 34
        index++;
      }
      return macBytes;
    }
    return null;
  }
}

5. 窗口主界面

checkBox控件的使用

在JavaFX中,CheckBox 是一个允许用户选择或取消选择的控件。以下是一些基本的用法和属性:

创建CheckBox

你可以使用 CheckBox 类来创建一个复选框。例如:

import javafx.scene.control.CheckBox;
import javafx.scene.layout.VBox;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class CheckBoxExample extends Application {
    @Override
    public void start(Stage primaryStage) {
        CheckBox checkBox1 = new CheckBox("Option 1");
        CheckBox checkBox2 = new CheckBox("Option 2");

        VBox vbox = new VBox(10); // 10 pixels spacing
        vbox.getChildren().addAll(checkBox1, checkBox2);

        Scene scene = new Scene(vbox, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
CheckBox属性
  • text: 显示在复选框旁边的文本。
  • selected: 表示复选框是否被选中的布尔值。CheckBox可以通过setSelected方法来设置是否勾选,通过isSelected方法来判断是否被选中。
  • allowIndeterminate: 允许复选框处于不确定状态(即既不是选中也不是未选中)。
  • indeterminate: 表示复选框是否处于不确定状态。
CheckBox事件

你可以监听复选框的状态变化事件:

checkBox.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        boolean isSelected = checkBox.isSelected();
        System.out.println("CheckBox is selected: " + isSelected);
    }
});
CheckBox组

如果你需要确保一组复选框中只有一个被选中,你可以使用 ToggleGroup

import javafx.scene.control.CheckBox;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.VBox;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class CheckBoxGroupExample extends Application {
    @Override
    public void start(Stage primaryStage) {
        ToggleGroup toggleGroup = new ToggleGroup();

        CheckBox checkBox1 = new CheckBox("Option 1");
        checkBox1.setToggleGroup(toggleGroup);
        checkBox1.setSelected(true);

        CheckBox checkBox2 = new CheckBox("Option 2");
        checkBox2.setToggleGroup(toggleGroup);

        VBox vbox = new VBox(10);
        vbox.getChildren().addAll(checkBox1, checkBox2);

        Scene scene = new Scene(vbox, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

在这个例子中,checkBox1checkBox2 被添加到同一个 ToggleGroup 中,所以它们之间是互斥的,即只能有一个被选中。

主界面搭建

image-20241130164129314

建议程序一运行就先显示网卡选择对话框,选择后再显示主界面,部分参考代码如下:

 ......  
primaryStage.setScene(scene);  
primaryStage.setTitle("发送自构包");  
primaryStage.setWidth(500);  
dialog = new NetworkChoiceDialog(primaryStage); 
dialog.showAndWait();  sender = dialog.getSender();  
primaryStage.show();  
......

最终代码

NetWorkChoiceDialog.java

package chapter11.PacketSender;

import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import jpcap.JpcapCaptor;
import jpcap.JpcapSender;
import jpcap.NetworkInterface;

import java.awt.event.ActionEvent;
import java.io.IOException;

public class NetworkChoiceDialog {
  //网卡列表
  private NetworkInterface[] devices = JpcapCaptor.getDeviceList();
  private Stage stage = new Stage();//对话框窗体
  private Button sureBtn = new Button("确定");
  private JpcapSender jpcapSender;

  //parentStage表示抓包主程序(PacketCaptureFX)的stage,传值可通过这种构造方法参数的方式
  public NetworkChoiceDialog(Stage parentStage) {
    //设置该对话框的父窗体为调用者的那个窗体
    stage.initOwner(parentStage);
    // 设置为模态窗口,不关闭则不能切换焦点
    stage.initModality(Modality.WINDOW_MODAL);
    stage.setResizable(false);  // 禁止尺寸变化
    stage.setTitle("选择网卡并设置参数");

    // 窗体主vbox
    VBox mainvbox = new VBox(10);
    mainvbox.setPadding(new Insets(10, 30, 10, 30));

    // 下拉条
    ComboBox<String> comboBox = new ComboBox<>();
    comboBox.setMaxWidth(800);
    // 假设 devices 是一个 NetworkInterface 数组或者其他类似的设备数组
    for (int i = 0; i < devices.length; i++) {
      // 确保 detailDevices 和 devices 列表是同步的
      comboBox.getItems().add(devices[i].description);
    }
    //默认选择第一项
    comboBox.getSelectionModel().selectFirst();

    //将各组件添加到主容器
    mainvbox.getChildren().addAll(new Label("请选择网卡:"), comboBox,sureBtn);

    Scene scene = new Scene(mainvbox);
    stage.setScene(scene);

    sureBtn.setOnAction(event->{
      try {
        int index = comboBox.getSelectionModel().getSelectedIndex();
        // 选择网卡的接口
        NetworkInterface networkInterface = devices[index];
        this.jpcapSender = JpcapSender.openDevice(networkInterface);
      } catch (IOException e) {
        e.printStackTrace();
      }
      stage.hide();
    });
  }

  public void showAndWait(){
    stage.showAndWait();
  }

  public JpcapSender getSender(){ return jpcapSender; }
}

PacketSender.java

package chapter11.PacketSender;

import jpcap.JpcapSender;
import jpcap.packet.EthernetPacket;
import jpcap.packet.IPPacket;
import jpcap.packet.TCPPacket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

/**
 * 封装Jpcap发包功能
 */
public class PacketSender {
  /**
   * @param sender  JpcapSender类型
   * @param srcPort 源端口
   * @param dstPort 目的端口
   * @param srcHost ip地址形式或类似 www.baidu.com的域名形式
   * @param dstHost
   * @param data    填充到tcp包中的数据
   * @param srcMAC  格式为"dc-8b-28-87-b9-82"或"dc:8b:28:87:b9:82"
   * @param dstMAC
   * @param syn     这几个为常用标识位
   * @param ack
   * @param rst
   * @param fin
   */
  public static void sendTCPPacket(JpcapSender sender, int srcPort, int dstPort, String srcHost, String dstHost, String data, String srcMAC, String dstMAC, boolean syn, boolean ack, boolean rst, boolean fin) {
    try {
      //构造一个TCP包
      TCPPacket tcp = new TCPPacket(srcPort,dstPort,56,78,false,ack,false,rst,syn,fin,true,true,200,10);


      //设置IPv4报头参数,ip地址可以伪造
      tcp.setIPv4Parameter(0,false,false,false,0,false,false,false,0,1010101,100, IPPacket.IPPROTO_TCP, InetAddress.getByName(srcHost),
        InetAddress.getByName (dstHost));

      //填充TCP包中的数据
      tcp.data = data.getBytes(StandardCharsets.UTF_8);//字节数组型的填充数据


      //构造相应的MAC帧
      //create an Ethernet packet (frame)
      EthernetPacket ether = new EthernetPacket();
      //set frame type as IP
      ether.frametype = EthernetPacket.ETHERTYPE_IP;
      //set the datalink frame of the tcp packet as ether
      tcp.datalink = ether;
      //set source and destination MAC addresses
      //MAC 地址要转换成十进制,ipconfig /all 查看本机的MAC地址
      //源地址是自己机器的MAC地址,以下仅为用法示例
//      ether.src_mac = new byte[]{(byte)00,(byte)27,(byte)185,(byte)177,(byte)74,(byte)70};
//      //根据实际情况设置目的MAC地址, arp -a 可以查看相关的MAC地址
//      ether.dst_mac = new
//        byte[]{(byte)00,(byte)17,(byte)93,(byte)157,(byte)128,(byte)00};

      ether.src_mac = convertMacFormat(srcMAC);
      ether.dst_mac = convertMacFormat(dstMAC);
      if (ether.src_mac == null || ether.dst_mac == null)
        throw new Exception("MAC地址输入错误");

      sender.sendPacket(tcp);

      System.out.println("发包成功!");
    } catch (Exception e) {
      System.err.println(e.getMessage());
      //重新抛出异常,调用者可以捕获处理
      throw new RuntimeException(e);
    }
  }

  public static byte[] convertMacFormat(String MAC) {
    if (MAC.contains("-") || MAC.contains(":")) {
      // 使用正则表达式同时匹配":"和"-"
      String[] macList = MAC.split("[:-]");
      byte[] macBytes = new byte[6]; // 字节基本类型,取值范围-128 ~ 127
      int index = 0;
      //(3)定义一个6字节的字节数组,循环将十六进制形式的字符串转为字节,赋值给字节数组;
      //提示:利用Integer.parseInt(字符串,进制)将16进制表示的字符串转为字节,
      //例如:(byte)Integer.parseInt("0F",16)
      for (String str : macList) {
        // 将每两个十六进制字符转换为一个字节
//        macBytes[index] = Byte.parseByte(str, 16);
        macBytes[index] = (byte) Integer.parseInt(str, 16); //强制类型转换
        index++;
      }
      return macBytes;
    }
    return null;
  }
}

SendPacketFx.java

package chapter11.PacketSender;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import jpcap.JpcapSender;


public class SendPacketFx extends Application {
  private NetworkChoiceDialog networkChoiceDialog;
  private JpcapSender sender;
  private TextField srcport_tf = new TextField();
  private TextField dstport_tf = new TextField();
  private CheckBox syn_check = new CheckBox("SYN");
  private CheckBox ack_check = new CheckBox("ACK");
  private CheckBox rst_check = new CheckBox("RST");
  private CheckBox fin_check = new CheckBox("FIN");
  private TextField srcHosttf = new TextField();
  private TextField dstHosttf = new TextField();
  private TextField srcMactf = new TextField();
  private TextField dstMactf = new TextField();
  private TextField datatf = new TextField();
  private Button sendButton = new Button("发送TCP包");
  private Button selectNetButton = new Button("选择网卡");
  private Button quitButton = new Button("退出");

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

  @Override
  public void start(Stage primaryStage) {

    VBox mainbox = new VBox(10);
    mainbox.setPadding(new Insets(10,20,10,20));

    HBox portHbox = new HBox(10);
    portHbox.setPadding(new Insets(10, 10, 10, 10));
    portHbox.getChildren().addAll(new Label("源端口:"), srcport_tf, new Label("目的端口:"), dstport_tf);

    HBox checkHbox = new HBox(10);
    checkHbox.setPadding(new Insets(10, 10, 10, 10));
    checkHbox.getChildren().addAll(new Label("TCP标识位:"), syn_check, ack_check, rst_check, fin_check);

    VBox dstVbox = new VBox(10);
    dstVbox.setPadding(new Insets(10, 10, 10, 10));
    dstVbox.getChildren().addAll(new Label("源主机地址:"), srcHosttf, new Label("目的主机地址:"), dstHosttf,
      new Label("源MAC地址:"), srcMactf, new Label("目的MAC地址"), dstMactf, new Label("数据:"), datatf);

    HBox btnHbox = new HBox(10);
    btnHbox.setPadding(new Insets(10,10,10,10));
    btnHbox.getChildren().addAll(sendButton,selectNetButton,quitButton);
    btnHbox.setAlignment(Pos.CENTER);

    mainbox.getChildren().addAll(portHbox,checkHbox,dstVbox,btnHbox);
    VBox.setVgrow(mainbox, Priority.ALWAYS);

    Scene scene = new Scene(mainbox, 700, 550);
    primaryStage.setScene(scene);
    primaryStage.setTitle("发送自构包");
    networkChoiceDialog = new NetworkChoiceDialog(primaryStage);
    networkChoiceDialog.showAndWait();
    sender = networkChoiceDialog.getSender();
    primaryStage.show();

    quitButton.setOnAction(event -> {
      System.exit(0);
    });
    sendButton.setOnAction(event -> {
      try {
        int srcPort = Integer.parseInt(srcport_tf.getText().trim());
        int dstPort = Integer.parseInt((dstport_tf.getText().trim()));
        String srcHost = srcHosttf.getText().trim();
        String dstHost = dstHosttf.getText().trim();
        String srcMAC = srcMactf.getText().trim();
        String dstMAC = dstMactf.getText().trim();
        String data = datatf.getText();
        //调用发包方法
        PacketSender.sendTCPPacket(sender, srcPort, dstPort, srcHost,
          dstHost, data, srcMAC, dstMAC,syn_check.isSelected(),
          ack_check.isSelected(),rst_check.isSelected(),fin_check.isSelected());
        new Alert(Alert.AlertType.INFORMATION, "已发送!").showAndWait();
      } catch (Exception e) {
        new Alert(Alert.AlertType.ERROR, e.getMessage()).showAndWait();
      }
    });
    selectNetButton.setOnAction(event -> {
      sender = networkChoiceDialog.getSender();
      networkChoiceDialog.showAndWait();
    });
  }
}

6. 扩展练习一:使用ListView优化数据显示

这段内部教学资料提供了关于如何在JavaFX中使用ListView组件来显示大量数据的指导,特别是与TextArea相比,ListView在处理大量数据时的性能优势。以下是对这段资料的总结和一些补充说明:

ListView的创建及数据绑定

  • 数据源ListView需要一个数据源来显示数据,通常是一个ObservableList对象。
  • **创建ListView**:可以直接在构造方法中绑定数据源,或者在之后通过setItems()方法绑定。
// 创建数据源
private ObservableList<String> dataList = FXCollections.observableArrayList();

// 在构造方法中绑定数据源
private ListView<String> listView = new ListView<>(dataList);

// 或者先创建ListView,然后在其他地方绑定数据源
private ListView<String> listView = new ListView<>();
listView.setItems(dataList);

基本操作

  • 添加数据:使用dataList.add("抓包数据")
  • 清空数据:使用dataList.clear()
  • UI刷新:由于ListView与数据源绑定,任何对数据源的修改都会自动反映在ListView上,无需额外操作。

设置虚拟布局

  • 虚拟布局:为了提高性能,ListView可以设置虚拟布局,只渲染可见区域内的项。
// 设置虚拟布局
listView.setCellFactory(ListView.<String>forListView(dataList));
listView.setFixedCellSize(-1); // 设置为-1则自动根据内容调整高度

实现复制功能

  • 复制选中项ListView默认不支持复制操作,但可以通过监听双击事件来实现。
listView.setOnMouseClicked(event -> {
    if (event.getClickCount() == 2) {
        String selectedText = listView.getSelectionModel().getSelectedItem();
        if (selectedText != null) {
            Clipboard clipboard = Clipboard.getSystemClipboard();
            ClipboardContent content = new ClipboardContent();
            content.putString(selectedText);
            clipboard.setContent(content);
        }
    }
});

完善抓包程序

  • **替换TextArea**:根据上述知识,可以将ListView用作抓包信息的显示界面,替换原有的TextArea

注意事项

  • 线程安全:对于UI操作,如添加数据到ListView,应确保在JavaFX的主线程中执行,可以使用Platform.runLater()来确保线程安全。

  • 性能优化:虚拟布局和合理的数据源管理对于处理大量数据至关重要。

  • 事件处理:对于复制等额外功能,可以通过事件监听和处理来实现。

7.扩展练习二:ARP欺骗

ARP协议与ARP欺骗

  • ARP协议:用于将网络层的IP地址解析为数据链路层的硬件地址。
  • ARP高速缓存:每个主机都有一个ARP高速缓存,存储局域网上的IP地址到硬件地址的映射表。
  • ARP欺骗:基于ARP协议建立在主机间互相信任的基础上,存在安全问题:
    1. 主机地址映射表基于高速缓存动态更新,易被假冒者修改。
    2. ARP请求以广播方式进行,攻击者可以伪装ARP应答。
    3. 可以随意发送ARP应答包,无状态的ARP协议导致无条件更新本机高速缓存。
    4. ARP应答无需认证,局域网协议未考虑安全防范。

ARP欺骗方案

  • 攻击者A与被监控目标B:在同一局域网内,A尝试拦截或监控B的数据包。
    1. A伪造ARP REPLY包,将B的ARP缓存表中的网关IP对应的MAC地址改为A的MAC。
    2. A再伪造ARP REPLY包,将网关的ARP缓存表中的B的IP对应的MAC地址改为A的MAC。
    3. 通过上述步骤,A可以拦截B的数据包,实现监控。
    4. A作为中间人,可以修改数据包的目的地址后转发。

用Jpcap实现拦截监听

  • 实现监听需要
    1. 发送ARP包修改B的ARP缓存表。
    2. 发送ARP包修改网关ARP缓存表。
    3. 转发B发过来的数据包。
    4. 转发网关发过来的数据包。

参考代码可见(ARPTrap.java)

chapter12(rmi远程服务)

RPC(Remote Procedure Call,远程过程调用)

定义与目标

  • RPC是一种程序设计模式,旨在简化分布式应用的构建,使其调用远程服务如同调用本地方法一样简单。
  • 它采用客户端/服务器(C/S)模式,隐藏了网络通信的细节。

基本工作

  1. 通信:RPC使用TCP或UDP等传输协议在客户端和服务器之间传输数据。
  2. 接口统一:客户端和服务器端使用统一的接口,各自实现逻辑。
  3. 序列化与反序列化:参数在网络上以二进制形式传输,需要序列化和反序列化。

实现RPC的协议/框架

  • CORBA、Java RMI、Dubbo、Web Service、Hessian、Thrift等。

RMI(Remote Method Invocation,远程方法调用)

定义

  • RMI是RPC的一种面向对象的Java实现,通常使用TCP协议。
  • 它允许Java虚拟机(JVM)之间的远程方法调用,就像调用本地方法一样。

实现原理

  1. 客户端和服务器辅助对象(Stub):客户端和服务器端都有辅助对象,帮助通信。
  2. 方法调用:客户端调用客户端Stub上的方法,Stub负责转发请求到服务器。
  3. 服务器处理:服务器端Stub接收请求,调用服务对象的方法,并将结果返回给客户端Stub。

image-20241225123632828

优点

  • 无需编写网络或I/O代码,调用远程方法就像调用本地方法。

注意事项

  • 远程对象是相对概念,服务端和客户端角色可以互换,实现回调功能。

RMI程序实现步骤

创建远程接口

  1. 创建包和接口
    • 在项目中新建包rmi
    • rmi包中创建远程接口HelloService
    • 继承java.rmi.Remote接口。
    • 方法声明抛出RemoteException
    • 参数和返回值必须是可序列化的。
    • 服务端和客户端必须有完全相同的远程接口定义。
package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Date;

public interface HelloService extends Remote {
    String echo(String msg) throws RemoteException;
    Date getTime() throws RemoteException;
    ArrayList<Integer> sort(ArrayList<Integer> list) throws RemoteException;
}

创建服务端的远程接口实现类

  1. 创建远程服务类
    • 在项目中新建包chapter12.server
    • chapter12.server包中创建类HelloServiceImpl
package chapter12.server;

import rmi.HelloService;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;

public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    private String name;

    public HelloServiceImpl() throws RemoteException {
    }

    public HelloServiceImpl(String name) throws RemoteException {
        this.name = name;
    }

    @Override
    public String echo(String msg) throws RemoteException {
        System.out.println("服务端完成一些echo方法相关任务......");
        return "echo: " + msg + " from " + name;
    }

    @Override
    public Date getTime() throws RemoteException {
        System.out.println("服务端完成一些getTime方法相关任务......");
        return new Date();
    }

    @Override
    public ArrayList<Integer> sort(ArrayList<Integer> list) throws RemoteException {
        System.out.println("服务端完成排序相关任务......");
        Collections.sort(list);
        return list;
    }
}

创建远程服务发布程序

  1. 发布远程服务
    • chapter12.server包中新建类HelloServer
package chapter12.server;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import rmi.HelloService;

public class HelloServer {
    public static void main(String[] args) {
        try {
            System.setProperty("java.rmi.server.hostname", "本机器的ip地址");
            Registry registry = LocateRegistry.createRegistry(1099);
            HelloService helloService = new HelloServiceImpl("张三的远程服务");
            registry.rebind("HelloService", helloService);
            System.out.println("发布了一个HelloService RMI远程服务");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

创建客户端程序

  1. 获取远程服务并调用
    • chapter12包下新建client包。
    • chapter12.client包中创建JavaFX程序HelloClientFX
    • 核心代码封装在rmiInit()方法中。
package chapter12.client;

import javafx.application.Application;
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.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

// 客户端程序,继承自JavaFX的Application类
public class HelloClientFX extends Application {

    // UI组件
    private TextArea taDisplay = new TextArea(); // 显示信息的文本区域
    private TextField tfMessage = new TextField(); // 输入信息的文本框
    private Button btnEcho = new Button("调用echo方法"); // echo方法按钮
    private Button btnGetTime = new Button("调用getTime方法"); // getTime方法按钮
    private Button btnListSort = new Button("调用sort方法"); // sort方法按钮

    // 客户端也有一份和服务端相同的远程接口
    private HelloService helloService; // 远程服务接口实例

    // 程序入口点
    public static void main(String[] args) {
        launch(args); // 启动JavaFX应用程序
    }

    // JavaFX应用程序的启动方法
    @Override
    public void start(Stage primaryStage) {
        // main区域
        VBox vBoxMain = new VBox();
        vBoxMain.setSpacing(10); // 各控件之间的间隔
        vBoxMain.setPadding(new Insets(10, 20, 10, 20)); // 面板中的内容距离四周的留空区域

        // 水平布局,包含按钮和输入框
        HBox hBox = new HBox();
        hBox.setSpacing(10); // 各控件之间的间隔
        hBox.setPadding(new Insets(10, 20, 10, 20)); // 面板中的内容距离四周的留空区域
        hBox.setAlignment(Pos.CENTER); // 居中对齐
        hBox.getChildren().addAll(new Label("输入信息:"), tfMessage, btnEcho, btnGetTime, btnListSort);

        // 将标签、文本区域和按钮布局添加到垂直布局中
        vBoxMain.getChildren().addAll(new Label("信息显示区:"), taDisplay, hBox);

        // 设置场景和舞台
        Scene scene = new Scene(vBoxMain);
        primaryStage.setScene(scene);
        primaryStage.show();

        // 初始化RMI相关操作
        new Thread(this::rmiInit).start(); // 在新线程中初始化RMI

        // 为按钮设置事件处理器
        btnEcho.setOnAction(event -> {
            try {
                String msg = tfMessage.getText(); // 获取文本框内容
                taDisplay.appendText(helloService.echo(msg) + "\n"); // 调用echo方法并显示结果
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        btnGetTime.setOnAction(event -> {
            try {
                String msg = helloService.getTime().toString(); // 调用getTime方法
                taDisplay.appendText(msg + "\n"); // 显示结果
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        btnListSort.setOnAction(event -> {
            // 演示传递和返回数组列表
            try {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(8);
                list.add(1);
                list.add(9);
                list.add(7);
                list = helloService.sort(list); // 调用sort方法
                for (Integer i : list) {
                    taDisplay.appendText(i + "  "); // 显示排序结果
                }
                taDisplay.appendText("\n");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 初始化RMI相关操作
     */
    private void rmiInit() {
        try {
            // 获取RMI注册器
            Registry registry = LocateRegistry.getRegistry("服务端的ip地址", 1099);
            System.out.println("RMI远程服务别名列表:");
            for (String name : registry.list()) {
                System.out.println(name);
            }

            // 客户端到注册器中使用助记符寻找并创建远程服务对象的客户端stub
            helloService = (HelloService) registry.lookup("HelloService");
            // 另外一种创建stub的方式
            // helloService = (HelloService) Naming.lookup("rmi://服务端ip:1099/" + "HelloService");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20241225123748847

课堂任务

RmiKitService.java

package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RmiKitService extends Remote {
  //以下远程方法全部由学生端实现(即后面的第  步任务),由教师端进行回调

  //远程方法一 将 ipv 格式字符串转为长整型
  long ipToLong(String ip) throws RemoteException;

  //远程方法二 将长整型转为 ipv 字符串格式
  String longToIp(long ipNum) throws RemoteException;

  //远程方法三 将"-"格式连接的 MAC 地址转为 Jpcap 可用的字节数组
  byte[] macStringToBytes(String macStr) throws RemoteException;

  //远程方法四 将 Jpcap 的 byte[]格式的 MAC 地址转为"-"连接 MAC 字符串
  String bytesToMACString(byte[] macBytes) throws RemoteException;
}

RmiMsgService.java

package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RmiMsgService extends Remote {
  //声明远程方法一,用于学生发送信息给教师端,该方法由教师端实现,学生端调用
  String send(String msg) throws RemoteException;

  //声明远程方法二 用于学生发送学号和姓名给教师端,该方法由教师端实现,学生端调用
  String send(String yourNo, String yourName) throws RemoteException;
}

RmiKitServiceImp.java

package chapter12;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

import rmi.RmiKitService;

public class RmiKitServiceImp extends UnicastRemoteObject implements RmiKitService {

  private String name;

  RmiKitServiceImp() throws RemoteException {

  }

  RmiKitServiceImp(String name) throws RemoteException {
    this.name = name;
  }


  @Override
  public long ipToLong(String ip) throws RemoteException {
    String[] ipArray = ip.split("\\.");
    long num = 0;
    for (int i = 0; i < ipArray.length; i++) {
      long valueOfSection = Long.parseLong(ipArray[i]);
      num = (valueOfSection << 8 * (3 - i)) | num;
    }
    return num;
  }


  @Override
  //远程方法二 将长整型转为 ipv 字符串格式
  public String longToIp(long ipNum) throws RemoteException {
    return ((ipNum >> 24) & 0xFF) + "." +
      ((ipNum >> 16) & 0xFF) + "." +
      ((ipNum >> 8) & 0xFF) + "." +
      (ipNum & 0xFF);
  }

  @Override
  //远程方法三 将"-"格式连接的 MAC 地址转为 Jpcap 可用的字节数组
  public byte[] macStringToBytes(String macStr) throws
    RemoteException {
    if (macStr.contains("-") || macStr.contains(":")) {
      // 使用正则表达式同时匹配":"和"-"
      String[] macList = macStr.split(":");
      byte[] macBytes = new byte[6]; // 字节基本类型,取值范围-128 ~ 127
      int index = 0;
      for (String str : macList) {
        // 将每两个十六进制字符转换为一个字节
//        macBytes[index] = Byte.parseByte(str, 16);
        macBytes[index] = (byte) Integer.parseInt(str, 16); //强制类型转换  "22"当作十六进制转成十进制是2*16^0 + 2*16^1 = 34
        index++;
      }
      return macBytes;
    }
    return null;
  }

  @Override
  //远程方法四 将 Jpcap 的 byte[]格式的 MAC 地址转为"-"连接 MAC 字符串
  public String bytesToMACString(byte[] macBytes) throws
    RemoteException {
    /**
     * 将byte数组转换为MAC地址字符串(格式:XX:XX:XX:XX:XX:XX)
     *
     * @param macBytes MAC地址的byte数组(长度应为6)
     * @return 格式化后的MAC地址字符串
     * @throws IllegalArgumentException 如果macBytes的长度不为6
     */
    if (macBytes == null || macBytes.length != 6) {
      throw new IllegalArgumentException("MAC地址的byte数组长度必须为6");
    }

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < macBytes.length; i++) {
      sb.append(String.format("%02X", macBytes[i]));
      if (i < macBytes.length - 1) {
        sb.append("-");
      }
    }
    return sb.toString();
  }
}

RmiStudentServer.java

package chapter12;

import rmi.RmiKitService;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiStudentServer  {

  public static void main(String[] args) {
    try {
      // 对于有多个网卡的机器,建议用下面的命令绑定固定的ip
      System.setProperty("java.rmi.server.hostname", "192.168.236.141");

      // (1)启动RMI注册器,并监听在1099端口(这是RMI的默认端口,正如HTTP的默认端口是80)
      Registry registry = LocateRegistry.createRegistry(1099);

      // (2)实例化远程服务对象,如果有多个远程接口,只实例化自己实现的接口
      RmiKitService rmiKitService = new RmiKitServiceImp("远程服务");

      // (3)用助记符来注册发布远程服务对象,助记符建议和远程服务接口命名相同,这样更好起到“助记”效果
      registry.rebind("RmiKitService",rmiKitService);
      System.out.println("发布了一个 RmiKitService RMI远程服务");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

RmiStudentClientFX.java

package chapter12;

import javafx.application.Application;
import javafx.geometry.Insets;
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.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import rmi.RmiMsgService;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiStudentClientFX extends Application {

  private TextArea taDisplay = new TextArea();
  private TextField tfMessage = new TextField();
  private TextField tfNO = new TextField();
  private TextField tfName = new TextField();
  Button btnSendMsg = new Button("发送信息");
  Button btnSendNoAndName = new Button("发送学号和姓名");

  // 客户端也有一份和服务端相同的远程接口
  private RmiMsgService rmiMsgService;

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

  @Override
  public void start(Stage primaryStage) {
    // main区域
    VBox vBoxMain = new VBox();
    vBoxMain.setSpacing(10); // 各控件之间的间隔
    // VBoxMain面板中的内容距离四周的留空区域
    vBoxMain.setPadding(new Insets(10, 20, 10, 20));

    HBox hBox = new HBox();
    hBox.setSpacing(10); // 各控件之间的间隔
    // HBox面板中的内容距离四周的留空区域
    hBox.setPadding(new Insets(10, 20, 10, 20));
    hBox.setAlignment(javafx.geometry.Pos.CENTER);
    hBox.getChildren().addAll(new Label("输入信息:"), tfMessage,this.btnSendMsg,new Label("学号:"), this.tfNO,new Label("姓名:"),tfName,
      this.btnSendNoAndName);

    vBoxMain.getChildren().addAll(new Label("信息显示区:"), taDisplay, hBox);
    Scene scene = new Scene(vBoxMain);
    primaryStage.setScene(scene);
    primaryStage.show();

    // 初始化rmi相关操作
    new Thread(this::rmiInit).start();

    btnSendMsg.setOnAction(event -> {
      try {
        String msg = tfMessage.getText();
        taDisplay.appendText(rmiMsgService.send(msg) + "\n");
      } catch (RemoteException e) {
        e.printStackTrace();
      }
    });

    btnSendNoAndName.setOnAction(event -> {
      try {
        String msg = rmiMsgService.send(tfNO.getText(), tfName.getText());
        taDisplay.appendText(msg + "\n");
      } catch (RemoteException e) {
        e.printStackTrace();
      }
    });
  }

  /**
   * 初始化rmi相关操作
   */
  public void rmiInit() {
    try {
      // (1)获取RMI注册器
      Registry registry = LocateRegistry.getRegistry("202.116.195.71", 1099);
      System.out.println("RMI远程服务别名列表:");
      for (String name : registry.list()) {
        System.out.println(name);
      }

      // (2)客户端(调用端)到注册器中使用助记符寻找并创建远程服务对象的客户端stub
      rmiMsgService = (RmiMsgService) registry.lookup("RmiMsgService");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

拓展练习

为了在Java RMI中固定TCP端口,可以通过自定义RMISocketFactory来实现。以下是具体的步骤和代码示例:

  1. **创建自定义的RMISocketFactory**:
    创建一个类继承自RMISocketFactory,并重写createServerSocket方法,在该方法中固定端口号。
import java.rmi.server.RMISocketFactory;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class CustomSocketFactory extends RMISocketFactory {
    private static final int FIXED_PORT = 10990; // 定义固定的端口号

    @Override
    public ServerSocket createServerSocket(int port) throws IOException {
        if (port == 0) {
            return new ServerSocket(FIXED_PORT);
        }
        return new ServerSocket(port);
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException {
        return new Socket(host, port);
    }
}
  1. **设置自定义的RMISocketFactory**:
    在实例化UnicastRemoteObject的子类前,设置RMISocketFactory为自定义的CustomSocketFactory
import java.rmi.server.RMISocketFactory;

// 设置自定义的RMISocketFactory
RMISocketFactory.setSocketFactory(new CustomSocketFactory());
  1. 导出远程对象
    使用UnicastRemoteObject.exportObject方法导出远程对象。
YourRemoteInterface yourRemoteObject = new YourRemoteObjectImpl();
yourRemoteObject.exportObject(FIXED_PORT);
  1. 注册远程对象
    使用LocateRegistry.createRegistry创建注册表,并绑定远程对象。
LocateRegistry.createRegistry(1099); // RMI注册表端口,也可以固定
Naming.rebind("rmi://主机名:FIXED_PORT/对象名", yourRemoteObject);

通过以上步骤,你可以固定Java RMI的服务端口,使得防火墙可以配置允许通过特定的端口,从而解决RMI服务端口随机分配导致的问题。