Java進階:基於TCP通信的網絡實時聊天室

2021-02-26 Java資料站

點擊上方藍色字體,選擇「標星公眾號」

優質文章,第一時間送達

開門見山
  最近一個月記錄了學習Socket網絡編程的知識和實戰案例,相對來說,比較系統地學習了基於TCP協議實現網絡通信,也是計算機網絡中重中之重,TCP/IP屬於網絡層,在java中,對該層的工作實現了封裝,在編程中,就更加容易地去實現通信,而不用顧及底層的實現。當然,我們需要熟悉五層協議,在實踐中體會其中的原理,理解更加深刻。

  所以,系列文章從入門開始,不斷完善C/S架構的Socket通信,回憶一下,首先是實現了Server和Client的互相通信,在這個過程發現問題,接著就使用多線程技術解決客戶端實時接收信息的問題,後來到了伺服器端,發現多用戶連接伺服器的「先到先得」問題,「後到者」無法正常通信,所以再使用線程池技術解決了多用戶伺服器的問題。

  到此,基本實現了一個簡單的客戶端-伺服器應用,因此,本篇將基於前面全部內容,使用客戶端-伺服器(C/S架構),結合多線程技術,模擬類似QQ、微信聊天功能,實現一個網絡實時聊天室,目前的功能包括:

1.L(list):查看當前上線用戶;
2. G(group):群聊;
3. O(one-one):私信;
4. E(exit):退出當前聊天狀態;
5. bye:離線;
6. H(help):查看幫助
本篇將詳細記錄網絡實時聊天室的實現步驟,以系列文章為前提基礎,可見文末。

一、數據結構Map
  前兩篇的TCPClientThreadFX和TCPThreadServer實現了多線程的通信,但也只是客戶端和伺服器的聊天,如何做到群組的聊天?想法就是客戶A的聊天信息通過伺服器轉發到同時在線的所有客戶。

具體做法是需要在伺服器端新增記錄登陸客戶信息的功能,每個用戶都有自己的標識。本篇將使用簡單的「在線方式」記錄客戶套接字,即採用集合來保存用戶登陸的套接字信息,來跟蹤用戶連接。

所以,我們需要選擇一種合適的數據結構來保存用戶的Socket和用戶名信息,那在java中,提供了哪些數據結構呢?

Java常用的集合類型有:Map、List和Set。Map是保存Key-Value對,List類似數組,可保存可重複的值,而Set只保存不重複的值,相當於是只保存key,不保存value的Map。

  如果是有用戶名、學號登錄的操作,就可以採用Map類型的集合來存儲,例如可使用key記錄用戶名+學號,value保存套接字。對於本篇的網絡聊天室的需求,需要採用Map,用來保存不同用戶的socket和登錄名。用戶套接字socket作為key來標識一個在線用戶是比較方便的選擇,因為每一個客戶端的IP位址+埠組合是不一樣的。

二、保證線程安全
  很明顯,我們需要使用到多線程技術,而在多線程環境中,對共享資源的讀寫存在線程並發安全的問題,例如HashMap、HaspSet等都不是線程安全的,可以通過synchronized關鍵字進行加鎖,但還有更方便的方案:可以直接使用Java標準庫的java.util.concurrent包提供的線程安全的集合。例如HashMap的線程安全是 ConcurrentHashMap,HashSet的線程安全Set是CopyOnWriteArraySet。

如圖,Map繼承體系:


 在JDK1.8中,對HashMap進行了改進,當結點數量超過TREEIFY_THRESHOLD 則要轉換為紅黑樹,這樣很大優化了查詢的效率,但仍然不是線程安全的。


這裡簡單了解一下,具體學習可以查詢相關資料。有了以上的基本知識,下面開始進入網絡實時聊天室的具體實現。

三、群聊核心方法
  基於前面這樣的想法:實現群聊就是客戶A的聊天信息通過伺服器轉發到同時在線的所有客戶,伺服器端根據HashMap記錄登陸用戶的socket,向所有用戶轉發信息。

核心的群組發送方法sendToAllMembers,用於給所有在線客服發送信息。

