「杂文」应用基础实践一(网络+Java)实验报告

写在前面

诈尸了,妈的我不想上学了事儿太几把多了好相似啊啊啊啊啊啊啊啊啊啊啊啊啊啊——但是似了实验报告还是要写妈的

理应来说这种脑瘫实验和杀软报告就是怎么水怎么来——但是人脑维护大工程太有趣了一开始写就觉得实在是太有趣了就停不下来了妈的,于是就有了这么一拖看着像超级无敌大大大卷王才会搞出来的东西——其实只是完全是停不下来了导致的、、、

老师没有规定实验报告格式要求,于是就用了神器 Keldos-Li_typora-latex-theme 并顺手丢上来一份造福后人。

两个实验都是基于基于基于学长的学长的学长的代码的学长的学长的代码的学长的代码大改的。在这里首先感谢三位学长的倾情奉献:2019 lqy,2020 sfj,2021 kawaii liqing

感觉直接拿过来水不太好,于是在学长的基础上为聊天室进行了若干修正并内嵌了 SQLite 以维护离线用户数据,为网络爬虫添加了链式网页爬取、可交互的敏感词分析界面与可视化爬取有向图分析。经过四次大型迭代,聊天室的源代码总大小已经达到了惊人的 130kb,网络爬虫程序源代码也有近 40kb 的大小。如果您觉得您没有人脑维护如此巨型的屎山(虽然我还觉得挺可读的)的能力,以便在老师的追问下证明这坨屎确实是自己写的——那么不建议直接使用这份代码。

实验目的与要求

实验(一)用JavaSocket编程开发聊天室

实践目的或任务:通过指导学生上机实践,对JavaSocket编程、Java多线程、Java图形用户界面进行掌握。

实践基本要求:

  1. 了解实验目的及实验原理;
  2. 编写程序,并附上程序代码和结果图;
  3. 总结在编程过程中遇到的问题、解决办法和收获。

实践的内容或要求:

  1. 用Java图形用户界面编写聊天室服务器端和客户端,支持多个客户端连接到一个服务器,每个客户端能够输入账号。
  2. 可以实现群聊(聊天记录显示在所有客户端界面)。
  3. 完成好友列表在各个客户端上显示。
  4. 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息。
  5. 服务器能够群发系统消息,能够强行让某些用户下线。
  6. 客户端的上线下线要求能够在其他客户端上面实时刷新。
  7. 用户能够自己建立小群聊天(选做)并解散小群。

实践类型或性质:开发性

实践要求:必做

实验(二)用JavaURL编程爬取并分析网页敏感词

实践目的或任务:通过指导学生上机实践,对JavaURL编程、Java图形界面进行掌握。

实践基本要求:

  1. 了解实验目的及实验原理;
  2. 编写程序,并附上程序代码和结果图;
  3. 总结在编程过程中遇到的问题、解决办法和收获。
    实践的内容或要求:
  4. 编写设计界面,输入一个网址,能够持续爬取从该地址开始链接的所有相关网页,并能控制爬取工作的开始和停止。
  5. 对爬取网页中的文本进行提取。
  6. 建立敏感词库,用文本文件保存,能够修改更新该敏感词库。
  7. 将爬取网页的文本中的敏感词提取出来并高亮显示。
  8. 为所有爬取的网页建立可视化有向图网(选做)。
  9. 编写一个主界面,整合上述功能。

实践类型或性质:开发性

实践要求:实践项目 002、003、004 选做 1 题。

实验环境

  • 操作系统:Windows 11 23H2
  • SDK:Oracle OpenJDK-22.0.1
  • 数据库:SQLite JDBC(版本3.45.10,JDBC4.2)
  • 集成开发环境:IntelliJ IDEA 2024.1
  • 依赖项:
    • 实验(一):数据库驱动 sqlite-jdbc-3.45.1.0e;数据库日志 slf4j-api-2.0.10
    • 实验(二):可视化建图 graphviz-11.0.0

配置环境参考:

实验原理

实验(一)

Socket编程接口

要实现客户/服务器(C/S)应用的工作方式,需使用套接字 Socket编程接口来使用操作系统提供的网络通信功能。 Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,是一组编程接口。它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,使用户仅需关注一组简单的接口,而不需要关注Socket 如何在接口后组织数据以符合指定的协议;无需深入理解 TCP/UDP 协议细节,仅需遵循 Socket 的规定去编程即可。Socket 的地位如下图所示:

Socket 可被理解为地址IP和端口Port的组合:IP 用来标识互联网中的一台主机的位置,Port 用来标识这台机器上的一个应用程序;IP 地址是配置到网卡上的,而 Port 是应用程序开启的,因此 IP 与 Port 的绑定就标识了互联网中独一无二的一个应用程序。

不同的套接字类型包括:

  1. 流式套接字(SOCK_STREAM):用于提供面向连接、可靠的数据传输服务。
  2. 数据报套接字(SOCK_DGRAM):提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
  3. 原始套接字(SOCK_RAW):主要用于实现自定义协议或底层网络协议。

在本实验程序中采用了流式套接字进行通信。其基本模型如下图所示:

其工作过程如下:

  1. 服务器首先启动,通过调用 socket() 建立一个套接字。
  2. 服务器调用绑定方法 bind() 将该套接字和本地网络地址联系在一起。
  3. 服务器调用 listen() 使套接字做好侦听连接的准备,并设定的连接队列的长度。
  4. 客户端在建立套接字后,就可调用连接方法 connect() 向服务器端提出连接请求。
  5. 服务器端在监听到连接请求后,建立和该客户端的连接,并放入连接队列中,并通过调用 accept() 来返回该连接,以便后面通信使用。
  6. 客户端和服务器连接一旦建立,就可以通过调用接收方法 recv()/recvfrom() 和发送方法 send()/sendto() 来发送和接收数据。
  7. 最后,待数据传送结束后,双方调用 close() 关闭套接字。

Java 中的 Socket 类

为了实现 TCP 连接,Java 提供了用于创建套接字的 java.net.Socket 类,与为服务器程序提供了监听客户端的 java.net.ServerSocket 类,同时提供了它们之间建立连接的机制。
使用 Java 在两台计算机之间使用套接字建立 TCP 连接时,一般步骤如下:

  1. 服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。
  2. 服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。
  3. 服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
  4. Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。
  5. 在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket。
    连接建立后,即可通过 I/O 流进行服务器与客户端的通信。每一个 socket 都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送。

服务器应用程序通过使用 java.net.ServerSocket 类以获取一个端口,并且侦听客户端请求。其常用构造方法为:

public ServerSocket(int port, int backlog, InetAddress address) throws IOException
//使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。

创建并绑定服务器套接字后即可侦听客户端请求。常用方法包括:

  1. public int getLocalPort():返回此套接字在其上侦听的端口
  2. public Socket accept() throws IOException:侦听并接受到此套接字的连接。
  3. public void setSoTimeout(int timeout):通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。
  4. public void bind(SocketAddress host, int backlog):将 ServerSocket 绑定到特定地址(IP 地址和端口号)。

java.net.Socket 类代表同时用于客户端和服务器进行互相沟通的套接字。客户端通过实例化获得一个 Socket 对象,而服务通过 accept() 方法的返回值获得一个 Socket 对象。其常用构造方法为:

public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException.
//创建一个套接字并将其连接到指定远程主机上的指定远程端口。

Socket 构造方法返回时,不仅简单地实例化了一个 Socket 对象,而且会尝试连接到指定的服务器和端口。Socket的常用方法如下所示,注意客户端和服务器端都有一个 Socket 对象,所以无论客户端还是服务端都能够调用这些方法:

  1. public void connect(SocketAddress host, int timeout) throws IOException:将此套接字连接到服务器,并指定一个超时值。
  2. public InetAddress getInetAddress():返回套接字连接的地址。
  3. public int getPort():返回此套接字连接到的远程端口。
  4. public int getLocalPort():返回此套接字绑定到的本地端口。
  5. public SocketAddress getRemoteSocketAddress():返回此套接字连接的端点的地址,如果未连接则返回 null
  6. public InputStream getInputStream() throws IOException:返回此套接字的输入流。
  7. public OutputStream getOutputStream() throws IOException:返回此套接字的输出流。
  8. public void close() throws IOException:关闭此套接字。

Java Swing

Swing 是一个为 Java 设计的 GUI 工具包,是 Java 基础类的一部分,包括了图形用户界面(GUI)器件如:文本框,按钮,分隔窗格和表。

Swing 提供许多比 AWT 更好的屏幕显示元素,用纯 Java 写成,同 Java 本身一样可以跨平台运行,支持可更换的面板和主题(各种操作系统默认的特有主题)。作为一种轻量级组件,其缺点则是执行速度较慢,优点就是可以在所有平台上采用统一的行为。

Swing 中主要的组件包括:

  1. JFrame:Java 的 GUI 程序的基本思路是以 JFrame 为基础,它是屏幕上窗口的对象,能够最大化、最小化、关闭。
  2. JPanel:一种轻量级面板容器类,功能是对窗体中具有相同逻辑功能的组件进行组合。可以进行嵌套,可以加入到 JFrame 窗体中。
  3. JLabel:可以显示文本、图像或同时显示二者。可以通过设置垂直和水平对齐方式,指定标签显示区中标签内容在何处对齐。默认情况下,标签在其显示区内垂直居中对齐。默认情况下,只显示文本的标签是开始边对齐;而只显示图像的标签则水平居中对齐。
  4. JTextField:允许编辑单行文本的一个轻量级组件。
  5. JPasswordField:允许输入了一行字像输入框,但隐藏星号(*) 或点创建密码。
  6. JButton :用于创建按钮,并可以定义与按钮的交互操作。

SQLite

SQLite 是一种嵌入式的关系型数据库管理系统(RDBMS),它是一个零配置的、服务器端的、自给自足的、无服务器的 SQL 数据库引擎。SQLite 的设计目标是轻量级、高效、可靠,因此它不像传统的数据库管理系统那样需要一个独立的服务器进程来管理数据,而是将数据库引擎与用户的应用程序直接链接,数据库以文件的形式存储在主机文件系统中。

