「雜文」應用基礎實踐一(網路+Java)實驗報告

Luckyblock發表於2024-05-25

目錄
  • 寫在前面
  • 實驗目的與要求
    • 實驗(一)用JavaSocket程式設計開發聊天室
    • 實驗(二)用JavaURL程式設計爬取並分析網頁敏感詞
  • 實驗環境
  • 實驗原理
    • 實驗(一)
      • Socket程式設計介面
      • Java 中的 Socket 類
      • Java Swing
      • SQLite
    • 實驗(二)
      • 網路爬蟲與網頁抓取
      • HTTP傳輸協議
      • 正規表示式
      • Graphviz
  • 實驗具體設計實現
    • 實驗(一)
      • 類設計
      • 總體流程概述
      • 介面設計
      • 核心問題設計
        • 訊息處理機制
        • C/S工作模式
        • 使用者與群組管理機制
        • 資料庫處理機制
    • 實驗(二)
      • 類設計
      • 總體流程設計
      • 介面設計
      • 核心問題設計
        • html 爬取與分析
        • 多執行緒爬取
        • 文字敏感詞分析
        • 視覺化圖形
  • 執行結果與測試分析
    • 實驗(一)
      • 介面展示
      • 聊天功能展示
      • 好友、群組操作
      • 伺服器操作
    • 實驗(二)
      • 初始介面
      • 可互動介面
      • 視覺化圖形
      • 敏感詞分析:
  • 完整程式碼
  • 總結與展望
  • 參考
  • 寫在最後

寫在前面

詐屍了,媽的我不想上學了事兒太幾把多了好相似啊啊啊啊啊啊啊啊啊啊啊啊啊啊——但是似了實驗報告還是要寫媽的

理應來說這種腦癱實驗和殺軟報告就是怎麼水怎麼來——但是人腦維護大工程太有趣了一開始寫就覺得實在是太有趣了就停不下來了媽的,於是就有了這麼一拖看著像超級無敵大大大卷王才會搞出來的東西——其實只是完全是停不下來了導致的、、、

老師沒有規定實驗報告格式要求,於是就用了神器 Keldos-Li_typora-latex-theme 並順手丟上來一份造福後人。

兩個實驗都是基於基於基於學長的學長的學長的程式碼的學長的學長的程式碼的學長的程式碼大改的。在這裡首先感謝三位學長的傾情奉獻:2019 lqy,2020 sfj,2021 kawaii liqing

感覺直接拿過來水不太好,於是在學長的基礎上為聊天室進行了若干修正並內嵌了 SQLite 以維護離線使用者資料,為網路爬蟲新增了鏈式網頁爬取、可互動的敏感詞分析介面與視覺化爬取有向圖分析。經過四次大型迭代,聊天室的原始碼總大小已經達到了驚人的 130kb,網路爬蟲程式原始碼也有近 40kb 的大小。如果您覺得您沒有人腦維護如此巨型的屎山(雖然我還覺得挺可讀的)的能力,以便在老師的追問下證明這坨屎確實是自己寫的——那麼不建議直接使用這份程式碼。

「雜文」應用基礎實踐一(網路+Java)實驗報告

實驗目的與要求

實驗(一)用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

配置環境參考:

  • 解決IDEA 無法下載sqlite驅動-CSDN部落格
  • java.lang.classnotfoundexception org.sqlite.jdbc intellij-掘金 (juejin.cn)
  • java.lang.NoClassDefFoundError: org.slf4j.LoggerFactory - Stack Overflow
  • youtrack.jetbrains.com/issue/IDEA-119743/ClassNotFoundException-org.sqlite.jdbc
  • Download _ Graphviz
  • How to call GraphViz from java - Stack Overflow
  • 在Java環境中使用GraphViz繪圖_graphvia-CSDN部落格
  • Java:使用第三方庫GraphViz畫圖TEST - fanghuiX - 部落格園

實驗原理

實驗(一)

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 專案:學期結束後上傳

總結與展望

GPT 生成的。

本次基於網路與 Java 的應用基礎實踐令我受益匪淺。

在本次實驗中,我首先學習瞭如何使用 Java 整合開發環境編寫網路程式,掌握了使用 Java 程式語言進行 GUI 設計的基本知識,瞭解瞭如何建立 Socket 連線、監聽埠、接受和傳送資料等基本操作,從而實現了客戶端和伺服器端的通訊功能。我深入理解了客戶/伺服器應用的工作方式,我學習了網路中程序之間通訊的原理和實現方法,掌握了 Socket 程式設計的基本技巧,提升了實際程式設計能力,為今後更復雜的網路應用開發打下了基礎。