private void sendToMembers(String msg,String hostAddress,Socket mySocket) throws IOException{

PrintWriter pw;
OutputStream out;
Iterator iterator=users.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry=(Map.Entry) iterator.next();
Socket tempSocket = (Socket) entry.getKey();
String name = (String) entry.getValue();
if (!tempSocket.equals(mySocket)){
out=tempSocket.getOutputStream();
pw=new PrintWriter(new OutputStreamWriter(out,"utf-8"),true);
pw.println(hostAddress+":"+msg);
}
}

}

使用到了Map的遍歷,對其他所有用戶發送信息。

相同的原理,我們實現私聊的功能,轉化為實現的思想,也就是當前用戶和指定用戶Socket之間的通信,所以我寫了一個sendToOne的方法。

private void sendToOne(String msg,String hostAddress,Socket another) throws IOException{

PrintWriter pw;
OutputStream out;

Iterator iterator=users.entrySet().iterator();
while (iterator.hasNext()){

Map.Entry entry=(Map.Entry) iterator.next();
Socket tempSocket = (Socket) entry.getKey();
String name = (String) entry.getValue();

if (tempSocket.equals(another)){
out=tempSocket.getOutputStream();
pw=new PrintWriter(new OutputStreamWriter(out,"utf-8"),true);
pw.println(hostAddress+"私信了你:"+msg);
}
}
}

以上兩個方法是本網絡聊天室的關鍵,後面實現的功能將是對這兩個方法的靈活運用。

四、聊天室具體設計
目前聊天室的功能定位包括:1)查看當前上線用戶;2):群聊;3)私信;4)退出當前聊天狀態;5)離線;6)查看幫助。

首先,初始化最關鍵的數據結構,作為類成員變量,HashMap用來保存Socket和用戶名:

private ConcurrentHashMap users=new ConcurrentHashMap();

每個功能具體實現如下:

0、用戶登錄伺服器
這裡是最開始的伺服器端的信息處理,需要記錄每個用戶的登錄信息,包括連接的套接字和自定義暱稱,方便後續使用。我採用的方法是當用戶連接伺服器時候,提醒用戶輸入用戶名來進一步操作,也實現了不重名的判斷邏輯。代碼如下:

pw.println("From 伺服器:歡迎使用服務!");
pw.println("請輸入用戶名:");
String localName = null;
while ((hostName=br.readLine())!=null){
users.forEach((k,v)->{
if (v.equals(hostName))
flag=true;//線程修改了全局變量
});

if (!flag){
localName=hostName;
users.put(socket,hostName);
flag=false;
break;
}
else{
flag=false;
pw.println("該用戶名已存在,請修改!");
}
}

登錄成功之後會向所有在線用戶告知上線信息。

1、查看當前上線用戶
其實就是將伺服器端記錄在HashMap中的信息返回給請求用戶,通過約定的命令L來查看:

if (msg.trim().equalsIgnoreCase("L")){

users.forEach((k,v)->{

pw.println("用戶:"+v);

});

continue;

}

2、群聊

else if (msg.trim().equals("G")){
pw.println("您已進入群聊。");
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&users.size()!=1)
sendToMembers(msg,localName,socket);
else if (users.size()==1){
pw.println("當前群聊無其他用戶在線,已自動退出!");
break;
}
else {
pw.println("您已退出群組聊天室!");
break;
}
}

}

3、私信
同理,處理邏輯變為一對一的通信,與之前伺服器-客戶端一對一類似,但是這裡需要更多的處理,保證邏輯正確,包括被私聊人的在線狀態,被私聊人用戶名是否正確等。