SQLite 支持大多数常见的 SQL 语法和功能,包括表、索引、触发器、视图等。它是在公共领域发布的开源软件,因此可以免费使用,也可以用于商业用途。由于 SQLite 的轻量级和易于集成的特性,它在嵌入式系统、移动设备应用程序、桌面应用程序和小型 Web 应用程序等方面得到广泛应用。

在本次实验中,需要使用数据库聊天程序的账号与群组信息进行管理。因为所需功能较为轻量化,因此选择了直接在程序中内嵌 SQLite来进行管理。

实验(二)

网络爬虫与网页抓取

网络爬虫(web crawler 或 spider)是一种用来自动浏览万维网的网络机器人。其目的一般为编纂网络索引、网络抓取、验证超链接和 HTML 代码。

网页抓取(web scraping)是一种从网页上获取页面内容的计算机软件技术。通常透过软件使用低级别的超文本传输协议模仿人类的正常访问。

网页抓取和网页索引极其相似,其中网页索引指的是大多数搜索引擎采用使用的机器人或网络爬虫等技术。与此相反,网页抓取更侧重于转换网络上非结构化数据(常见的是 HTML 格式)成为能在一个中央数据库和电子表格中储存和分析的结构化数据。网页抓取也涉及到网络自动化,它利用计算机软件模拟了人的浏览。

HTTP传输协议

超文本传输协议(HTTP)是用于Web上进行通信的协议:它定义Web浏览器如何从Web服务器请求资源以及服务器如何响应。请求和响应消息共享一个通用的基本格式:

  1. 初始行(请求或响应行)
  2. 零个或多个头部行
  3. 空行(CRLF)
  4. 可选消息正文。

对于大多数常见的HTTP事务,协议归结为一系列相对简单的步骤:

  1. 首先,客户端创建到服务器的连接;
  2. 然后客户端通过向服务器发送一行文本来发出请求。这请求行包HTTP方法(比如GET,POST、PUT等),请求URI(类似于URL),以及客户机希望使用的协议版本(比如HTTP/1.0);
  3. 接着,服务器发送响应消息,其初始行由状态线(指示请求是否成功),响应状态码(指示请求是否成功完成的数值),以及推理短语(一种提供状态代码描述的英文消息组成);
  4. 最后一旦服务器将响应返回给客户端,它就会关闭连接。

本实验中将会对指定网页发送 GET 请求,并得到服务器响应得到的 HTML 文件,然后再对 HTML 文件进行解析得到网页上的所有文本和链接,以便进行敏感词的标注与链式连续爬取。

正则表达式

正则表达式,指一种对字符串(包括普通字符(例如,az 之间的字母)和特殊字符(称为“元字符”))操作的逻辑公式,用事先定义好的一些特定字符、及这些特定字符的组合,组成一个用来表达对字符串的过滤逻辑的“规则字符串”。

正则表达式是一种文本模式,该模式描述在搜索文本时要匹配的一个或多个字符串。很多文本编辑器都支持用正则表达式搜索、取代匹配指定格式的字符串。定义了字符串的模式,可以用来搜索、编辑或处理文本。

Java 提供了java.util.regex 包,包含了 PatternMatcher 类,用于处理正则表达式的匹配操作。其中主要包含如下三个类:

  1. Pattern 类:

    • Pattern 对象是一个正则表达式的编译表示。
    • Pattern 类没有公共构造方法。
    • 创建一个 Pattern 对象,需要首先调用其公共静态编译方法。该方法接受一个正则表达式作为它的第一个参数,返回一个Pattern 对象。
  2. Matcher 类:

    • Matcher 对象是对输入字符串进行解释和匹配操作的引擎。
    • Matcher 没有公共构造方法。
    • 需要调用 Pattern 对象的 Matcher 方法来获得一个 Matcher 对象。
  3. PatternSyntaxException

    • PatternSyntaxException 是一个非强制异常类。
    • 表示一个正则表达式模式中的语法错误。

Graphviz

Graphviz(Graph Visualization Software)是一个开源的图形可视化软件,它能够从简单的文本文件描述中生成复杂的图形和网络。它使用一种名为 DOT 的描述语言来定义图形,使得用户可以专注于内容而非布局和设计,提供了一种快速、灵活且高效的方式来创建和可视化复杂的图形和网络。

Graphviz 的主要特点和用途包括:

  1. 灵活的渲染功能:Graphviz 可以生成多种格式的图形文件,包括 raster 和 vector 格式,如 PNG、PDF、SVG 等。
  2. 自动布局:Graphviz 的一个主要特点是其自动布局能力。用户只需定义图的元素和它们之间的关系,Graphviz 就能够自动计算出合适的布局。
  3. 扩展性:Graphviz 提供了多种工具和库,可以用于各种应用,如 Web 服务、生成报告,或与其他软件的集成。

实验具体设计实现

实验(一)

类设计

基础类:

  • BackgroundPanel.javaFramePosition.javaJTab.java:用于建立与处理用户界面。
  • User.javaGroup.java:分别定义了用户与群组的信息。