在本次實驗中,我感到面對綱領性的課程的學習,不僅僅要把書本上,課堂上的內容掌握,理解許多抽象的知識,還應該努力去了解其實際如何發揮作用的,在實踐中去學習會使印象更加深刻,且對以後的學習很有意義。

瞭解和嘗試計算機網路相關程式設計的工具是非常有必要的,例如相關的軟體、相關的庫,這樣的學習可以幫助我擴充知識面,雖然無法全面地掌握,但是有了粗略的瞭解之後,相關知識即可實際需要的時候調動出來。本次實驗就加強了我的程式設計能力,讓我擴充學習了許多課程相關的內容。

最後,我透過本次實驗發現計算機網路是一個有很多細節的研究方向,既需要全域性的瞭解,又需要區域性的精通,其中不乏很多可以繼續提升的地方。這次實驗讓我收穫頗豐,不僅學到了理論知識,還透過實際操作加深了對網路程式設計的理解和掌握。希望今後能夠繼續深入學習和應用這方面的知識,為未來的工作和學習打下堅實的基礎。

參考

原理參考:

  • 正規表示式 - 維基百科,自由的百科全書
  • 前言 · 爬取你要的資料:爬蟲技術
  • SQLite - 維基百科,自由的百科全書 (wikipedia.org)
  • 正規表示式 – 語法 _ 菜鳥教程
  • HTML 連結 _ 菜鳥教程
  • HTML 連結

語言實現參考:

  • Java網路程式設計的基礎:計算機網路 | 二哥的Java進階之路 (javabetter.cn)
  • Java 網路程式設計 | 菜鳥教程 (runoob.com)
  • java使用Socket類接收和傳送資料_java socket 接收環保資料-CSDN部落格
  • Socket類的getInputStream方法與getOutputStream方法的使用_socket.getinputstream()-CSDN部落格
  • java 判斷url 地址是否是圖片路徑_mob64ca12e3a791的技術部落格_51CTO部落格
  • Java正規表示式之Pattern和Matcher-CSDN部落格
  • Java 多執行緒 終止執行緒的4中方式_list 中多執行緒一個ok則終止-CSDN部落格
  • Java中如何檢測HashMap中是否存在指定Key呢?_java判斷hashmap中是否包含某鍵值-CSDN部落格
  • Java 去掉協議和IP地址_mob649e81643021的技術部落格_51CTO部落格
  • 去除url的協議部分 php去除http(s)___ (正則) (parse_url)_php 去掉字串連結前面的http-CSDN部落格
  • 從JavaScript字串中刪除http或https _ 碼農家園
  • 【Java系列】List資料去重的五種有效方法-阿里雲開發者社群
  • Java LinkedList _ 菜鳥教程
  • java設定JLabel字型字號顏色_jl.setfont-CSDN部落格
  • JAVA 字串三個常用操作(查詢子串、擷取字串、分割字串)_java字串裡面查詢一個子字串的開始結束索引-CSDN部落格
  • 利用Java正規表示式提取HTML中的連結_正規表示式 提取 href-CSDN部落格
  • 通用正則, 抓取a標籤href屬性_正則獲取a標籤href-CSDN部落格
  • java socket的正確關閉姿勢_java 怎麼關閉未連線的socket-CSDN部落格

Java.Swing 使用者介面參考:

  • 給jlable新增圖片,並使圖片適應jlable大小_imageicon icon = new imageicon(jlabeldemo.jpg);-CSDN部落格
  • Java的Swing在介面的JPanel皮膚中中新增圖片_jpanel怎麼新增jif圖片-CSDN部落格
  • java 中JLabel中的內容垂直居中和水平居中問題_jlabel文字居中-CSDN部落格
  • java彈出圖片_Java對話方塊上顯示圖片-CSDN部落格
  • Java Swing 對話方塊JOptionPane的基本使用_joptionpane用法-CSDN部落格

視覺化圖形參考:

  • About _ Graphviz
  • 一小時實踐入門 Graphviz - 知乎
  • 資料視覺化(三)基於 Graphviz 實現程式化繪圖 - 知乎
  • How to call GraphViz from java - Stack Overflow
  • 在Java環境中使用GraphViz繪圖_graphvia-CSDN部落格
  • Java:使用第三方庫GraphViz畫圖TEST - fanghuiX - 部落格園

