Socket網絡編程(六)——簡易聊天室案例

目錄

  • 聊天室數據傳輸設計
    • 客戶端、服務器數據交互
    • 數據傳輸協議
    • 服務器、多客戶端模型
    • 客戶端如何發送消息到另外一個客戶端
    • 2個以上設備如何交互數據?
  • 聊天室消息接收實現
    • 代碼結構
    • client客戶端重構
    • server服務端重構
      • 自身描述信息的構建
      • 重構TCPServer.java
      • 基于synchronized 解決多線程操作的安全問題
    • 聊天室Server/Client啟動、測試
    • 源碼下載

聊天室數據傳輸設計

  • 必要條件:客戶端、服務器
  • 必要約束:數據傳輸協議
  • 原理:服務器監聽消息來源、客戶端鏈接服務器并發送消息到服務器

客戶端、服務器數據交互

20240229-145006-Go.png

client 發送消息到服務器端,服務器端回復消息也就是回送消息。

數據傳輸協議

20240229-145145-zG.png

數據在傳輸的時候,需要在尾部追加換行符,也就是說原來5個字節的數據,在實際傳輸時,是有6個字節長度的。

服務器、多客戶端模型

20240229-145351-SU.png
在客戶端有多個情況下,客戶端都會向服務器端進行發送消息;想要在PC發送消息給服務器端時,也讓安卓、平板等終端都能收到,其操作應該是,當PC端發送一條消息到服務器端之后,服務器端得到該數據后,它會把這條數據發送(回送)給當前連接的客戶端。而這些當前連接的客戶端收到這條消息后,就實現了把PC消息發送到手機的過程。

20240229-145449-mB.png

客戶端如何發送消息到另外一個客戶端

每個客戶端都是服務器也是客戶端?
答:不是

2個以上設備如何交互數據?

答:約定一個基礎的數據格式,這里使用回車換行符來作為信息的截斷
客戶端-服務器-轉發到客戶端,如下圖:
20240229-145850-t9.png

User1發送消息到服務端,服務端將消息轉發給其他的客戶端(比如User2),從而實現聊天室的功能

聊天室消息接收實現

代碼結構

20240229-170606-4z.png

代碼分為四個module,分別為clink、constants、client、server。

  • clink:該module為提供工具類進行校驗與流處理。
  • constants:基礎的共用類代碼
  • server:服務端代碼,需要依賴 clink、constants兩個module
  • client:客戶端代碼,需要依賴 clink、constants兩個module

clink、constants的工具類,基礎數據類參考前面 TCP點對點傳輸的代碼邏輯

client客戶端重構

初版代碼和TCP點對點傳輸的基本一致,聊天室主要在TCPServer端進行轉發,所以Client不需要代碼重構。

server服務端重構

初版代碼和TCP點對點傳輸的基本一致,要實現聊天室消息接收則需要進行重構。主要重構 TCPServer.java 、ClientHandler.java類。

ClientHandler.java - 消息轉發
原有的消息在收到后就只是打印到控制臺

// 打印到屏幕
System.out.println(str);