1 //一對一私聊
2 else if (msg.trim().equalsIgnoreCase("O")){
3 pw.println("請輸入私信人的用戶名:");
4 String name=br.readLine();

6 //查找map中匹配的socket,與之建立通信
7 //有待改進,處理輸入的用戶名不存在的情況
8 users.forEach((k, v)->{
9 if (v.equals(name)) {
10 isExist=true;//全局變量與線程修改問題
11 }
12 
13 });
14 //已修復用戶不存在的處理邏輯
15 Socket temp=null;
16 for(Map.Entry mapEntry : users.entrySet()){
17 if(mapEntry.getValue().equals(name))
18 temp = mapEntry.getKey();
19 // System.out.println(mapEntry.getKey()+":"+mapEntry.getValue()+'\n');
20 }
21 if (isExist){
22 isExist=false;
23 //私信後有一方用戶離開,另一方未知,仍然發信息而未收到回復,未處理這種情況
24 while ((msg=br.readLine())!=null){
25 if (!msg.equals("E")&&!isLeaved(temp))
26 sendToOne(msg,localName,temp);
27 else if (isLeaved(temp)){
28 pw.println("對方已經離開,已斷開連接!");
29 break;
30 }
31 else{
32 pw.println("您已退出私信模式!");
33 break;
34 }
35 }
36 }
37 else
38 pw.println("用戶不存在!");
39 }


4、退出當前聊天狀態
這個功能主要融入到群聊和私聊中,可見上面兩個功能實現的內部調用,定義了一個方法isLeaved,判斷用戶是否已經下線。

//判斷用戶是否已經下線
private Boolean isLeaved(Socket temp){
Boolean leave=true;
for(Map.Entry mapEntry : users.entrySet()) {
if (mapEntry.getKey().equals(temp))
leave = false;
}
return leave;
}

5、離線
這個功能比較簡單,通過約定的命令執行。

if (msg.trim().equalsIgnoreCase("bye")) {
pw.println("From 伺服器:伺服器已斷開連接,結束服務!");

users.remove(socket,localName);

sendToMembers("我下線了",localName,socket);
System.out.println("客戶端離開。");//加當前用戶名
break;
}

6、查看幫助
通過命令H請求伺服器的幫助,是指用戶查看哪些命令對應的功能,來進行選擇。

else if (msg.trim().equalsIgnoreCase("H")){
pw.println("輸入命令功能:(1)L(list):查看當前上線用戶;(2)G(group):進入群聊;(3)O(one-one):私信;(4)E(exit):退出當前聊天狀態;(5)bye:離線;(6)H(help):幫助");
continue;//返回循環
}

五、聊天室服務完整代碼
聊天室實現主要工作在於服務端,聚焦於伺服器線程處理的內部類Hanler,上面是各個功能具體介紹,下面完整給出代碼,只需要在前面文章的基礎上,見Java多線程實現多用戶與服務端Socket通信。

修改伺服器端線程處理代碼:

class Handler implements Runnable {
private Socket socket;

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

public void run() {
//本地伺服器控制臺顯示客戶端連接的用戶信息
System.out.println("New connection accept:" + socket.getInetAddress().getHostAddress());
try {
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);

pw.println("From 伺服器:歡迎使用服務!");
pw.println("請輸入用戶名:");
String localName = null;
while ((hostName=br.readLine())!=null){
users.forEach((k,v)->{
if (v.equals(hostName))
flag=true;//線程修改了全局變量
});

if (!flag){
localName=hostName;
users.put(socket,hostName);
flag=false;//可能找出不一致問題
break;
}
else{
flag=false;
pw.println("該用戶名已存在,請修改!");
}
}

// System.out.println(hostName+": "+socket);
sendToMembers("我已上線",localName,socket);
pw.println("輸入命令功能:(1)L(list):查看當前上線用戶;(2)G(group):進入群聊;(3)O(one-one):私信;(4)E(exit):退出當前聊天狀態;(5)bye:離線;(6)H(help):幫助");

String msg = null;
//用戶連接伺服器上線,進入聊天選擇狀態
while ((msg = br.readLine()) != null) {
if (msg.trim().equalsIgnoreCase("bye")) {
pw.println("From 伺服器:伺服器已斷開連接,結束服務!");

users.remove(socket,localName);

sendToMembers("我下線了",localName,socket);
System.out.println("客戶端離開。");//加當前用戶名
break;
}
else if (msg.trim().equalsIgnoreCase("H")){
pw.println("輸入命令功能:(1)L(list):查看當前上線用戶;(2)G(group):進入群聊;(3)O(one-one):私信;(4)E(exit):退出當前聊天狀態;(5)bye:離線;(6)H(help):幫助");
continue;//返回循環
}
else if (msg.trim().equalsIgnoreCase("L")){
users.forEach((k,v)->{
pw.println("用戶:"+v);
});
continue;
}
//一對一私聊
else if (msg.trim().equalsIgnoreCase("O")){
pw.println("請輸入私信人的用戶名:");
String name=br.readLine();

//查找map中匹配的socket,與之建立通信
users.forEach((k, v)->{
if (v.equals(name)) {
isExist=true;//全局變量與線程修改問題
}

});
//已修復用戶不存在的處理邏輯
Socket temp=null;
for(Map.Entry mapEntry : users.entrySet()){
if(mapEntry.getValue().equals(name))
temp = mapEntry.getKey();
}
if (isExist){
isExist=false;
//私信後有一方用戶離開,另一方未知,仍然發信息而未收到回復,未處理這種情況
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&!isLeaved(temp))
sendToOne(msg,localName,temp);
else if (isLeaved(temp)){
pw.println("對方已經離開,已斷開連接!");
break;
}
else{
pw.println("您已退出私信模式!");
break;
}
}
}
else
pw.println("用戶不存在!");
}
//選擇群聊
else if (msg.trim().equals("G")){
pw.println("您已進入群聊。");
while ((msg=br.readLine())!=null){
if (!msg.equals("E")&&users.size()!=1)
sendToMembers(msg,localName,socket);
else if (users.size()==1){
pw.println("當前群聊無其他用戶在線,已自動退出!");
break;
}
else {
pw.println("您已退出群組聊天室!");
break;
}
}

}
else
pw.println("請選擇聊天狀態!");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}