配置環境參考:

  • 解決IDEA 無法下載sqlite驅動-CSDN部落格
  • java.lang.classnotfoundexception org.sqlite.jdbc intellij-掘金 (juejin.cn)
  • java.lang.NoClassDefFoundError: org.slf4j.LoggerFactory - Stack Overflow
  • youtrack.jetbrains.com/issue/IDEA-119743/ClassNotFoundException-org.sqlite.jdbc
  • Download _ Graphviz
  • How to call GraphViz from java - Stack Overflow
  • 在Java環境中使用GraphViz繪圖_graphvia-CSDN部落格
  • Java:使用第三方庫GraphViz畫圖TEST - fanghuiX - 部落格園

資料庫設計參考:

實驗報告格式:

  • Keldos-Li_typora-latex-theme_ 將Typora偽裝成LaTeX的中文樣式主題,本科生輕量級課程論文撰寫的好幫手。This is a theme disguising Typora into Chinese LaTeX style

雜項:

  • Windows下如何檢視某個埠被誰佔用 | 菜鳥教程 (runoob.com)

寫在最後

媽的終於他媽的寫完了我還想說:

對於體操服優香,我的評價是四個字:好有感覺。我主要想注重於兩點,來闡述我對於體操服優香的拙見:第一,我非常喜歡優香。優香的立繪雖然把優香作為好母親的一面展現了出來(安產型的臀部)。但是她這個頭髮,尤其是雙馬尾,看起來有點奇怪。但是這個羈絆劇情裡的優香,馬尾非常的自然,看上去比較長,真的好棒,好有感覺。這個泛紅的臉頰,迷離的眼神,和這個袖口與手套之間露出的白皙手腕,我就不多說了。第二,我非常喜歡體操服。這是在很久很久之前,在認識優香之前,完完全全的xp使然。然而優香她不僅穿體操服,她還扎單馬尾,她還穿外套,她竟然還不好好穿外套,她甚至在臉上貼星星(真的好可愛)。(倒吸一口涼氣)我的媽呀,這已經到了僅僅是看一眼都能讓人癲狂的程度。然而體操服優香並不實裝,她真的只是給你看一眼,哈哈。與其說體操服優香讓我很有感覺,不如說體操服優香就是為了我的xp量身定做的。拋開這一切因素,只看性格,優香也是數一數二的好女孩:公私分明,精明能幹;但是遇到不擅長的事情也會變得呆呆的。我想和優香一起養一個愛麗絲當女兒,所以想在這裡問一下大家,要買怎樣的枕頭才能做這樣的夢呢?優香是越看越可愛的,大家可以不必拘束於這機會上的小粗腿優香,大膽的發現這個又呆又努力的女孩真正的可愛之處。優香有種適合當新婚的賢妻良母的感覺,非常的可靠和可愛。她沒有未花那樣的夢幻,也沒有愛麗絲那樣的純真,不會像小綠那樣耍著稚嫩含蓄的小心機,也不會像聖婭那樣高雅深晦的說著難懂的謎語,樸素但又十分耐看。在千年她是後輩眼裡嚴肅嚴謹嚴厲的前輩。在遊戲部眼裡她又是管教嚴格的媽媽兼大魔王。和老師工作時,她是個會囉嗦說教,嚴於律己的工作狂。這樣的優香在運動會換上體操服後,又能變得清純開朗和陽光,有著青春女生特有的可愛和少女感。遞給老師自己喝過的水後才會意識到害羞的單純,加上懷有情愫半推半就的傲嬌,這是何等直擊內心的可愛!結婚!雖然優香一直給人可靠成熟的感覺,但遇到自己不擅長的事,還是會不知所措,笨拙又慌張,一眼就能被看出想法的優香很可愛!和老師在一起的時候,聲音比起在後輩跟其他學生面前,音調更高和溫柔的優香也很可愛!(小雪在優香諾亞老師面前吐槽優香的時候知道的,小雪可愛捏)一邊嬌嗔老師總是給自己添麻煩,總是需要自己幫助和督促工作。一邊有些得意地“抱怨”著【老師根本就離不開我!】的優香超級可愛!結婚!結婚!還是踏碼的結婚!想到結婚後有優香在背後輔佐,計算著生活的開銷,心裡就感到非常踏實。和優香結婚的話,女友,妻子,母三個願望一次滿足滿足,實在是人生的至福。啊,好想給優香戴上結婚戒指,一邊聽著優香說【老師又在這種沒用的地方亂花錢了】,一邊看著她把戒指當作一生寶物,一臉幸福的表情。

相關文章