而實現聊天室功能需要將收到的消息進行通知出去。這里可以通過 CloseNotify() 接口進行實現。這里對該接口進行改造,并新增轉發的接口方法來將消息通知回去。

    /*** 消息回調*/public interface ClientHandlerCallback {// 自身不安比通知void onSelfClosed(ClientHandler handler);// 收到消息時通知void onNewMessageArrived(ClientHandler handler,String msg);}

在將消息打印到屏幕的同時,將消息通知出去:

       // 打印到屏幕System.out.println(str);clientHandlerCallback.onNewMessageArrived(ClientHandler.this,str);

調用onNewMessageArrived()方法從而進行轉發。這里主要是把當前收到的消息傳遞回去,同時也要把自身傳遞回去。

自身描述信息的構建

新增clientInfo類變量:

    private final String clientInfo;

自身描述信息初始化:

    public ClientHandler(Socket socket, ClientHandlerCallback clientHandlerCallback) throws IOException {this.socket = socket;this.readHandler = new ClientReadHandler(socket.getInputStream());this.writeHandler = new ClientWriteHandler(socket.getOutputStream());this.clientHandlerCallback = clientHandlerCallback;// 新增自身描述信息this.clientInfo = "A[" + socket.getInetAddress().getHostAddress() + "] P[" + socket.getPort() + "]";System.out.println("新客戶端連接:" + clientInfo);}public String getClientInfo() {return clientInfo;}

重構TCPServer.java

重構 clientHandler.ClientHandlerCallback的兩個回調方法,這里要將之提到TCPServer.java類上。

讓TCPServer.java 實現 clientHandler.ClientHandlerCallback接口。并實現兩個方法:

    @Overridepublic synchronized void onSelfClosed(ClientHandler handler) {}@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {}

并將 客戶端構建溢出線程的remove操作遷移到 onSelfClosed() 方法實現內:

    @Overridepublic synchronized void onSelfClosed(ClientHandler handler) {clientHandlerList.remove(handler);}

原有的ClientHandler異步線程處理邏輯如下

        // 客戶端構建異步線程ClientHandler clientHandler = new ClientHandler(client,handler -> clientHandlerList.remove(handler));

重構后,如下:

    // 客戶端構建異步線程ClientHandler clientHandler = new ClientHandler(client,TCPServer.this);

消息轉發

    /*** 轉發消息給其他客戶端* @param handler* @param msg*/@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {// 打印到屏幕System.out.println("Received-" + handler.getClientInfo() + ":" + msg);// 轉發forwardingThreadPoolExecutor.execute(()->{for (ClientHandler clientHandler : clientHandlerList){if(clientHandler.equals(handler)){// 跳過自己continue;}// 向其他客戶端投遞消息clientHandler.send(msg);}});}

基于synchronized 解決多線程操作的安全問題

由于這里有對 clientHandlerList集合的刪除、添加、遍歷等操作,這涉及到對所有客戶端的操作,在多線程的環境下,默認的List不是線程安全的,所以存在多線程的安全問題。

    public void stop() {if (mListener != null) {mListener.exit();}synchronized (TCPServer.this){for (ClientHandler clientHandler : clientHandlerList) {clientHandler.exit();}clientHandlerList.clear();}// 停止線程池forwardingThreadPoolExecutor.shutdownNow();}public synchronized void broadcast(String str) {for (ClientHandler clientHandler : clientHandlerList) {clientHandler.send(str);}}/*** 刪除當前消息* @param handler*/@Overridepublic synchronized void onSelfClosed(ClientHandler handler) {clientHandlerList.remove(handler);}/*** 轉發消息給其他客戶端* @param handler* @param msg*/@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {// 打印到屏幕System.out.println("Received-" + handler.getClientInfo() + ":" + msg);// 轉發}

這里加類鎖來保證刪除操作的線程安全。

關于添加操作的線程安全問題解決如下:

          try {// 客戶端構建異步線程ClientHandler clientHandler = new ClientHandler(client,TCPServer.this);// 讀取數據并打印clientHandler.readToPrint();// 添加同步處理synchronized (TCPServer.this) {clientHandlerList.add(clientHandler);}} catch (IOException e) {e.printStackTrace();System.out.println("客戶端連接異常:" + e.getMessage());}

異步轉發

        // 轉發clientHandlerCallback.onNewMessageArrived(ClientHandler.this,str);

在ClientHandler.java中,上述代碼所在的線程是主要線程,會一直有消息進來,所以不能做同步處理,那樣會導致當前線程阻塞,從而導致后面進來的消息無法及時處理。

所以當 onNewMessageArrived()將消息拋出去之后,TCPServer.java的實現要采取異步轉發的方式退給其他客戶端。創建一個新的單例線程池來做轉發的操作:

新增轉發線程池:

    // 轉發線程池private final ExecutorService forwardingThreadPoolExecutor;public TCPServer(int port) {this.port = port;this.forwardingThreadPoolExecutor = Executors.newSingleThreadExecutor();}

轉發投遞消息給其他客戶端:

    /*** 轉發消息給其他客戶端* @param handler* @param msg*/@Overridepublic void onNewMessageArrived(ClientHandler handler, String msg) {// 打印到屏幕System.out.println("Received-" + handler.getClientInfo() + ":" + msg);// 轉發forwardingThreadPoolExecutor.execute(()->{synchronized (TCPServer.this){for (ClientHandler clientHandler : clientHandlerList){if(clientHandler.equals(handler)){// 跳過自己continue;}// 向其他客戶端投遞消息clientHandler.send(msg);}}});}

防止客戶端下線后,依舊重復發送的問題:

ClientHandler.java - ClientWriteHandler

       /*** 發送到客戶端* @param str*/void send(String str) {// 如果已經發送完成,就返回if(done){return;}executorService.execute(new WriteRunnable(str));}

聊天室Server/Client啟動、測試

idea單個程序同時啟動多個窗口的方法:

  1. 啟動main方法
    20240301-171559-Pt.png

  2. 勾選運行運行多個
    20240301-171650-l3.png

  3. 保存退出就可以了

測試結果如下:

  1. 先啟動服務端,再啟動三個客戶端
    20240301-171752-6g.png
    20240301-171809-S0.png

  2. 服務端和客戶端發消息
    服務端發送:我是服務端
    客戶端發送客戶端1、客戶端2、客戶端3
    20240301-171954-5i.png
    20240301-172007-It.png

  3. 其中一個客戶端退出,不影響其他客戶端和服務端發送消息
    20240301-172133-bs.png

至此,socket簡易,聊天室重構完成

源碼下載

下載地址:https://gitee.com/qkongtao/socket_study/tree/master/src/main/java/cn/kt/socket/SocketDemo_chatroom

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/712636.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/712636.shtml
英文地址,請注明出處:http://en.pswp.cn/news/712636.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Nginx多次代理后獲取真實的用戶IP訪問地址

需求:記錄用戶操作記錄,類似如下表格的這樣 PS: 注意無論你的服務是Http訪問還是Https 訪問的都是可以的,我們服務之前是客戶只給開放了一個端口,但是既要支持https又要支持http協議,nginx 是可以通過stream 模塊配置雙…

2023中國PostgreSQL數據庫生態大會:洞察前沿趨勢,探索無限可能(附核心PPT資料下載)

隨著數字化浪潮的推進,數據庫技術已成為支撐各行各業數字化轉型的核心力量。2023中國PostgreSQL數據庫生態大會的召開,無疑為業界提供了一個深入交流、共同探索PostgreSQL數據庫技術未來發展趨勢的平臺。本文將帶您走進這場盛會,解析大會的亮…

k8s Pod基礎(概念,容器功能及分類,鏡像拉取和容器重啟策略)

目錄 pod概念 Kubernetes設計Pod概念和特殊組成結構的用意 Pod內部結構: 網絡共享: 存儲共享: pause容器主要功能 pod創建方式 pod使用方式 pod分類 pod的容器分類 基礎容器(infrastructure container)&…

加密和簽名的區別及應用場景

原文網址:加密和簽名的區別及應用場景_IT利刃出鞘的博客-CSDN博客 簡介 本文介紹加密和簽名的區別及應用場景。 RSA是一種非對稱加密算法, 可生成一對密鑰(私鑰和公鑰)。(RSA可以同時支持加密和簽名)。 …

元宇宙3D虛擬場景制作深圳華銳視點免費試用

隨著元宇宙興起,3D線上展廳得到了越來越多的關注和應用。基于VR虛擬現實技術的元宇宙3D線上展廳在線編輯系統,更是為企業在展覽展示領域帶來了前所未有的輔助。 高效便捷: 元宇宙3D線上展廳在線編輯無需復雜的施工和搭建過程,只需…

報錯問題解決django.db.utils.OperationalError: (1049, “Unknown database ‘ mxshop‘“)

開發環境:ubuntu22.04 pycharm 功能:django連接使用mysql數據庫,各項配置看似正常 報錯: django.db.utils.OperationalError: (1049, "Unknown database mxshop") 分析檢查原因: Setting的配置文件內&…

gcd+線性dp,[藍橋杯 2018 國 B] 矩陣求和

一、題目 1、題目描述 經過重重筆試面試的考驗,小明成功進入 Macrohard 公司工作。 今天小明的任務是填滿這么一張表: 表有 �n 行 �n 列,行和列的編號都從 11 算起。 其中第 �i 行第 �j 個元素…

GRPC 錯誤碼表

code數描述OK0不是錯誤;成功返回。CANCELLED1操作通常由調用方取消。UNKNOWN2未知錯誤。例如,當從另一個地址空間接收的值屬于此地址空間中未知的錯誤空間時,可能會返回此錯誤。此外,未返回足夠錯誤信息的 API 引發的錯誤可能會轉換為此錯誤。…

ggplot去除背景

在ggplot2中去除背景,通常指的是去除圖表的灰色背景和網格線,使圖表背景變為透明或白色,以及去除或簡化坐標軸的背景。這可以通過調整主題(theme)來實現。ggplot2提供了多種主題設置,可以用來調整圖表的外觀…

Spring MVC 和 Spring Cloud Gateway不兼容性問題

當啟動SpringCloudGateway網關服務的時候,沒注意好依賴問題,出現了這個問題: Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway. 解決辦法就是:刪除SpringMVC的依賴,即下列依賴。 &…

ChatGPT/GPT4科研應用與AI繪圖及論文高效寫作

原文:ChatGPT/GPT4科研應用與AI繪圖及論文高效寫作 第一:2024年AI領域最新技術 1.OpenAI新模型-GPT-5 2.谷歌新模型-Gemini Ultra 3.Meta新模型-LLama3 4.科大訊飛-星火認知 5.百度-文心一言 6.MoonshotAI-Kimi 7.智譜AI-GLM-4 第二:…

【C++從0到王者】第四十六站:圖的深度優先與廣度優先

文章目錄 一、圖的遍歷二、廣度優先遍歷1.思想2.算法實現3.六度好友 三、深度優先遍歷1.思想2.代碼實現 四、其他問題 一、圖的遍歷 對于圖而言,我們的遍歷一般是遍歷頂點,而不是邊,因為邊的遍歷是比較簡單的,就是鄰接矩陣或者鄰接…

《匯編語言》第3版 (王爽)檢測點3.1解析

第三章 檢測點3.1 (1).在Debug中,用“d 0:0 1f”查看內存,結果如下。 下面的程序執行前,AX 0,BX 0,寫出每條匯編指令執行完后相關寄存器中的值。 mov ax,1 ;將1放入AX寄存器中,…

GC如何判定對象已死

GC判定對象已死的2種方法 引用計數法 給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;Java語言中沒有選用引用計數算法來管理內存,其中最主要的原因是它很…

【零基礎SRC】成為漏洞賞金獵人的第一課:加入玲瓏安全漏洞挖掘班。

我們是誰 你是否對漏洞挖掘充滿好奇?零基礎或有基礎但想更進一步?想賺取可觀的漏洞賞金讓自己有更大的自由度? 那么,不妨了解下我們《玲瓏安全團隊》。 玲瓏安全團隊,擁有多名實力講師,均就職于互聯網頭…

一線互聯網大廠中高級Android面試真題收錄,記一次字節跳動Android社招面試

在開始回答前,先簡單概括性地說說Linux現有的所有進程間IPC方式: 1. **管道:**在創建時分配一個page大小的內存,緩存區大小比較有限; 2. 消息隊列:信息復制兩次,額外的CPU消耗;不合…

指針與malloc動態內存申請,堆和棧的差異

定義了兩個函數print_stack()和print_malloc(),分別演示了兩種不同的內存分配方式:棧內存和堆內存。然后在main()函數中調用這兩個函數,并將它們返回的指針打印出來。 由于print_stack()中的數組c是在棧上分配的,當函數返回后&…

【哈希表算法題記錄】242.有效的字母異位詞

題目鏈接 這題思路比較簡單,考慮到26個小寫字母的ASCII是連續的,那么我們可以設置一個size為26的哈希表record,然后記錄26個字母分別在string中出現的次數。例如,record[0]記錄的是字母‘a’出現的次數。于是我們主要就是要獲得每…

Python裝飾器的使用詳解

目錄 1、函數裝飾器 1.1、閉包函數 1.2、裝飾器語法 1.3、裝飾帶參數的函數 1.4、被裝飾函數的身份問題 1.4.1、解決被裝飾函數的身份問題 1.5、裝飾器本身攜帶/傳參數 1.6、嵌套多個裝飾器 2、類裝飾器 裝飾器顧名思義作為一個裝飾的作用,本身不改變被裝…

Acwing 周賽135 解題報告 | 珂學家 | 反悔堆貪心

前言 整體評價 VP了這場比賽, T3挺有意思的,反悔貪心其實蠻套路的。 A. 買蘋果 思路: 簽到 n, x list(map(int, input().split())) print (n // x)B. 牛群 思路: 分類討論 from collections import Counters input() cnt Counter(s)lists sorte…