六、效果演示:TCP網絡實時聊天室
首先,開啟多個客戶端,連接伺服器開始進入通信狀態。


下面動圖演示了幾個基本功能,可以看到三個用戶實現了實時通信聊天,包括群聊和私聊功能。其他功能就留給大家去探索。

結語
  系列文章從入門開始,不斷完善C/S架構的Socket通信,回憶一下,首先是實現了Server和Client的互相通信,在這個過程發現問題,接著就使用多線程技術解決客戶端實時接收信息的問題,後來到了伺服器端,發現多用戶連接伺服器的「先到先得」問題,「後到者」無法正常通信,所以再使用線程池技術解決了多用戶伺服器的問題。

  本篇基本實現了一個簡單的客戶端-伺服器應用,使用客戶端-伺服器(C/S架構),結合多線程技術,模擬類似QQ、微信聊天功能,實現一個網絡實時聊天室。

學習到的知識有:多線程、線程池、Socket通信、TCP協議、HashMap、JavaFX等,所有知識的結合運用,並通過實戰練習,一步步理解知識!
————————————
本博文版權歸作者所有!
禁止商業轉載等用途或聯繫作者授權,非商業轉載請註明出處!
作者:Charzueus 來源:博客園
本文連結:https://www.cnblogs.com/chenzhenhong/p/14168284.html
版權聲明:本文為博主原創文章,轉載請附上原文出處連結和本聲明。

粉絲福利:Java從入門到入土學習路線圖

👇👇👇

感謝點讚支持下哈 

