Java Socket編程實(shí)現(xiàn)群聊實(shí)踐案例
上一篇文章已經(jīng)可以實(shí)現(xiàn)服務(wù)端與客戶端之間消息的交換并且開啟多個(gè)客戶端,那么如何實(shí)現(xiàn)特定客戶端與特定客戶端之間私聊以及特定客戶端發(fā)給所有在線客戶端的群聊呢,這一篇文章一起學(xué)習(xí)一下吧
代碼詳細(xì)解析
1.Mysever類-服務(wù)器主程序
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
/**1.創(chuàng)建服務(wù)器 綁定端口號(hào)
* 2.在循環(huán)中 調(diào)用accept方法 重復(fù)監(jiān)聽連接過來的客戶端
* 3.啟動(dòng)線程保持和多個(gè)客戶端連接
* 4.在線程中 傳進(jìn)去對(duì)應(yīng)的socket 獲取輸入輸出流 實(shí)現(xiàn)和客戶端的雙向通訊
* 5.創(chuàng)建客戶端對(duì)象 綁定服務(wù)器的IP地址和端口 讓客戶端去連接服務(wù)器
* 6.利用客戶端輸入輸出流 跟服務(wù)端通訊
* 7.在客戶端啟動(dòng)線程 一直讀取服務(wù)器發(fā)來的消息
*
*/
public class MyServer {
public static void main(String[] args){
try {
new MyServer().startServer();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//啟動(dòng)服務(wù)器
public void startServer() throws IOException {
//創(chuàng)建服務(wù)器對(duì)象,并綁定監(jiān)聽器端口
//端口取值范圍(2字節(jié)):0~65535
//知名端口:0~1024
ServerSocket server=new ServerSocket(8888);
System.out.println("服務(wù)端在8888端口監(jiān)聽器");
//哈希表:保存一組映射關(guān)系:每個(gè)編號(hào)對(duì)應(yīng)唯一的socket
//基本數(shù)據(jù)類型對(duì)應(yīng)的包裝類
HashMap<Integer,Socket> mp=new HashMap<>();
//每個(gè)鏈接過來的客戶端ID
int ID=1;
//重復(fù)監(jiān)聽連接這個(gè)服務(wù)器的客戶端
while(true){
//監(jiān)聽連接這個(gè)服務(wù)器的客戶端
//該方法的返回的socket用來客戶端通信
Socket socket=server.accept();
//保存當(dāng)前對(duì)應(yīng)的數(shù)據(jù)
mp.put(ID,socket);
//利用線程保持和多個(gè)客戶端連接
ServerThread st=new ServerThread(socket,mp,ID);
new Thread(st).start();
ID++;
}
}
}知識(shí)點(diǎn)解析說明:
1.端口號(hào)
ServerSocket(int port):創(chuàng)建綁定到特定端口的服務(wù)器套接字
端口范圍:0~65535
知名端口:0~1024,被系統(tǒng)服務(wù)占用
2.hashMap的使用
HashMap<Integer,Socket> mp=new HashMap<>();
key:Integer類型的客戶端ID
values:Socket對(duì)象,代表與客戶端的鏈接
作用:集中管理所有客戶端鏈接,便于消息路由
3.多線程處理
new Thread(st).start();
為每個(gè)客戶端連接創(chuàng)造獨(dú)立線程
避免單個(gè)客戶端阻塞整個(gè)服務(wù)器
實(shí)現(xiàn)真正的并發(fā)處理
2.ServerThread類-服務(wù)器線程處理
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;
public class ServerThread implements Runnable{
//跟客戶端連接的套接字
public Socket socket;
public InputStream is;
public OutputStream os;
public HashMap<Integer,Socket> mp;
public int ID;
public ServerThread(Socket socket,HashMap<Integer,Socket> mp,int ID){
this.socket=socket;
this.mp=mp;
try {
os = socket.getOutputStream();
is = socket.getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
//發(fā)送客戶端上線消息
sendMsg(ID+":客戶端上線..",os);
}
@Override
public void run() {
while(true) {
try {
//msg消息內(nèi)容: 目標(biāo)用戶:消息內(nèi)容
String msg = readMsg();
//對(duì)msg進(jìn)行解析:拆分
// 字符串分割
String[] msgArr = msg.split(":");
System.out.println(msgArr[0] + ":" + msgArr[1]);
if(msgArr[0].equals("g")){
Collection<Socket> values=mp.values();
for (Socket value : values) {
//不將消息發(fā)給自己
if(value != this.socket) {
System.out.println("值:" + value);
OutputStream output = value.getOutputStream();
sendMsg(msgArr[1], output);
}
}
}else{
//根據(jù)目標(biāo)用戶(ID)從mp中找到對(duì)應(yīng)的socket(聊天對(duì)象)
int id = Integer.parseInt(msgArr[0]); //字符串?dāng)?shù)字轉(zhuǎn)成int
Socket socket = mp.get(id);
//獲取該對(duì)象的輸出流,寫入數(shù)據(jù)
OutputStream output = socket.getOutputStream();
//把聊天內(nèi)容轉(zhuǎn)發(fā)給目標(biāo)用戶(聊天對(duì)象)
sendMsg(msgArr[1], output);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//發(fā)送消息的方法
public void sendMsg(String msg,OutputStream os){
try {
byte[] b = msg.getBytes();
os.write(b);
os.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//讀取消息的方法
public String readMsg(){
byte[] b = new byte[1024]; //最多讀取的消息長度
try {
is.read(b);
} catch (IOException e) {
throw new RuntimeException(e);
}
// trim() 去掉字符尾部空格
String s=new String(b);
return s.trim();
}
}知識(shí)點(diǎn)解釋說明:
InputStream:從套接字讀取消息(接收消息)
OutputStream:向套接字寫入數(shù)據(jù)(發(fā)送消息)
私聊:
客戶端1給客戶端3發(fā)消息,讀取客戶端1消息,獲得客戶端3輸出流 ,將消息發(fā)給客戶端3
群聊:
客戶端1將消息發(fā)送給除自己以外所有客戶端,接收客戶端1消息,獲得其他所有客戶端輸出流,將消息發(fā)送給其他所有客戶端
2.消息格式
String[] msgArr = msg.split(":");
System.out.println(msgArr[0] + ":" + msgArr[1]);split()分隔符
返回值:分割后的字符串?dāng)?shù)組
+操作符用于字符串連接,將數(shù)組第一個(gè)元素、冒號(hào)分隔符、第二個(gè)元素連接成一個(gè)新字符串
3.消息協(xié)議
群聊:g:消息內(nèi)容
私聊:目標(biāo)ID:消息內(nèi)容
4.遍歷HashMap中所有數(shù)據(jù)
因?yàn)楸揪幊绦枰闅v套接字,查看原代碼socket是values,所以用values方法遍歷
代碼示例:
import java.util.Collection;
import java.util.HashMap;
public class HashMapTraversal {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
Collection<Integer> values = map.values();
for (Integer value : values) {
System.out.println("Value: " + value);
}
}
}通過values()獲取所有值的集合,適合只關(guān)心值的場(chǎng)景。
3.Client類-客戶端程序
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args){
try {
new Client().startClient();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public InputStream is;
public OutputStream os;
//啟動(dòng)客戶端
public void startClient() throws IOException{
Socket client =new Socket("127.0.0.1",8888);
os=client.getOutputStream();
is=client.getInputStream();
new Thread(()->{
while(true){
//讀取服務(wù)器消息
String msg=readMsg();
System.out.println(msg);
}
}).start();
//發(fā)消息
Scanner scanner=new Scanner(System.in);
while(true){
System.out.println("client:");
String clientMsg=scanner.nextLine();
sendMsg(clientMsg);
}
}
//發(fā)送消息的方法
public void sendMsg(String msg){
try {
os.write(msg.getBytes());
os.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String readMsg(){
byte[] b=new byte[1024];
try {
//阻塞方法:一直等待讀取消息,如果沒有會(huì)一直等著
is.read(b);
} catch (IOException e) {
throw new RuntimeException(e);
}
//trim
return new String(b).trim();
}
}知識(shí)點(diǎn)解釋說明:
1.Socket客戶端連接
127.0.0.1:本地回環(huán)地址,用于本地測(cè)試
8888:服務(wù)端監(jiān)聽端口
2.多線程消息處理
接收線程:
new Thread(()->{
while(true){
//讀取服務(wù)器消息
String msg=readMsg();
System.out.println(msg);
}
}).start();· 持續(xù)監(jiān)聽服務(wù)器發(fā)來的消息
發(fā)送消息:
Scanner scanner=new Scanner(System.in);
while(true){
System.out.println("client:");
String clientMsg=scanner.nextLine();
sendMsg(clientMsg);
}
}Scanner用于讀取控制臺(tái)輸入
nextLine()是阻塞方法,等待用戶輸入
補(bǔ)充:
服務(wù)端為什么使用線程:服務(wù)端要保持與多個(gè)客戶端通信,每個(gè)客戶端都需要一個(gè)單獨(dú)的線程來控制通信,不然沒有辦法進(jìn)行消息的收發(fā)
客戶端為什么使用線程:讀消息是一個(gè)阻塞方法,如果讀消息發(fā)消息沒有單獨(dú)的線程去控制,他就只能按順序收發(fā),無法連續(xù)發(fā)消息 ,想要連續(xù)發(fā)消息,就沒有辦法控制,必須要有單獨(dú)的線程去控制讀和寫
4.使用指南
1.啟動(dòng)服務(wù)器
2.啟動(dòng)客戶端(可啟動(dòng)多個(gè))
3.消息發(fā)送格式
群發(fā)消息:g:hello
私聊消息:3:nihao
測(cè)試:
群聊
客戶端1輸入:g;hello
客戶端2顯示:hello
客戶端3顯示:hello
私聊
客戶端1輸入:3:nihao
客戶端2顯示:無顯示
客戶端3顯示:nihao
總結(jié)
這個(gè)Java聊天室項(xiàng)目展示了Socket編程的核心概念,包括服務(wù)器監(jiān)聽、客戶端連接、多線程處理和消息路由。雖然功能相對(duì)基礎(chǔ),但架構(gòu)清晰,為擴(kuò)展更復(fù)雜的功能提供了良好的基礎(chǔ)。
通過這個(gè)項(xiàng)目,可以深入理解網(wǎng)絡(luò)編程、多線程同步和客戶端-服務(wù)器架構(gòu)的設(shè)計(jì)模式,是學(xué)習(xí)Java網(wǎng)絡(luò)編程的優(yōu)秀實(shí)踐案例。
后續(xù)可以再進(jìn)行擴(kuò)展升級(jí),例如添加注冊(cè)登錄及聊天界面。
到此這篇關(guān)于Java Socket編程實(shí)現(xiàn)群聊的文章就介紹到這了,更多相關(guān)Java Socket群聊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot 自動(dòng)參數(shù)校驗(yàn)的實(shí)現(xiàn)步驟
在 Spring Boot中實(shí)現(xiàn)參數(shù)自動(dòng)校驗(yàn)主要依靠 Java Bean Validation API(JSR 380)和 Spring 的集成支持,下面給大家介紹Spring Boot 自動(dòng)參數(shù)校驗(yàn)的實(shí)現(xiàn)步驟,感興趣的朋友一起看看吧2025-06-06
java實(shí)現(xiàn)利用String類的簡(jiǎn)單方法讀取xml文件中某個(gè)標(biāo)簽中的內(nèi)容
下面小編就為大家?guī)硪黄猨ava實(shí)現(xiàn)利用String類的簡(jiǎn)單方法讀取xml文件中某個(gè)標(biāo)簽中的內(nèi)容。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-12-12
SpringBoot 實(shí)現(xiàn)定時(shí)任務(wù)的方法詳解
這篇文章主要介紹了SpringBoot 實(shí)現(xiàn)定時(shí)任務(wù)的方法詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
JavaGUI實(shí)現(xiàn)隨機(jī)單詞答題游戲
這篇文章主要為大家詳細(xì)介紹了JavaGUI實(shí)現(xiàn)隨機(jī)單詞答題游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12
教你Spring Cloud保證各個(gè)微服務(wù)之間調(diào)用安全性
在微服務(wù)的架構(gòu)下,系統(tǒng)會(huì)根據(jù)業(yè)務(wù)拆分為多個(gè)服務(wù),各自負(fù)責(zé)單一的職責(zé),在這樣的架構(gòu)下,我們需要確保各api的安全性,今天通過本文給大家分享Spring Cloud中如何保證各個(gè)微服務(wù)之間調(diào)用的安全性,需要的朋友參考下吧2021-08-08
關(guān)于feign.codec.DecodeException異常的解決方案
這篇文章主要介紹了關(guān)于feign.codec.DecodeException異常的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
java讀寫excel文件實(shí)現(xiàn)POI解析Excel的方法
在日常工作中,我們常常會(huì)進(jìn)行Excel文件讀寫操作,這篇文章主要介紹了java讀寫excel文件實(shí)現(xiàn)POI解析Excel的方法,實(shí)例分析了java讀寫excel的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-10-10
mybatis執(zhí)行錯(cuò)誤但sql執(zhí)行正常問題
這篇文章主要介紹了mybatis執(zhí)行錯(cuò)誤但sql執(zhí)行正常問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
SpringCloud @RefreshScope刷新機(jī)制深入探究
RefeshScope這個(gè)注解想必大家都用過,在微服務(wù)配置中心的場(chǎng)景下經(jīng)常出現(xiàn),他可以用來刷新Bean中的屬性配置,那大家對(duì)他的實(shí)現(xiàn)原理了解嗎?它為什么可以做到動(dòng)態(tài)刷新呢2023-03-03