Client端:

  • Client 类:定义了客户端的 Socket,进行客户端的初始化,建立 MessageThread 线程并开始对服务器进行监听。
  • MessageThread 类:建立消息处理线程,用于处理监听到的服务器的回应。
  • StartWindow:登录用户界面。

Server 端:

  • Servermethod 类:Server 类的基类,实现了服务器的包括开启关闭在内的基本操作。
  • Server 类:定义了服务端的 Socket,进行服务端的初始化,并对每一个连接的客户端创建 ClientThread 进行监听。
  • ClientThread 类:用于处理监听到的客户端的请求。
  • ServerUI:服务器端操作界面。

数据库:

  • DBConnection.java:初始化数据库连接。
  • ImportFactory.java:用于多个数据库的管理,从中可以得到具体数据库的引用。
  • UserImport.java:数据库具体操作类 UserImportSqlite.java 的接口,定义了所有数据库操作的虚函数。
  • UserImportSqlite.java:具体实现了所有数据库操作。

总体流程概述

  1. 开启服务器 Server,初始化服务端 socket 并且绑定指定端口,循环监听等待客户端的连接。
  2. 开启客户端 Client,客户端绑定指定端口并尝试进行服务器连接;
  3. 服务器接收到连接请求,为客户端新建 ClientThread 线程进行循环监听。
  4. 用户在客户端的可视化界面中进行登录请求,将请求格式化后传输给服务器,服务器查询数据库检查是否匹配,若匹配则成功回应,并返回用户的好友与群组。
  5. 用户在可视化界面中选择交流对象并进行信息的发送请求,将请求格式化后传输给服务器,服务器进行处理后将回应发送给交流对象。

界面设计

1.登录界面

  • 客户端需要登录界面,用户可以输入账号信息进行登录;
  • 提供登录按钮,用于提交登录信息;
  • 提供注册按钮,用于新注册用户。

2.主界面

  • 聊天室主界面应具有聊天记录显示区域,用于显示群聊消息的历史记录;
  • 提供发送消息的输入框,用户可以在此输入消息内容;
  • 提供发送按钮,用于发送消息

3.副界面

  • 显示在线好友列表,包括好友的昵称或用户名;
  • 显示在线成员列表,包括成员的昵称或用户名;
  • 提供私聊按钮,用户可以点击按钮选择私聊对象;
  • 提供添加和删除好友按钮,用户可以点击按钮选择好友对象;
  • 提供创建群聊按钮,用户可以创建新的群聊并添加成员;
  • 提供退出群聊功能,允许修改群聊的成员列表。
  • 提供广播功能,允许用户向所有在线用户进行消息广播。

4.弹出窗口

  • 当用户选择与某个好友私聊时,弹出窗口显示与该好友的私聊消息记录;
  • 当服务器发送系统消息时,弹出窗口显示系统消息的内容;
  • 当用户选择群聊时,弹出窗口显示该群的消息记录。

5.通知和提醒

  • 当有新的群聊消息或私聊消息时,通过弹窗或其他方式提醒用户;
  • 当服务器发送系统消息或其他重要通知时,通过弹窗或在聊天界面中显示;
  • 当被服务器下线时,通过弹窗或其他方式提醒用户;
  • 当被邀请加入群聊时,通过弹窗或其他方式提醒用户;
  • 当被添加或删除好友时,通过弹窗或其他方式提醒用户;

核心问题设计

消息处理机制

本实验中采用了C/S方式,所以实际上不存在客户端与客户端之间的直接通信,若两者通信必需经过服务器端来中转。具体的信息处理方式需要借助了报文思想,即规定了报文结构:使用头部来指示信息的具体类型,正文部分为通信参数,然后将报文以字符串形式通过线程间的输入输出流进行传递。具体地,报文信息分为两个类型:COMMAND 类型与 MESSAGE 类型。前者用于描述加群,退群,加好友等对用户数据对象进行的操作,后者表示用户间通信时的信息,用于实现用户的聊天。

MessageThread.java 中代码节选如下:

public void run() {
    String message;
    while (isConnected) {
        try {
            message = reader.readLine();
            if (message == null) continue;
            System.out.println(message);

            StringTokenizer stringTokenizer = new StringTokenizer(message, "@");
            String type = stringTokenizer.nextToken();// 命令
            if (type.equals("COMMAND")) {
                solveCommond(stringTokenizer);
            } else if (type.equals("MESSAGE")) {
                solveMessage(stringTokenizer);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
C/S工作模式

为了实现可靠的通信传输,本实验采用了 C/S 工作模式,并使用了传统的 BIO 多线程模型。

在服务端程序中,建立了 ServerUI 类来负责服务器的前端设计,而在 Server 类中主要负责处理各种按钮的交互事件和与客户端之间的信息联系。在连接犯过错中,首先服务器端自身先运行一个线程,用来处理来自不同客户端的连接请求。这一部分编写一个 ServerThread 类来进行处理。对于每一个客户端还需要单独处理与之的消息交互,所以需要单独编写 ClientThread 类来进行处理,实现了上述多线程模式。

ServerThread .java 中代码节选如下:

class ServerThread extends Thread {
        private ServerSocket serverSocket;
        private int max;// 人数上限
        private Boolean runningFlag = false;

        // 服务器线程的构造方法
        public ServerThread(ServerSocket serverSocket, int max) {
            this.serverSocket = serverSocket;
            this.max = max;
            this.runningFlag = true;
        }

        public void serverThreadClose() throws IOException{
            this.runningFlag = false;
            serverSocket.close();
        }

        public void run() {
            while (isStart && runningFlag) {// 不停的等待客户端的链接
                try {
                    Socket socket = serverSocket.accept();
                    BufferedReader r = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    PrintWriter w = new PrintWriter(socket.getOutputStream());
                    new Thread() {
                        public void run() {
                            try {//负责客户端的注册与登录操作
                                boolean isDone = false;
                                while (!isDone) {
                                    // 接收客户端的基本用户信息
                                    //...
                                    } else if (command.equals("LOGIN")) {
                                        //...
                                        } else if (!onlineUsers.containsKey(account)) {
                                            //...
                                        } else if (onlineUsers.containsKey(account)) {
                                            //...
                                        } else {
                                            //...
                                        }
                                    }
                                }
                            }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
用户与群组管理机制

首先定义了 User 与 Group 类,用于建立对应的实例。

Group 类表示了一个具体的群,User 类则表示的是一个具体的用户。在服务器运行过程中使用了 Hashmap 进行了总体管理,groups 管理着所有的群,而 onLineUsers 管理着在线用户。对于个人的群组好友管理基于上述两个 Hashmap 建立了两个 DefaultListModel 的管理,friendListModel 记录着好友列表,而 groupListModel 记录着群组列表。

将把加群,加好友,删好友等操作看作是一种特殊的消息后,基于上述消息处理机制处理,即可实现对于上述数据结构的变更处理。

User 与 Group 类的定义如下:

public class User {
	private String account;
	private String password;
	private String username;

	public User(String account, String password, String username) {
		this.account = account;
		this.password = password;
		this.username = username;
	}
	public User(String account, String username) {
		this.account = account;
		this.username = username;
	}
}
public class Group {
    private ArrayList<User> members;
    private String name;
    private String account;

    public Group(String name, String account) {
        this.name = name;
        this.account = account;
        this.members = new ArrayList<>();
    }
    public Boolean haveMember(User user) {
        for (User member: members) {
            if (user.getAccount().equals(member.getAccount())) {
                return true;
            }
        }
        return false;
    }
}
数据库处理机制

在本次实验中,需要使用数据库聊天程序的账号与群组信息进行管理。因为所需功能较为轻量化,因此选择了直接在程序中内嵌 SQLite 来进行管理。

数据库中主要包含如下四张表:

  • tbl_user:存储用户信息,包括编号、昵称、密码;
  • friends:储存好友信息;
  • groupMember:储存每个群组中的成员;
  • groups:存储群组信息,包括群号、群名。

同时为了便于查询操作,并保证安全性,建立了三张视图:

  • v_friends:用于查询特定用户的好友;
  • v_groupMember:用于查询某个特定群组的成员;
  • v_groups:用于查询某个特定用户所在群组。

具体实现时,首先在服务器启动时调用 DBConnection.java 类进行初始化数据库连接,根据设置文件与内嵌 SQLite 文件进行连接。之后在程序中需要进行数据库操作时将调用 UserImportSqlite 类中的接口函数即可。

接口 UserImport.java 定义如下:

public interface UserImport {
    public User login(String userId, String userPassword) throws SQLException;
    public Boolean register(User user) throws SQLException;
    public ArrayList<User> getFriendList(String user_id) throws SQLException;
    public ArrayList<Group> getGroupList(String user_id) throws SQLException;
    public Boolean addFriend(String userId1, String userId2) throws SQLException;
    public Boolean deleteFriend(String userId1, String userId2) throws SQLException;
    public Group createGroup(Group group) throws SQLException;
    public Boolean deleteGroup(String groupId) throws SQLException;
    public Boolean addGroupMember(String userId, String GroupId) throws SQLException;
    public Boolean deleteGroupMember(String userId, String GroupId) throws SQLException;
}

实验(二)

类设计

主要类:

  • HtmlHandler.java:实现了 html 文件的分析,使用正则表达式匹配 html 中的文本与链接。
  • p1.java:启动软件初始界面,读入待爬取的源网页与最大爬取数量。
  • p2.java:可交互的敏感词分析界面;
  • Spider.java:对网页进行爬取的线程类。
  • Main.java:启动。

用户界面组件:

  • GridManager.java:布局组件。
  • MyProgressBar.java:进度条组件。

图形可视化:

  • Graph.java:建立可视化图形;
  • GraphViz.java:初始化配置,定义接口函数。

总体流程设计

  1. 用户启动 Main.java,进入软件初始界面,读入待爬取的源网页与最大爬取数量。
  2. 进入可交互的敏感词分析界面,用户点击开始爬取后建立 Spider 线程进行网页的链式爬取。在爬取过程中用户可随时点击停止爬取按钮终止线程运行。
  3. 用户终止运行后,可选择是否建立可视化图形。
  4. 爬取完成后,得到的所有 html 文件与文本将会以列表形式显示可交互界面中,用户可自由选择单独查看其中的哪个文件并进行分析。
  5. 用户在可交互界面中直接输入待分析的敏感词,也可以选择文本文件并将文件中的内容导入到敏感词中。
  6. 用户选择了待分析文本后,点击提取敏感词即可在可交互界面中标注出文本中所有敏感词,同时返回敏感词统计数据。
  7. 用户可在可交互界面中打开可视化图形。

界面设计

初始界面:

  • 读入待爬取的源网页
  • 读入最大爬取数量

可交互界面:

  • 左侧为所有已爬取的网页的列表。
  • 中间为多页面文本编辑界面,可显示列表中选中的网页的 html 源文件与分析得到的文本;可在其中进行敏感词的输入。
  • 下方为操作按钮,包括开始爬取、停止爬取、敏感词导入、敏感词分析、显示可视化图形。

核心问题设计

html 爬取与分析

爬取 html 文件时,考虑建立当前程序与对应网页的 URL 连接,并不断地将返回的 html 以字符流的形式按照 UTF-8 格式进行读入与存储,从而得到以字符串形式返回的 html 文件。然后使用正则表达式进行 html 源文件的分析,从而提取出其中的文本与链接。

提取文本时:考虑匹配 html 文件中所有特殊含义的标签,并将它们从 html 字符串中删除即可得到所有文本。

提取链接时:考虑匹配 html 文件中所有超链接标签 <a herf=> </a>,然后将标签的链接对象 herf= 进行提取,并返回得到的所有 url。

定义的正则表达式如下:

static String regExHtml = "<[^>]+>";        //匹配标签
static String regExScript = "<script[^>]*?>[\\s\\S]*?<\\/script>";        //匹配script标签
static String regExStyle = "<style[^>]*?>[\\s\\S]*?<\\/style>";        //匹配style标签
static String regExA = "<a[^>]+href=[\"'](.*?)[\"']";        //匹配 a 标签
static String regExSpace = "[\\s]{2,}";    //匹配连续空格或回车等
static String regExImg = "&[\\S]*?;+";    //匹配转义符

文本提取:

public static String getText(String str) {
    var matcher = patternScript.matcher(str);
    str = matcher.replaceAll("");        //去掉普通标签
    matcher = patternStyle.matcher(str);
    str = matcher.replaceAll("");        //去掉script标签
    matcher = patternHtml.matcher(str);
    str = matcher.replaceAll("");        //去掉style标签
    matcher = patternSpace.matcher(str);
    str = matcher.replaceAll("\n");    //连续回车或空格变一个
    matcher = patternImg.matcher(str);
    str = matcher.replaceAll("");        //去掉转义符
    return str;        //返回文本
}

链接提取:

public static ArrayList<String> getNextUrl(String str) {
    ArrayList<String> urls = new ArrayList<>();

    var matcherA = patternA.matcher(str);
    while (matcherA.find()) {
        var strMatcherA = matcherA.group();
        int position = strMatcherA.indexOf("href=");
        var url = strMatcherA.substring(position + 5);
        if (url.startsWith("\"") || url.startsWith("'")) url = url.substring(1, url.length() - 1);
        if (url.endsWith("\"") || url.endsWith("'")) url = url.substring(0, url.length() - 1);
        if (url.isEmpty()) continue;

        System.out.println("Found: " + url);
        if (urls.contains(url)) continue;
        urls.add(url);
    }
    return urls;
}
多线程爬取

为了实现用户可随时终止爬取的功能,考虑对爬取功能单独建立线程,在线程中按照链式枚举所有待爬取文件并循环逐个爬取。此时仅需修改线程中的信号量,即可实终止爬取功能,并返回当前已经爬取的所有网页的信息。

在爬取线程中,按照链式进行网页爬取时使用了广度优先搜索的思想,即按照待爬取网页与源网页的跳转次数为顺序进行爬取,使得爬取的内容有了一定的广度与相关性,利于进行数据分析,而不会像按照深度优先的策略一样一直进行链式爬取导致爬取的内容与源网页的内容失去联系。

代码节选如下:

class Spider extends Thread {
        String url;    //网页链接
        MyProgressBar mpb;    //进度条
        public Spider(JFrame fa, String str) {
            mpb = new MyProgressBar(fa, "Running");
            url = str;
            //GUI 操作...
        }
        private void crawl(String nowUrl) {
            if (nowUrl.isEmpty()) {    //判断网址是否正常
                return;
            }
            mpb.setText("爬取" + nowUrl + "中...");    //设置进度条界面标题
            mpb.setVisible(true);        //显示进度条
            var html = HtmlHandler.getHtml(nowUrl);    //开始爬取
            mpb.dispose();    //关闭进度条
            if (!html.isEmpty()) {    //若爬取正常
                ++ nowUrlCount;
                var text = HtmlHandler.getText(html);    //匹配网页文本
                urlList.add(nowUrl);
                htmlList.add(html);
                textList.add(text);
                succeedHashMap.put(nowUrl, nowUrlCount);
                ArrayList<String> nextUrls = HtmlHandler.getNextUrl(html);
                nextList.put(nowUrl, nextUrls);
            }
        }
        public void run() {
            LinkedList<String> queue = new LinkedList<String>();
            queue.add(url);
            while (runningFlag && !queue.isEmpty() && nowUrlCount < maxUrlCount) {
                String front = queue.removeFirst();
                if (visitedHashMap.containsKey(front)) continue;
                crawl(front);
                visitedHashMap.put(front, true);
                if (!nextList.containsKey(front)) continue;
                for (var next: nextList.get(front)) {
                    if (!visitedHashMap.containsKey(next)) {
                        queue.add(next);
                    }
                }
            }
            //GUI 操作...
        }
}
文本敏感词分析

将读入敏感词界面中的所有敏感词提取出来,在当前交互界面中显示的文本中进行逐个匹配,并高亮显示即可。

代码节选如下:

public void showSensword() {
        String wordtext = wordArea.getText();
        if (wordtext.isEmpty()) return ;
        String[] splitLines = wordtext.split("\\n");
        wordList.clear();
        wordNum.clear();
        for (String line : splitLines) {
            wordList.add(line);    //添加到记录中
            wordNum.add(0);        //设置对应的初始值
        }

        var hg = textArea.getHighlighter();    //设置文本框的高亮显示
        hg.removeAllHighlights();    //清除之前的高亮显示记录
        var text = textArea.getText();    //得到文本框的文本
        var painter = new DefaultHighlighter.DefaultHighlightPainter(Color.PINK);
        if(wordList.isEmpty()) return ;

        int senwordCount = 0;
        for (var str : wordList) {    //匹配其中的每一个敏感词
            var index = 0;
            while ((index = text.indexOf(str, index)) >= 0) {
                try {
                    hg.addHighlight(index, index + str.length(), painter);    //高亮显示匹配到的词语
                    index += str.length();    //更新匹配条件继续匹配
                    ++ senwordCount;
                } catch (BadLocationException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        JOptionPane.showMessageDialog(null, "共检测出 " + senwordCount + " 个敏感词",
                "提取结束", JOptionPane.PLAIN_MESSAGE);
}
可视化图形

可视化图形的建立使用了第三方库 GraphViz,调用了其中的 createDotGraph 方法。该方法接受一个以 Dot 语言描述需要生成的图的字符串,并将生成的可视化图形保存在设置中的指定目录中。

代码节选如下:

if (JOptionPane.showConfirmDialog(null, "共爬取了 " +
    nowUrlCount + " 个网页,是否生成可视化有向图网?", "爬取完毕", JOptionPane.YES_NO_OPTION)
    == JOptionPane.YES_OPTION) {
    try {
        StringBuilder dotFormat= new StringBuilder();
        for (var s: urlList) {
            // System.out.println(s + ":");
            dotFormat.append(succeedHashMap.get(s)).append(";");
            for (var next: nextList.get(s)) {
                if (succeedHashMap.containsKey(next)) {
                    System.out.println("    " + next);
                    dotFormat.append(succeedHashMap.get(s)).append("->").append(succeedHashMap.get(next)).append(";");
                }
            }
        }
        // System.out.println(dotFormat);
        Graph.createDotGraph(dotFormat.toString(), "DotGraph");

        buttonShowGraph.setEnabled(true);
        JOptionPane.showMessageDialog(null, "已在当前目录下生成可视化有向图网",
                "成功", JOptionPane.PLAIN_MESSAGE);
    } catch (Exception e) {
        e.printStackTrace();
        buttonShowGraph.setEnabled(false);
        JOptionPane.showMessageDialog(null, "生成可视化有向图网失败",
            "错误", JOptionPane.WARNING_MESSAGE);
    }
}

运行结果与测试分析

实验(一)

界面展示

服务器端界面:

客户端登录界面:

客户端注册界面:

登录后显示聊天室侧边栏:

聊天界面:

聊天功能展示

登录账号优香(10001)与诺亚(10002)后,优香(10001)向诺亚(10002)发送消息:

优香(10001)在群聊我们仨(4)中发送消息:

优香(10001)在群聊不带诺亚玩(2)中发送消息:

优香(10001)在用户广播中发送消息:

好友、群组操作

登录账号优香(10001)与小雪(10004)后,优香(10001)向小雪(10004)添加好友:

优香(10004)建立群聊千年研讨会(5):

优香(10004)邀请小雪(10004)加入群聊千年研讨会(5):

优香(10001)删除好友小雪(10004):

小雪(10004)退出千年研讨会(5):

优香(10001)退出千年研讨会(5):

服务器操作

登录账号优香(10001)与小雪(10004)后,服务器广播消息:

服务器强制下线用户小雪(10004)

服务器停止:

实验(二)

初始界面

可交互界面

在输入最大爬取数量 5,爬取网站 https://www.cnblogs.com/ 后,进入可交互界面:

开始爬取:

爬取结束:

可视化图形

显示可视化图形:

敏感词分析:

读入敏感词 PythonAI

选中网页 https://news.cnblogs.com 进行分析:

完整代码

见 Github 项目:https://github.com/Luckyblock233/MomoTalkFullVersionhttps://github.com/Luckyblock233/Spider

总结与展望

GPT 生成的。

本次基于网络与 Java 的应用基础实践令我受益匪浅。

在本次实验中,我首先学习了如何使用 Java 集成开发环境编写网络程序,掌握了使用 Java 编程语言进行 GUI 设计的基本知识,了解了如何建立 Socket 连接、监听端口、接受和发送数据等基本操作,从而实现了客户端和服务器端的通信功能。我深入理解了客户/服务器应用的工作方式,我学习了网络中进程之间通信的原理和实现方法,掌握了 Socket 编程的基本技巧,提升了实际编程能力,为今后更复杂的网络应用开发打下了基础。

在本次实验中,我感到面对纲领性的课程的学习,不仅仅要把书本上,课堂上的内容掌握,理解许多抽象的知识,还应该努力去了解其实际如何发挥作用的,在实践中去学习会使印象更加深刻,且对以后的学习很有意义。

了解和尝试计算机网络相关编程的工具是非常有必要的,例如相关的软件、相关的库,这样的学习可以帮助我拓展知识面,虽然无法全面地掌握,但是有了粗略的了解之后,相关知识即可实际需要的时候调动出来。本次实验就加强了我的编程能力,让我拓展学习了许多课程相关的内容。

最后,我通过本次实验发现计算机网络是一个有很多细节的研究方向,既需要全局的了解,又需要局部的精通,其中不乏很多可以继续提升的地方。这次实验让我收获颇丰,不仅学到了理论知识,还通过实际操作加深了对网络编程的理解和掌握。希望今后能够继续深入学习和应用这方面的知识,为未来的工作和学习打下坚实的基础。

参考

原理参考:

语言实现参考:

Java.Swing 用户界面参考:

可视化图形参考:

配置环境参考:

数据库设计参考:

实验报告格式:

杂项:

写在最后

妈的终于他妈的写完了我还想说:

对于体操服优香,我的评价是四个字:好有感觉。我主要想注重于两点,来阐述我对于体操服优香的拙见:第一,我非常喜欢优香。优香的立绘虽然把优香作为好母亲的一面展现了出来(安产型的臀部)。但是她这个头发,尤其是双马尾,看起来有点奇怪。但是这个羁绊剧情里的优香,马尾非常的自然,看上去比较长,真的好棒,好有感觉。这个泛红的脸颊,迷离的眼神,和这个袖口与手套之间露出的白皙手腕,我就不多说了。第二,我非常喜欢体操服。这是在很久很久之前,在认识优香之前,完完全全的xp使然。然而优香她不仅穿体操服,她还扎单马尾,她还穿外套,她竟然还不好好穿外套,她甚至在脸上贴星星(真的好可爱)。(倒吸一口凉气)我的妈呀,这已经到了仅仅是看一眼都能让人癫狂的程度。然而体操服优香并不实装,她真的只是给你看一眼,哈哈。与其说体操服优香让我很有感觉,不如说体操服优香就是为了我的xp量身定做的。抛开这一切因素,只看性格,优香也是数一数二的好女孩:公私分明,精明能干;但是遇到不擅长的事情也会变得呆呆的。我想和优香一起养一个爱丽丝当女儿,所以想在这里问一下大家,要买怎样的枕头才能做这样的梦呢?优香是越看越可爱的,大家可以不必拘束于这机会上的小粗腿优香,大胆的发现这个又呆又努力的女孩真正的可爱之处。优香有种适合当新婚的贤妻良母的感觉,非常的可靠和可爱。她没有未花那样的梦幻,也没有爱丽丝那样的纯真,不会像小绿那样耍着稚嫩含蓄的小心机,也不会像圣娅那样高雅深晦的说着难懂的谜语,朴素但又十分耐看。在千年她是后辈眼里严肃严谨严厉的前辈。在游戏部眼里她又是管教严格的妈妈兼大魔王。和老师工作时,她是个会啰嗦说教,严于律己的工作狂。这样的优香在运动会换上体操服后,又能变得清纯开朗和阳光,有着青春女生特有的可爱和少女感。递给老师自己喝过的水后才会意识到害羞的单纯,加上怀有情愫半推半就的傲娇,这是何等直击内心的可爱!结婚!虽然优香一直给人可靠成熟的感觉,但遇到自己不擅长的事,还是会不知所措,笨拙又慌张,一眼就能被看出想法的优香很可爱!和老师在一起的时候,声音比起在后辈跟其他学生面前,音调更高和温柔的优香也很可爱!(小雪在优香诺亚老师面前吐槽优香的时候知道的,小雪可爱捏)一边娇嗔老师总是给自己添麻烦,总是需要自己帮助和督促工作。一边有些得意地“抱怨”着【老师根本就离不开我!】的优香超级可爱!结婚!结婚!还是踏码的结婚!想到结婚后有优香在背后辅佐,计算着生活的开销,心里就感到非常踏实。和优香结婚的话,女友,妻子,母三个愿望一次满足满足,实在是人生的至福。啊,好想给优香戴上结婚戒指,一边听着优香说【老师又在这种没用的地方乱花钱了】,一边看着她把戒指当作一生宝物,一脸幸福的表情。

posted @ 2024-05-25 01:45  Luckyblock  阅读(275)  评论(2编辑  收藏  举报