相關焦點

  • 基於TCP的網絡實時聊天室(socket通信案例)
    開門見山最近一個月記錄了學習Socket網絡編程的知識和實戰案例,相對來說,比較系統地學習了基於TCP協議實現網絡通信,也是計算機網絡中重中之重,TCP/IP屬於網絡層,在java中,對該層的工作實現了封裝,在編程中,就更加容易地去實現通信,而不用顧及底層的實現。當然,我們需要熟悉五層協議,在實踐中體會其中的原理,理解更加深刻。
  • 詳解Java網絡編程
    埠和套接字當網絡中的兩臺計算機進行通信時,除了確定計算機在網絡中的IP外,還需要確定計算機中的一個埠,埠並不是實際的物理設備,它是一個應用程式,這個應用程式來負責兩臺計算機的通信。一個IP標識了一臺主機(伺服器),主機可以提供多種服務,如web服務、ftp服務、遠程桌面等。主機的每個服務都會等待客戶端的連接,客戶端如何區別這些服務呢?
  • TCP長連接和心跳那些事
    應用,所以你能夠看到是本地的 53078 埠與 20880 埠在通信。=90net.ipv4.tcp_keepalive_intvl=15net.ipv4.tcp_keepalive_probes=2KeepAlive 機制是在網絡層面保證了連接的可用性,但站在應用框架層面我們認為這還不夠。
  • 基於VxWorks實時作業系統的通信模型設計
    本文提出了一種任務間的通信模型,將用於網絡通信的UDP方式引進到任務間的通信中,使通信更加靈活和便於管理,改善了整個系統的性能。本文引用地址:http://www.eepw.com.cn/article/90136.htm多任務實時作業系統VxWorks簡介  VxWorks作業系統是一種嵌入式實時作業系統(RTOS),是嵌入式開發環境的關鍵組成部分,具有可靠高、實時性強、可裁減性的特點。VxWorks為程式設計師提供了高效的實時任務調度、中斷管理、實時的系統資源以及任務間通信。
  • Java Socket應用——通信是這樣煉成的
    一、網絡基礎知識1.1、兩臺計算機通過網絡進行通信(1)兩臺主機要有一個唯一的標識(IP位址)。(2)需要有共同的語言(協議)。(3)每臺主機需要有相應的埠號(為了辨別不同應用程式的通信)。1.4、Java提供網絡功能四大類(1)InetAddress:用語標識網絡上硬體資源。(2)URL:統一資源定位符,直接讀取或寫入網絡上數據。(3)Sockets: 使用TCP協議實現網絡通信的Socket相關類。(4)Datagram: 使用UDP,將數據保存在數據報中實現網絡通信。
  • 基於B/S結構的網絡控制系統開發
    但是,由於網絡延時的存在,基於網絡的控制系統不可能是一種閉環控制,採用的是遠程監督控制方案,而邏輯控制功能由現場設備層完成。 本文根據這一思想提出基於b/s結構進行的網絡控制,並開發了一套基於plc和交流變頻器的實時遠程控制系統,該系統集工控組態軟體、plc技術、變頻控制技術,網絡通信技術於一體。
  • 深入原理學習之–TCP長連接與心跳保活
    短連接:每次通信時,創建 Socket;一次通信結束,調用 socket.close()。這就是一般意義上的短連接,短連接的好處是管理起來比較簡單,存在的連接都是可用的連接,不需要額外的控制手段。 長連接:每次通信完畢後,不會關閉連接,這樣就可以做到連接的復用。長連接的好處便是省去了創建連接的耗時。短連接和長連接的優勢,分別是對方的劣勢。
  • 幾種遠程監控通信方式的介紹_幾種遠程監控方式的比較
    stub 充當遠程對象的客戶端代理,有著和遠程對象相同的遠程接口,遠程對象的調用實際是通過調用該對象的客戶端代理對象stub來完成的,通過該機制RMI就好比它是本地工作,採用tcp/ip協議,客戶端直接調用服務端上的一些方法。優點是強類型,編譯期可檢查錯誤,缺點是只能基於Java語言,客戶機與伺服器緊耦合。   來看下基於RMI的一次完整的遠程通信過程的原理:1.
  • 基於Socket的java通信編程詳解
    Socket原理機制:  通信的兩端都有Socket  網絡通信其實就是Socket間的通信  數據在兩個Socket間通過IO傳輸  7、Java中的網絡支持  針對網絡通信的不同層次,Java提供了不同的API,其提供的網絡功能有四大類:  InetAddress:用於標識網絡上的硬體資源
  • 《圖解TCP/IP(第5版)》
    目錄  · · · · · ·第1章 網絡基礎知識1.1  計算機網絡出現的背景1.1.1  計算機的普及與多樣化1.1.2  從獨立模式到網絡互連模式1.1.3  從計算機通信到信息通信1.1.4  計算機網絡的作用1.2  計算機與網絡發展的7個階段1.2.1  批處理1.2.2  分時系統1.2.3  計算機之間的通信
  • Java socket通信基本原理介紹
    Java socket通信基本原理介紹 Java socket通信在不斷的進行相關代碼的開發,下面我們就看看如何才能更好的使用有關技術為我們的編程工作帶來一定的幫助。
  • python筆記28(TCP,UDP,socket協議)
    4、代碼部分:①介紹socket;②使用socket完成tcp協議的web通信;③使用socket完成udp協議的web通信。1、TCP協議1、可靠,慢,全雙工通信2、建立連接的時候,三次握手3、斷開連接的時候,四次揮手4、在建立起連接後發送的每條信息都有回執為了保證數據的完整性,還有重傳機制5、長連接:會一直佔用雙方的埠6、I/O操作(input,output),輸入輸出相對內存來說的。
  • Tcptraceroute:基於TCP數據包的路由跟蹤器
    現代網絡廣泛使用防火牆,導致傳統路由跟蹤工具發出的(ICMP應答(ICMP echo)或UDP)數據包都被過濾掉了,所以無法進行完整的路由跟蹤。儘管如此,許多情況下,防火牆會準許反向(inbound)TCP數據包通過防火牆到達指定埠,這些埠是主機內防火牆背後的一些程序和外界連接用的。
  • 網絡協議-TCP和UDP最完整的區別介紹
    ,可以在菜單欄-文章整理-進階篇模塊中查看。TCP與UDP基本區別  1.基於連接與無連接  2.TCP要求系統資源較多,UDP較少;   3.UDP程序結構較簡單   4.流模式(TCP)與數據報模式(UDP);   5.TCP保證數據正確性,UDP可能丟包   6.TCP保證數據順序,UDP不保證   UDP應用場景:  1.面向數據報方式  2
  • 作為一個Java 程式設計師 你應該會什麼
    ◆java.util 包下的80%以上的類的靈活運用,特別是集合類體系、規則 表達式、zip、以及時間、隨機數、屬性、資源和Timer.◆java.io 包下的60%以上的類的使用,理解IO 體系的基於管道模型的設計思路以及常用IO 類的特性和使用場合。◆java.math 包下的100%的內容。
  • java網絡編程之基礎知識點總結
    概述本文是網絡編程系列的第一篇文章,所以不講代碼,主要是網絡編程的一些基礎知識,下面的這些知識點主要是對java網絡編程這本書的歸納與整理。有興趣的同學可以看看。我們知道計算機之間的通信要經過一系列複雜的過程,計算機之間通過傳輸介質、通信設施和網絡通信協議互聯,實現資源共享和數據傳輸。
  • Java socket編程
    而TCP層則提供面向應用的可靠(tcp)的或非可靠(UDP)的數據傳輸機制,這是網絡編程的主要對象,一般不需要關心IP層是如何處理數據的。目前較為流行的網絡編程模型是客戶機/伺服器(C/S)結構。即通信雙方一方作為伺服器等待客戶提出請求並予以響應。客戶則在需要服務時向伺服器提 出申請。
  • 基於物聯網的嬰兒實時監控系統的設計
    2.2 無線網絡模塊   2.2.1 WiFi傳輸模塊   無線網絡模塊設計的是本系統的主要核心部分,通過無線網絡進行傳感器之間的數據傳輸,使得處在無線網絡中的各傳感器通信布線少,提高通信的效率和協調化
  • 噓~聽說有逼格的人都基於Arduino搭建個人聊天室了!
    在UNO或其他使用ATmega328晶片的主控板上,佔用13、12、11、10、4引腳進行網絡通信及SD卡存儲。而MEGA的引腳佔用情況可不一樣。同時,舊版的Ethernet擴展板是通過10~13號引腳連接到W5100晶片,使得其只能堆疊到UNO上,與此同時,MEGA的SPI引腳是50~53、Leonardo的SPI引腳在ICSP引腳處。
  • 什麼是下一代通信網絡(NGN)
    什麼是下一代通信網絡(NGN)更新時間:2007-05-11 12:49:28  核心提要:什麼是下一代通信網絡(NGN)  NGN(Next Generation Network)即下一代通信網絡。  下一代網絡是以軟交換為核心的,能夠提供包括語音、數據、視頻和多媒體業務的基於分組技術的綜合開放的網絡架構,代表了通信網絡發展的方向。