目錄
前言
一、拾枝雜談
? ? ? ? 1.項目開發(fā)大體流程 :?
? ? ? ? 2.多用戶即時通信系統(tǒng)分析 :?
? ? ? ? ? ? ? ? 1° 需求分析
? ? ? ? ? ? ? ? 2° 整體分析
二、用戶登錄
? ? ? ? 1.準備工作?:?
????????2.客戶端 :?
????????????????1° 菜單界面
? ? ? ? ? ? ? ? 2° 登錄驗證
? ? ? ? ? ? ? ? 3° 線程創(chuàng)建
? ? ? ? ? ? ? ? 4° 線程管理
? ? ? ? 3.服務端 :?
? ? ? ? ? ? ? ? 1° 用戶驗證
? ? ? ? ? ? ? ? 2° 線程創(chuàng)建
? ? ? ? ? ? ? ? 3° 線程管理
? ? ? ? 4.登錄測試?:?
三、在線列表
? ? ? ? 1.擴充MessageType中的類型?:?
? ? ? ? 2.擴充UserClientService類中的方法 :?
? ? ? ? 3.擴充客戶端線程類中的內(nèi)容 :?
????????4.擴充ControlServerConnectClientThread類中的方法 :?
? ? ? ? 5.擴充服務端線程類中的內(nèi)容 :?
? ? ? ? 6.拉取測試 :?
四、退出系統(tǒng)
? ? ? ? 1.需要解決的問題 :?
? ? ? ? 2.解決辦法 :?
? ? ? ? ? ? ? ? 1° 總思路
? ? ? ? ? ? ? ? 2° 客戶端
????????????????3° 服務端
五、私聊群聊
? ? ? ? 1.私發(fā)消息 :?
? ? ? ? ? ? ? ? 1° 思路分析
? ? ? ? ? ? ? ? 2° 代碼實現(xiàn)
? ? ? ? ? ? ? ? 3° 運行測試?
? ? ? ? 2.群發(fā)消息 :?
? ? ? ? ? ? ? ? 1° 客戶端?
? ? ? ? ? ? ? ? 2° 服務端?
? ? ? ? ? ? ? ? 3° 運行測試?
六、傳輸文件
? ? ? ? 1.思路分析 :?
? ? ? ? 2.客戶端 :?
????????3.服務端 :?
????????4.運行測試 :?
七、最終代碼
? ? ? ? 1.客戶端 :?
? ? ? ? ? ? ? ? 1° View
? ? ? ? ? ? ? ? 2°?UserClientService
? ? ? ? ? ? ? ? 3°?MessageClientService
? ? ? ? ? ? ? ? 4°?FileClientService
? ? ? ? ? ? ? ? 5°?ClientConnectServiceThread
? ? ? ? ? ? ? ? 6°?ControlClientConnectServiceThread
? ? ? ? 2.服務端 :?
? ? ? ? ? ? ? ? 1°?ChatServer
? ? ? ? ? ? ? ? 2°?ServerConnectClientThread
? ? ? ? ? ? ? ? 3°?ControlServerConnectClientThread
? ? ? ? ? ? ? ? 4°?ChatFrame
? ? ? ? 3.公共部分 :?
? ? ? ? ? ? ? ? 1° Message
? ? ? ? ? ? ? ? 2° MessageType
? ? ? ? ? ? ? ? 3° User
前言
? ? ? ? 本篇博文適合javaSE基礎較為扎實的小伙伴兒們閱讀,up會從實現(xiàn)層面和大家分享一個多用戶即時通信系統(tǒng),類似于QQ,微信這種可以實現(xiàn)登錄,聊天,發(fā)文件,下線等功能的程序。但是聲明一點,該多用戶即時通信系統(tǒng)不是項目(up之后會專門開新的專欄來出項目),而只是對已學的java知識的聯(lián)系和應用,可以理解為一個模擬項目,主要涉及到oop,集合,IO流,多線程,網(wǎng)絡編程等內(nèi)容。如果你想進一步鞏固自己的java基礎,這篇博文或許會是很好的選擇。感謝閱讀!
一、拾枝雜談
? ? ? ? 1.項目開發(fā)大體流程 :?
? ? ? ? ①分析階段 : 需求分析師會從“技術實現(xiàn)”和“行業(yè)情況”兩方面綜合考慮,出一個需求分析報告(通常是白皮書),包含客戶的具體要求以及項目最終要實現(xiàn)的功能。需求分析在整個項目開發(fā)流程中所占用的時間和資源——往往與項目本身的大小成正比。
? ? ? ? ②設計階段 : 主要是架構師和項目經(jīng)理攬活兒,有些公司會將二者合并。架構師/項目經(jīng)理需要負責項目的設計工作(UML類圖,流程圖,模塊設計,數(shù)據(jù)庫,項目架構);并且要完成項目的原型開發(fā)——先在虛擬機上跑出一個預覽的項目效果(不過多考慮性能),與客戶進行對接,簽訂合約。一切就緒后,架構師/項目經(jīng)理就會在公司的各個部門物色人選;比方說,當前項目是用java來實現(xiàn)的,架構師/項目經(jīng)理就會挑選java技術牛逼的??。因此,有些時候會出現(xiàn)一個??同時在兩個甚至多個項目組的情況,這時候這只牛逼的??會很忙,但是卻痛并快樂著,因為它可以領到double甚至是multiple的工資(??們的工資往往由基本工資 + 項目提成構成),設計階段在整個項目流程中所占用的時間往往比分析階段短一些,但依然與項目本身的大小成正比。
? ? ? ? ③實現(xiàn)階段 : 不多解釋,??兒們的主場。??兒們要負責把架構師/項目經(jīng)理給的模塊功能進行一一實現(xiàn),完事兒后在自己run一run,看看自己負責的代碼有沒有bug。實現(xiàn)階段在整個項目流程中所占的比重和項目本身成反比,即項目越大,實現(xiàn)階段反而不如需求階段和分析階段重要。但是,實際情況是,小公司小項目的實現(xiàn)階段往往是占比最大的一個,而且還會出現(xiàn)一邊實現(xiàn)一邊改需求的情況,即設計階段和實現(xiàn)階段纏一塊兒了。
? ? ? ? ④測試階段 : 測試工程師,??兒們的天敵;負責把??兒們的代碼拿來做各種測試,例如黑白盒測試,集成測試,單元測試等;因此,測試工程師與開發(fā)工程師往往打成一片,不可開交。在測試階段,最怕的事情就是——高耦合性的代碼出現(xiàn)了bug。
? ? ? ? ⑤實施階段 : 實施工程師,需要將項目正確地部署到客戶的平臺,并保證其運行正常,需要有較強的開發(fā)能力和環(huán)境配置能力,以及較好的身體素質??蛻舻钠脚_可能部署在不同的省市,甚至國家,因此實施工程師需要東奔西走,把每個平臺的服務器,操作系統(tǒng),環(huán)境配置等問題都給搞定。打個比方,小公司的實施工程師——使命召喚;大公司的實施工程師——塞爾達傳說。
? ? ? ? ⑥維護階段 : 解決程序后期出現(xiàn)的bug,解決項目升級相關的問題。大公司——運維工程師;小公司——背鍋俠。
? ? ? ? 2.多用戶即時通信系統(tǒng)分析 :?
? ? ? ? ? ? ? ? 1° 需求分析
? ? ? ? ①用戶登錄????????
? ? ? ? ②在線檢測? ? ? ? (拉取在線用戶列表)
? ? ? ? ③退出系統(tǒng)? ? ? ? (客戶端與服務器端)
? ? ? ? ④私聊群聊? ? ? ? (實現(xiàn)單發(fā)和群發(fā)消息)
? ? ? ? ⑤傳送文件???????
? ? ? ? ? ? ? ? 2° 整體分析
? ? ? ??Δ對于服務端——
? ? ? ? ①服務端上往往提供了不同的服務,因此服務端需要通過ServerSocket來監(jiān)聽不同的端口;
? ? ? ? ②每當有客戶端成功連接到服務端,都會獲得一個Socket對象;此時,啟動一個線程,并令該線程持有Socket對象,即令每個線程都操縱一個自己的Socket對象,此舉可以實現(xiàn)消息的群發(fā);
? ? ? ? ③可以使用HashMap集合來管理服務端的多個線程。
? ? ? ? Δ對于客戶端——
? ? ? ? ①客戶端采用對象的形式來與服務端進行通訊,此舉可以發(fā)送更多的信息;可以使用對象處理流 ObjectInputStream和ObjectOutputStream來進行數(shù)據(jù)的讀取。
? ? ? ? ②當客戶端成功根據(jù)"IP + 端口"成功連接到服務端后,客戶端獲得自己的Socket對象;此時,類似地,也啟動一個線程,并令該線程持有Socket對象。
? ? ? ? ③同樣使用HashMap集合來管理客戶端的多個線程。
? ? ? ? Δ如下圖所示 :?
? ? ? ? User對象可以驗證是否為合法的登錄用戶;Message對象則包含了要傳輸?shù)男畔?/span>。
二、用戶登錄
? ? ? ? 1.準備工作?:?
? ? ? ? ? ? ? ? ①在IDEA中創(chuàng)建一個新項目“ChatServer”,用來模擬通信系統(tǒng)的服務端;并另建一個新項目“ChatClient_0”用來模擬通信系統(tǒng)的一個客戶端;如下圖所示 :?
? ? ? ? ? ? ? ? ②在服務端(ChatServer)項目中,src包下,創(chuàng)建一個mutual包,表示服務端和客戶端共有的內(nèi)容(用戶信息和發(fā)送的消息)。在mutual包下創(chuàng)建User類,并令其實現(xiàn)Serializable接口;實現(xiàn)Serializable接口后User對象就可以序列化,進行網(wǎng)絡傳輸,就可以被對象處理流操作。User類中定義用戶名和用戶密碼兩個屬性。User類代碼如下 :?
package mutual;
import java.io.Serializable;
/**
* @author : Cyan_RA9
* @version : 21.0
* @meaning : The shared User between Server and Client
*/
public class User implements Serializable {
private static final long serialVersionUID = 1L; //增強兼容性
private String id;
private String pwd;
public User() {}
public User(String id, String pwd) {
this.id = id;
this.pwd = pwd;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
? ? ? ? ? ? ? ? ③在mutual包下創(chuàng)建Message類,表示傳輸?shù)南㈩愋停⒘钇?strong>實現(xiàn)Serializable接口;Message類中應該包括消息的發(fā)送者,消息的接收者,消息的類型等屬性,這樣服務端解包后才知道這消息是發(fā)給誰的,以及消息的具體內(nèi)容是什么。Message類代碼如下 :?
package mutual;
import java.io.Serializable;
/**
* @author : Cyan_RA9
* @version : 21.0
* @message : information that are transmitted
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1L; //增強兼容性
private String sendTime; //發(fā)送時間
private String sender; //發(fā)送者
private String receiver; //接收者
private String content; //消息內(nèi)容
private String mesType; //消息類型
public String getSendTime() {
return sendTime;
}
public void setSendTime(String sendTime) {
this.sendTime = sendTime;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getMesType() {
return mesType;
}
public void setMesType(String mesType) {
this.mesType = mesType;
}
}
? ? ? ? ? ? ? ? ④還需要確定Message內(nèi)容的具體類型,可以定義MessageType接口,在接口中定義不同的常量,以表示不同的消息類型;MessageType接口代碼如下 :?
package mutual; /** * @author : Cyan_RA9 * @version : 21.0 * @meaning : Types of message */ public interface MessageType { //定義常量 String MESSAGE_LOGIN_SUCCESS = "1"; //表示登錄成功 String MESSAGE_LOGIN_FAIL = "0"; //表示登錄失敗 }
? ? ? ? ? ? ? ? ⑤最后,將mutual包拷貝一份到客戶端,如下圖所示 :?
????????2.客戶端 :?
????????????????1° 菜單界面
? ? ? ? ? ? ? ? 在客戶端(ChatClient_0)新建一個包client,用戶存放用戶相關的類;在client包下,另建一個包menu,用于菜單的界面顯示。在menu包下新建一個View類,View類代碼如下 :?
package client.menu;
import client.service.UserClientService;
import java.io.IOException;
import java.util.Scanner;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 菜單界面的顯示
* @PS : Run -> Edit Configurations -> Modify options -> allow multiple instances
*/
public class View {
private boolean loop = true; //控制是否顯示菜單
private String key = ""; //接收用戶的鍵盤輸入
private static Scanner sc = new Scanner(System.in);
/*
將UserClientService對象置為屬性,
該對象用于執(zhí)行用戶登錄/注冊等操作(該步驟將功能與界面聯(lián)系起來)
*/
private UserClientService userClientService = new UserClientService();
public static void main(String[] args) throws IOException {
new View().mainMenu();
System.out.println("客戶端退出...");
}
private void mainMenu() throws IOException {
while (loop) {
System.out.println("===========Welcome to the system of chat:===========");
System.out.println("\t\t1.登錄系統(tǒng)");
System.out.println("\t\t9.退出系統(tǒng)");
System.out.print("請輸入你的選擇:");
key = sc.nextLine();
switch (key) {
case "1" :
//登錄操作
System.out.print("請輸入用戶名:");
String userID = sc.nextLine();
System.out.print("請輸入密 碼:");
String password = sc.nextLine();
//驗證登錄的用戶是否合法(封裝思想)
if (userClientService.check(userID, password)) { //驗證成功
System.out.println("\n===========Welcome user " + userID + "===========");
//向用戶顯示二級菜單
while (loop) {
System.out.println("\n===========網(wǎng)絡通信系統(tǒng)二級菜單(user:" + userID + ")===========");
System.out.println("\t\t1.在線列表:");
System.out.println("\t\t2.群發(fā)消息:");
System.out.println("\t\t3.私發(fā)消息:");
System.out.println("\t\t4.文件發(fā)送:");
System.out.println("\t\t9.退出系統(tǒng):");
System.out.print("請輸入你的選擇:");
key = sc.nextLine();
switch (key) {
case "1" :
System.out.println(1);
break;
case "2" :
System.out.println(2);
break;
case "3" :
System.out.println(3);
break;
case "4" :
System.out.println(4);
break;
case "9" :
loop = false; //在二級菜單中用戶也可以直接選擇退出系統(tǒng)
}
}
} else { //驗證失敗
System.out.println("登錄失??!請重新嘗試!");
}
break;
case "9" :
sc.close();
loop = false; //將控制while循環(huán)的布爾變量設置為false
break;
}
}
}
}
? ? ? ? ? ? ? ? 為了實現(xiàn)多用戶登錄,需要對View類進行配置,依次點擊Run -> Edit Configurations -> Modify options -> allow multiple instances,允許并行,如下圖所示 :?
? ? ? ? ? ? ? ? 2° 登錄驗證
? ? ? ? ? ? ? ? View類中有關“用戶登錄驗證”部分的代碼,利用封裝的思想,將其封裝到client.service包下的類UserClientService中,UserClientService類代碼如下 :?
package client.service;
import mutual.Message;
import mutual.MessageType;
import mutual.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 登錄驗證
*/
public class UserClientService {
/*
將User對象設置成一個屬性,可利用getter和setter修改User對象的引用,便于操作。
Socket對象同樣也可能在其他類中使用,因此也設置為屬性。
*/
private User user = new User();
private Socket socket;
public boolean check(String userID, String password) throws IOException {
//局部變量
boolean b = false;
//初始化User對象
user.setId(userID);
user.setPwd(password);
//向服務端發(fā)送信息
try {
//1.獲取Socket對象
socket = new Socket(InetAddress.getByName("127.0.0.1"), 8888);
//2.獲取與Socket對象相關聯(lián)的對象處理流(輸出流)
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//3.序列化User對象,寫入數(shù)據(jù)通道(向服務端發(fā)送一個User對象,服務端會對這個User對象進行驗證)
oos.writeObject(user);
//.........
//4.獲取與Socket對象相關聯(lián)的對象處理流(輸入流)
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//5.讀取服務端傳輸過來的Message對象
Message message = (Message) ois.readObject(); //類型強轉
if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)) {
//創(chuàng)建線程對象(目的是為了與服務端保持通訊)
ClientConnectServiceThread ccst = new ClientConnectServiceThread(socket);
//啟動線程
ccst.start();
//將線程放入集合中統(tǒng)一管理
ControlClientConnectServiceThread.addClientConnectServiceThread(userID, ccst);
b = true;
} else {
//如果沒有啟動線程,關閉Socket對象。
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
}
? ? ? ? ? ? ? ? 3° 線程創(chuàng)建
? ? ? ? ? ? ? ? 為了保持通訊,需要讓線程持有Socket對象;同時,利用HashMap集合來管理多個線程。UserClientService類中有關線程的部分,同樣新建一個類ClientConnectServiceThread,在client.service包下,ClientConnectServiceThread類代碼如下 :?
package client.service;
import mutual.Message;
import java.io.ObjectInputStream;
import java.net.Socket;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 客戶端用于和服務端進行通訊的線程
*/
public class ClientConnectServiceThread extends Thread{
//該線程需要持有Socket對象
private Socket socket;
public ClientConnectServiceThread(Socket socket) {
this.socket = socket;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
//∵Thread需要在后臺與服務器通信,因此使用while循環(huán)
while (true) {
try {
System.out.println("客戶端線程,等待讀取來自服務器端的消息...");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
/*
如果服務端沒有發(fā)送Message對象到數(shù)據(jù)通道中,線程就會阻塞在這里。
*/
Message message = (Message) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
? ? ? ? ? ? ? ? 4° 線程管理
? ? ? ? ? ? ? ? UserClientService類中涉及到線程“管理”,將相關代碼進行封裝,在client.service包下新建一個ControlClientConnectServiceThread類,代碼如下 :?
package client.service;
import java.util.HashMap;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 管理客戶端的線程
*/
public class ControlClientConnectServiceThread {
/*
使用HashMap類來管理多個線程(模擬數(shù)據(jù)庫),key表示用戶的id,value表示線程。
*/
private static HashMap<String, ClientConnectServiceThread> hashMap = new HashMap<>();
//添加線程的方法
public static void addClientConnectServiceThread(String userID, ClientConnectServiceThread ccst) {
hashMap.put(userID, ccst);
}
//取出線程的方法
public static ClientConnectServiceThread getClientConnectServiceThread(String userID) {
return hashMap.get(userID);
}
}
? ? ? ? 3.服務端 :?
? ? ? ? ? ? ? ? 1° 用戶驗證
? ? ? ? ? ? ? ? 服務端的代碼與客戶端類似,都需要創(chuàng)建一個類用于讀取數(shù)據(jù)通道中的數(shù)據(jù);還需要一個線程類來持有Socket對象;最后就是一個類來管理服務端的線程。
????????????????在ChatServer包下創(chuàng)建server.service包,在該包下創(chuàng)建ChatServer類,用于接收客戶端法來的User和Message對象,并給出回應。ChatServer類代碼如下 :?
package server.service;
import mutual.Message;
import mutual.MessageType;
import mutual.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 服務端
*/
public class ChatServer {
//將ServerSocket設置為屬性,寫在main函數(shù)外
private ServerSocket serverSocket = null;
/**
將合法的用戶放入集合中(使用“id + user”的泛型),
建議使用ConcurrentHashMap集合,線程同步,可在多線程程序下安全使用。
*/
private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
static { //在靜態(tài)代碼塊中初始化validUsers集合對象
validUsers.put("Cyan", new User("Cyan", "RA9"));
validUsers.put("Rain", new User("Rain", "flo"));
validUsers.put("Ice", new User("Ice", "ais"));
validUsers.put("Five", new User("Five", "55555"));
validUsers.put("Kyrie", new User("Kyrie", "lrving"));
}
public boolean checkUser(String userID, String password) {
User user = validUsers.get(userID);
if (user == null) { //如果合法用戶集合中不存在當前用戶,直接返回false;
return false;
}
if (!(user.getPwd().equals(password))) { //如果存在該用戶,但密碼錯誤,返回false;
return false;
}
return true;
}
public ChatServer() {
//端口也可以寫在配置文件中
try {
System.out.println("服務端正在8888端口監(jiān)聽...");
serverSocket = new ServerSocket(8888);
/*
監(jiān)聽是不間斷的,當服務端和某個客戶端建立連接后,服務端會繼續(xù)監(jiān)聽。
*/
while (true) {
//獲取Socket類對象(服務端是通過accept方法來獲取Socket對象的)
Socket socket = serverSocket.accept();
//獲取Socket對象關聯(lián)的輸入流與輸出流(對象處理流)
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//客戶端第一次傳過來的是User對象
User user = (User) ois.readObject();
//暫時以單用戶登錄為例(id = Cyan, pwd = RA9)
//創(chuàng)建一個Message對象,用于回復客戶端是否連接成功(Message對象寫在if-else語句外)
Message message = new Message();
if (checkUser(user.getId(), user.getPwd())) { //登錄成功
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
//將包含“登錄成功與否”信息的Message對象寫入數(shù)據(jù)通道
oos.writeObject(message);
//創(chuàng)建一個線程,與客戶端保持通訊
ServerConnectClientThread scct = new ServerConnectClientThread(socket, user.getId());
//啟動線程
scct.start();
//將線程放入集合中
ControlServerConnectClientThread.addServerConnectClientThread(user.getId(), scct);
} else { //登錄失敗
System.out.println("id = " + user.getId() + ",pwd = " + user.getPwd() + " 驗證失??!");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close(); //關閉Socket
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//若退出while循環(huán),說明服務端不再監(jiān)聽,需要關閉ServerSocket對象
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
? ? ? ? ? ? ? ? 2° 線程創(chuàng)建
? ? ? ? ? ? ? ? 同樣,為了保持通訊,需要讓線程持有Socket對象,相關代碼封裝到service包下的ServerConnectClientThread類中,代碼如下 :?
package server.service;
import mutual.Message;
import java.io.ObjectInputStream;
import java.net.Socket;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 服務端的線程,用于和客戶端保持通訊
*/
public class ServerConnectClientThread extends Thread {
private Socket socket;
private String userID; //當前連接到服務端的用戶的id
public ServerConnectClientThread(Socket socket, String userID) {
this.socket = socket;
this.userID = userID;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
while (true) {
try {
System.out.println("服務端與客戶端" + userID + "保持通訊,讀取數(shù)據(jù)中...");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
? ? ? ? ? ? ? ? 3° 線程管理
? ? ? ? ? ? ? ? 涉及到線程管理的代碼封裝到ControlServerConnectClientThread類中,代碼如下 :?
package server.service;
import java.util.HashMap;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 用于管理服務端的線程
*/
public class ControlServerConnectClientThread {
private static HashMap<String, ServerConnectClientThread> hashMap = new HashMap<>();
//添加線程到集合中
public static void addServerConnectClientThread(String userID, ServerConnectClientThread scct) {
hashMap.put(userID, scct);
}
//根據(jù)用戶的id獲取對應的線程
public static ServerConnectClientThread getServerConnectClientThread(String userID) {
return hashMap.get(userID);
}
}
? ? ? ? 4.登錄測試?:?
? ? ? ? ? ? ? ? 在服務器端新建一個frame包,在該包下新建一個ChatFrame類,用于啟動服務端(客戶端在View類中啟動)。ChatFrame類代碼如下 :?
package frame;
import server.service.ChatServer;
public class ChatFrame {
public static void main(String[] args) {
new ChatServer();
}
}
? ? ? ? ? ? ? ? 同時啟動ChatFrame類和View類,效果如下GIF圖 :?
三、在線列表
? ? ? ? 1.擴充MessageType中的類型?:?
????????????????客戶端如果想獲取當前多用戶通訊系統(tǒng)中的在線成員列表,需要通過Message對象向服務端申請,服務端再通過Message對象的形式,將系統(tǒng)的在線用戶列表發(fā)送給客戶端。
? ? ? ? ? ? ? ? 首先我們需要對MessageType中的類型進行擴充,如下圖所示 :?
? ? ? ? 2.擴充UserClientService類中的方法 :?
? ? ? ? ? ? ? ? 在客戶端的UserClientService類中新增一個用于拉取在線用戶列表的onlineList方法,代碼如下 :?
public void onlineList() {
//向服務端發(fā)送一個Message對象,類型是MESSAGE_GET_ONLINE_FRIENDS
Message message = new Message();
message.setSender(user.getId()); //用戶登錄時已在check方法中設置了id的值,所以可直接用
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIENDS);
try {
//得到當前線程持有的Socket對象對應的對象處理流(輸出流)
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message); //向服務端發(fā)送“拉取在線用戶列表”的請求
} catch (IOException e) {
throw new RuntimeException(e);
}
}
? ? ? ? 3.擴充客戶端線程類中的內(nèi)容 :?
? ? ? ? ? ? ? ? 在客戶端的ClientConnectServerThread類中,run方法里面,增加對于Message類型判斷和處理的邏輯語句,代碼如下 :?
/*
判斷客戶端讀取到的Message的類型,并做出相應的業(yè)務處理。
*/
if (message.getMesType().equals(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS)) {
//若Message的類型是返回的在線用戶列表,取出在線列表并顯示,使用空格分隔不同用戶的id
String[] onlineUsers = message.getContent().split(" ");
System.out.println("===========在線用戶列表如下:===========");
for (int i = 0; i < onlineUsers.length; i++) {
System.out.println("用戶: " + onlineUsers[i]);
}
} else {
System.out.println("...other content");
}
????????4.擴充ControlServerConnectClientThread類中的方法 :?
? ? ? ? ? ? ? ? 拉取在線用戶列表的操作要在服務端線程的run方法中進行,同樣可以利用oop思想,將相關代碼封裝起來;考慮每個線程都保存了當前用戶的id和對應的Socket對象,于是決定在服務端線程的管理類ControlServerConnectClientThread類中新增一個onlineList方法,用于拉取用戶的在線列表,返回一個String類型的字符串給客戶端,客戶端的線程再對該字符串進行處理。onlineList方法代碼如下 :?
//獲取在線用戶列表
public static String getOnlineFriends() {
/*
利用hashMap集合中的key是用戶id的特點,可以對hashMap對象進行遍歷,從而獲取用戶列表。
*/
Iterator<String> iterator = hashMap.keySet().iterator();
String onlineUsers = "";
while (iterator.hasNext()) {
onlineUsers += iterator.next() + " "; //加空格對應客戶端的split方法。
}
return onlineUsers;
}
? ? ? ? 5.擴充服務端線程類中的內(nèi)容 :?
? ? ? ? ? ? ? ? 有了拉取在線用戶的方法,就可以在服務端的線程類中調用該方法了,如下圖所示 :?
? ? ? ? 6.拉取測試 :?
? ? ? ? ? ? ? ? 啟動ChatFrame類和多個View類,如下GIF圖所示 :??
四、退出系統(tǒng)
? ? ? ? 1.需要解決的問題 :?
? ? ? ? 當用戶登錄成功后,即客戶端與服務端建立連接后,若我們在控制臺輸入9,整個進程并沒有退出,如下圖所示 :?
????????這是因為主線程退出后,負責聯(lián)絡服務端的子線程還沒有退出(Socket數(shù)據(jù)通道還沒有關閉),還在不停運行,等待服務端發(fā)送數(shù)據(jù),如下圖所示 :?
? ? ? ? 2.解決辦法 :?
????????? ??1° 總思路
? ? ? ? ? ? ? ? 如果我們可以直接令客戶端的整個進程關閉,就可以自動退出該進程下的所有線程;可以在客戶端的View類下增加一個方法的調用,若用戶輸入9,就給服務器端發(fā)送一個Mesage對象,令服務端退出與當前對象相關聯(lián)的線程。
? ? ? ? ? ? ? ? 服務端的線程類中保存了與當前用戶相關聯(lián)的Socket對象和當前用戶的ID,如下所示 :?
? ? ? ? ? ? ? ? 因此,服務端可以根據(jù)接收到的Message對象,關閉指定線程及Socket對象。
? ? ? ? ? ? ? ? 然后,在客戶端調用System.exit(0)方法退出當前進程。
????????? ??2° 客戶端
????????????????在UserClientService類中的新定義一個方法logout,用于完成對服務端發(fā)送“關閉線程”的Message對象的功能。logout方法代碼如下 :?
/** logout方法可以退出當前用戶 */
public void logout() {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(user.getId()); //指定具體要退出的客戶端
//發(fā)送Message對象
try {
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message);
System.out.println(user.getId() + " 退出系統(tǒng)...");
System.exit(0); //0表示正常退出當前“進程”。
} catch (IOException e) {
throw new RuntimeException(e);
}
}
? ? ? ? ? ? ? ? 同時,在View類中調用該方法,如下圖所示 :?
????????? ??3° 服務端
? ? ? ? ? ? ? ? 首先,在服務端的線程管理類ControlServerConnectClientThread類中,新定義一個方法用來刪除服務端指定的線程,如下所示 :?
? ? ? ? ? ? ? ? 然后在服務端的線程類ServerConnectClientThread中新增一個else if的判斷,并在其中調用該方法(可在刪除前令線程休眠0.5s,以避免EOF異常),如下圖所示 :?
? ? ? ? ? ? ? ? 測試結果如下(成功):
五、私聊群聊
? ? ? ? 1.私發(fā)消息 :?
? ? ? ? ? ? ? ? 1° 思路分析
? ? ? ? ? ? ? ? 不管是私發(fā)還是群發(fā),一般情況下,一個客戶端與另一個客戶端都是無法直接通訊的,需要經(jīng)過服務端來轉發(fā)。
? ? ? ? ? ? ? ? 對于客戶端,它需要將要發(fā)送的信息打包成Message對象,然后發(fā)給服務端;同時,要接收來自其他用戶的(經(jīng)過服務端轉發(fā)的)消息。
? ? ? ? ? ? ? ? 對于服務端,它需要讀取某個用戶發(fā)送給另一個用戶的消息,然后根據(jù)Message對象中的id信息獲取到對應線程,繼而獲取到該線程持有的Socket,最后通過Socket將信息發(fā)送給另一個用戶。
? ? ? ? ? ? ? ? 2° 代碼實現(xiàn)
? ? ? ? ? ? ? ? 客戶端 :?
? ? ? ? ? ? ? ? 在client.service包下新建一個MessageClientService類,用于管理消息,代碼如下 :?
package client.service;
import mutual.Message;
import mutual.MessageType;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 提供與消息有關的服務
*/
public class MessageClientService {
/**
* @param receiver : 消息的接收者
* @param content : 消息內(nèi)容
* @param sender : 消息的發(fā)送者
*/
public void sendMessageToOne(String receiver, String content, String sender) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMMON_MES); //消息類型
message.setReceiver(receiver);
message.setContent(content);
message.setSender(sender);
message.setSendTime(new java.util.Date().toString()); //發(fā)送時間
System.out.println(sender + " 對 " + receiver + " 說 \"" + content + "\"");
try {
//獲取發(fā)送消息的用戶的輸出流對象,并將上面的消息發(fā)給服務端
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
? ? ? ? ? ? ? ? 在客戶端的線程類中,增加一條else if語句,打印出服務端轉發(fā)來的消息,如下圖所示 :?
? ? ? ? ? ? ? ? 最后,在客戶端界面的相關部分(View類),調用MessageCilentService類的發(fā)送消息的方法,如下圖所示 :?
? ? ? ? ? ? ? ? 服務端 :?
? ? ? ? ? ? ? ? 在服務端與“發(fā)消息用戶”通訊的線程中(ServerConnectClientThread類中),通過Message對象封裝的信息,獲取服務端與“要接收消息的用戶”通訊的線程,然后通過該線程獲取要接受消息的用戶的Socket以及其對應的對象處理流;最后將消息寫入到該Socket對應的數(shù)據(jù)通道中,實現(xiàn)消息的轉發(fā),如下圖所示 :?
? ? ? ? ? ? ? ? 3° 運行測試?
? ? ? ? ? ? ? ? 如下GIF圖所示 :?
? ? ? ? 2.群發(fā)消息 :?
? ? ? ? ? ? ? ? 群發(fā)消息,就是在私發(fā)消息的基礎上,在服務端遍歷在線用戶列表;然后將發(fā)消息的用戶自己排除后,把Message對象轉發(fā)給其他所有的在線用戶。這里要對MessageType接口進行擴充,增加一個消息類型,如下圖所示 :?
? ? ? ? ? ? ? ? 1° 客戶端?
? ? ? ? ? ? ? ? 在MessageClientService類中定義一個群發(fā)消息的方法sendMessageToAll,代碼如下 :?
public void sendMessageToAll(String content, String sender) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMMON_MES_ALL); //消息類型
message.setContent(content); //消息內(nèi)容
message.setSender(sender); //消息發(fā)送者
message.setSendTime(new java.util.Date().toString()); //消息發(fā)送時間
System.out.println(sender + " 對所有在線的??們說 \"" + content + "\"");
try {
//獲取發(fā)送消息的用戶的輸出流對象,并將上面的消息發(fā)給服務端
ClientConnectServiceThread clientConnectServiceThread = ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
ObjectOutputStream oos = new ObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
? ? ? ? ? ? ? ? 然后在View類中的相應區(qū)域調用該方法,如下圖所示 :?
? ? ? ? ? ? ? ? 接著,在客戶端的線程類中增加一個else if 的判斷,用于接收來自服務端轉發(fā)的群發(fā)消息并顯示在控制臺,如下圖所示 :?
? ? ? ? ? ? ? ? 2° 服務端?
? ? ? ? ? ? ? ? 首先在管理線程的類中,定義一個可以返回hashMap的方法,如下圖所示 :?
? ? ? ? ? ? ? ? 然后,在服務端的線程類中,新增一個else if 的判斷,如果判斷Message類型是群發(fā)類型,就遍歷集合,實現(xiàn)群發(fā)。如下圖所示 :?
? ? ? ? ? ? ? ? 3° 運行測試?
? ? ? ? ? ? ? ? 如下GIF圖 :??
六、傳輸文件
? ? ? ? 1.思路分析 :?
? ? ? ? ? ? ? ? 發(fā)送文件與發(fā)送消息原理類似,都是以Message對象為載體;只不過發(fā)送文件時,Message對象中的內(nèi)容是一個保存了圖片的字節(jié)數(shù)組了。
? ? ? ? ? ? ? ? 對于客戶端——
? ? ? ? ? ? ? ? 先把要發(fā)送的文件讀取到客戶端(字節(jié)數(shù)組);然后把文件對應的字節(jié)數(shù)組封裝到Message對象中;最后將Message對象發(fā)送給服務端;當然,客戶端還需要接收來自服務端轉發(fā)過來的Message對象,并將其中的文件內(nèi)容保存到磁盤。
? ? ? ? ? ? ? ? 對于服務端——
? ? ? ? ? ? ? ? 服務端接收到來自某一個用戶發(fā)來的Message對象后,要進行拆包,獲取到具體的接收者,然后實現(xiàn)轉發(fā)即可。
? ? ? ? ? ? ? ? 還需要對MessageType接口進行擴充,如下圖所示 :?
? ? ? ? ? ? ? ? 對Message類進行擴充,如下圖所示 :?
? ? ? ? ? ? ? ? 提供一些新的屬性以及它們對應的getter, setter方法。??
? ? ? ? 2.客戶端 :?
? ? ? ? ? ? ? ? 在client.service包下新定義一個FileClientService類,用于文件發(fā)送功能的實現(xiàn),FileClientService類代碼如下 :?
package client.service;
import mutual.Message;
import mutual.MessageType;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 實現(xiàn)發(fā)送文件相關的功能
*/
public class FileClientService {
/**
* @param souPath : 數(shù)據(jù)源文件路徑
* @param desPath : 目的地文件路徑
* @param sender : 發(fā)送者(ID)
* @param receiver : 接收者(ID)
*/
public void setFileToOne(String souPath, String desPath, String sender, String receiver) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_FILE_TRANSMISSION);
message.setSouPath(souPath);
message.setDesPath(desPath);
message.setSender(sender);
message.setReceiver(receiver);
//1.讀取文件
/*
利用File類的length方法(獲取當前文件的大小,以字節(jié)計算),
可以得知要創(chuàng)建的字節(jié)數(shù)組的大??;
因為length方法的返回值是long類型,所以此處需要類型強轉。
*/
byte[] file = new byte[(int)new File(souPath).length()];
//創(chuàng)建一個輸入流
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(souPath);
fileInputStream.read(file); //將file文件讀取到字節(jié)數(shù)組中。
//將文件包裝到Message對象
message.setFile(file);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//關閉輸入流
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//2.提示信息
System.out.println("\n" + sender + " 給 " + receiver + " 發(fā)送 " + souPath +
"到對方電腦的目錄" + desPath + "下...");
//3.發(fā)送文件
try {
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
? ? ? ? ? ? ? ? 然后,還要在客戶端的線程類中擴展一個else if 的判斷語句,若接收到的Message對象為服務端轉發(fā)來的文件消息,就將其讀取并保存到本地磁盤中。如下圖所示 :?
? ? ? ? ? ? ? ? 最后在View類中創(chuàng)建FileClientService對象,如下圖所示 :?
? ? ? ? ? ? ? ? 然后在對應的部分調用該對象的方法,如下圖所示:?
????????3.服務端 :?
? ? ? ? ? ? ? ? 服務端還是老樣子,在服務端的線程中增加一個else if 的Message類型判斷,如果判斷為發(fā)送文件的Message對象,就和私發(fā)消息一樣給轉發(fā)一下就OK了。如下圖所示 :?
? ? ? ? ? ? ? ? 截圖沒截到的部分,就是之前的老樣子——先通過線程管理類的得到線程的方法,根據(jù)接收者的id獲取對應的線程;然后再獲取線程持有的Socket對象,最后再獲取與該Socket對象相關聯(lián)的輸出流。
????????4.運行測試 :?
? ? ? ? ? ? ? ? 如下GIF圖演示 :?
七、最終代碼
? ? ? ? 1.客戶端 :?
? ? ? ? ? ? ? ? 1° View
package client.menu;
import client.service.FileClientService;
import client.service.MessageClientService;
import client.service.UserClientService;
import java.io.IOException;
import java.util.Scanner;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 菜單界面的顯示
* @PS : Run -> Edit Configurations -> Modify options -> allow multiple instances
*/
public class View {
private boolean loop = true; //控制是否顯示菜單
private String key = ""; //接收用戶的鍵盤輸入
private static Scanner sc = new Scanner(System.in); //靜態(tài)掃描儀
/*
將userClientService對象置為屬性,
該對象用于執(zhí)行用戶登錄/注冊等操作(該步驟將功能與界面聯(lián)系起來)
*/
private UserClientService userClientService = new UserClientService();
/*
將messageClientService對象置為屬性,
該對象用于消息的管理(該步驟將功能與界面聯(lián)系起來)
*/
private MessageClientService messageClientService = new MessageClientService();
/*
將fileClientService對象置為屬性,
該對象用于文件的發(fā)送(該步驟將功能與界面聯(lián)系起來)
*/
private FileClientService fileClientService = new FileClientService();
public static void main(String[] args) throws IOException {
new View().mainMenu();
System.out.println("客戶端退出...");
sc.close();
}
private void mainMenu() throws IOException {
while (loop) {
System.out.println("===========Welcome to the system of chat:===========");
System.out.println("\t\t1.登錄系統(tǒng)");
System.out.println("\t\t9.退出系統(tǒng)");
System.out.print("請輸入你的選擇:");
key = sc.nextLine();
switch (key) {
case "1" :
//登錄操作
System.out.print("請輸入用戶名:");
String userID = sc.nextLine();
System.out.print("請輸入密 碼:");
String password = sc.nextLine();
//驗證登錄的用戶是否合法(封裝思想)
if (userClientService.check(userID, password)) { //驗證成功
System.out.println("\n===========Welcome user " + userID + "===========");
//向用戶顯示二級菜單
while (loop) {
System.out.println("\n===========網(wǎng)絡通信系統(tǒng)二級菜單(user:" + userID + ")===========");
System.out.println("\t\t1.在線列表:");
System.out.println("\t\t2.群發(fā)消息:");
System.out.println("\t\t3.私發(fā)消息:");
System.out.println("\t\t4.文件發(fā)送:");
System.out.println("\t\t9.退出系統(tǒng):");
System.out.print("請輸入你的選擇:");
key = sc.nextLine();
switch (key) {
case "1" :
userClientService.onlineList();
break;
case "2" :
System.out.println("請輸入你要對大家說的話:");
String announcement = sc.nextLine();
//調用群發(fā)消息的方法
messageClientService.sendMessageToAll(announcement, userID);
break;
case "3" :
System.out.print("請輸入你想聊天的對象(在線),receiver = ");
String receiver = sc.nextLine();
System.out.print("請輸入你要說的話: ");
String content = sc.nextLine();
//調用私發(fā)消息的方法
messageClientService.sendMessageToOne(receiver, content, userID);
break;
case "4" :
System.out.print("請輸入你想發(fā)送文件的對象(在線),receiver = ");
String fileReceiver = sc.nextLine();
System.out.print("請輸入數(shù)據(jù)源文件的路徑, souPath = ");
String souPath = sc.nextLine();
System.out.print("請輸入目的地文件的路徑, desPath = ");
String desPath = sc.nextLine();
fileClientService.setFileToOne(souPath, desPath, userID, fileReceiver);
break;
case "9" :
userClientService.logout();
loop = false; //在二級菜單中用戶也可以直接選擇退出系統(tǒng)
break;
}
}
} else { //驗證失敗
System.out.println("登錄失?。≌堉匦聡L試!");
}
break;
case "9" :
loop = false; //將控制while循環(huán)的布爾變量設置為false
break;
}
}
}
}
? ? ? ? ? ? ? ? 2°?UserClientService
package client.service;
import mutual.Message;
import mutual.MessageType;
import mutual.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 登錄驗證
*/
public class UserClientService {
/*
將User對象設置成一個屬性,可利用getter和setter修改User對象的引用,便于操作。
Socket對象同樣也可能在其他類中使用,因此也設置為屬性。
*/
private User user = new User();
private Socket socket;
/** check方法可以向服務端發(fā)起用戶登錄的驗證 */
public boolean check(String userID, String password) throws IOException {
//局部變量
boolean b = false;
//初始化User對象
user.setId(userID);
user.setPwd(password);
//向服務端發(fā)送信息
try {
//1.獲取Socket對象
socket = new Socket(InetAddress.getByName("127.0.0.1"), 8888);
//2.獲取與Socket對象相關聯(lián)的對象處理流(輸出流)
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//3.序列化User對象,寫入數(shù)據(jù)通道(向服務端發(fā)送一個User對象,服務端會對這個User對象進行驗證)
oos.writeObject(user);
//.........
//4.獲取與Socket對象相關聯(lián)的對象處理流(輸入流)
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//5.讀取服務端傳輸過來的Message對象
Message message = (Message) ois.readObject(); //類型強轉
if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)) {
//創(chuàng)建線程對象(目的是為了與服務端保持通訊)
ClientConnectServiceThread ccst = new ClientConnectServiceThread(socket);
//啟動線程
ccst.start();
//將線程放入集合中統(tǒng)一管理
ControlClientConnectServiceThread.addClientConnectServiceThread(userID, ccst);
b = true;
} else {
//如果沒有啟動線程,關閉Socket對象。
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
/** onlineList方法可以向服務端請求拉取在線列表 */
public void onlineList() {
//向服務端發(fā)送一個Message對象,類型是MESSAGE_GET_ONLINE_FRIENDS
Message message = new Message();
message.setSender(user.getId()); //用戶登錄時已在check方法中設置了id的值,所以可直接用
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIENDS);
try {
//得到當前線程持有的Socket對象對應的對象處理流(輸出流)
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message); //向服務端發(fā)送“拉取在線用戶列表”的請求
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** logout方法可以退出當前用戶 */
public void logout() {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(user.getId()); //指定具體要退出的客戶端
//發(fā)送Message對象
try {
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message);
ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).setLoop(false);
System.out.println(user.getId() + " 退出系統(tǒng)...");
System.exit(0); //0表示正常退出當前“進程”。
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
? ? ? ? ? ? ? ? 3°?MessageClientService
package client.service;
import mutual.Message;
import mutual.MessageType;
import java.io.ObjectOutputStream;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 提供與消息有關的服務
*/
public class MessageClientService {
/**
* @param receiver : 消息的接收者
* @param content : 消息內(nèi)容
* @param sender : 消息的發(fā)送者
*/
public void sendMessageToOne(String receiver, String content, String sender) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMMON_MES); //消息類型
message.setReceiver(receiver); //消息接收者
message.setContent(content); //消息內(nèi)容
message.setSender(sender); //消息發(fā)送者
message.setSendTime(new java.util.Date().toString()); //發(fā)送時間
System.out.println(sender + " 對 " + receiver + " 說 \"" + content + "\"");
try {
//獲取發(fā)送消息的用戶的輸出流對象,并將上面的消息發(fā)給服務端
ClientConnectServiceThread clientConnectServiceThread = ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
ObjectOutputStream oos = new ObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param content : 群發(fā)消息的內(nèi)容
* @param sender : 群發(fā)消息的發(fā)送者
*/
public void sendMessageToAll(String content, String sender) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMMON_MES_ALL); //消息類型
message.setContent(content); //消息內(nèi)容
message.setSender(sender); //消息發(fā)送者
message.setSendTime(new java.util.Date().toString()); //消息發(fā)送時間
System.out.println(sender + " 對所有在線的??們說 \"" + content + "\"");
try {
//獲取發(fā)送消息的用戶的輸出流對象,并將上面的消息發(fā)給服務端
ClientConnectServiceThread clientConnectServiceThread = ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
ObjectOutputStream oos = new ObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
? ? ? ? ? ? ? ? 4°?FileClientService
package client.service;
import mutual.Message;
import mutual.MessageType;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 實現(xiàn)發(fā)送文件相關的功能
*/
public class FileClientService {
/**
* @param souPath : 數(shù)據(jù)源文件路徑
* @param desPath : 目的地文件路徑
* @param sender : 發(fā)送者(ID)
* @param receiver : 接收者(ID)
*/
public void setFileToOne(String souPath, String desPath, String sender, String receiver) {
Message message = new Message();
message.setMesType(MessageType.MESSAGE_FILE_TRANSMISSION);
message.setSouPath(souPath);
message.setDesPath(desPath);
message.setSender(sender);
message.setReceiver(receiver);
//1.讀取文件
/*
利用File類的length方法(獲取當前文件的大小,以字節(jié)計算),
可以得知要創(chuàng)建的字節(jié)數(shù)組的大?。? 因為length方法的返回值是long類型,所以此處需要類型強轉。
*/
byte[] file = new byte[(int)new File(souPath).length()];
//創(chuàng)建一個輸入流
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(souPath);
fileInputStream.read(file); //將file文件讀取到字節(jié)數(shù)組中。
//將文件包裝到Message對象
message.setFile(file);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//關閉輸入流
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//2.提示信息
System.out.println("\n" + sender + " 給 " + receiver + " 發(fā)送 " + souPath +
" 到對方電腦的目錄 " + desPath + " 下...");
//3.發(fā)送文件
try {
ObjectOutputStream oos =
new ObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
? ? ? ? ? ? ? ? 5°?ClientConnectServiceThread
package client.service;
import mutual.Message;
import mutual.MessageType;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 客戶端用于和服務端進行通訊的線程
*/
public class ClientConnectServiceThread extends Thread {
//該線程需要持有Socket對象
private Socket socket;
private boolean loop = true;
public ClientConnectServiceThread(Socket socket) {
this.socket = socket;
}
public Socket getSocket() {
return socket;
}
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
//∵Thread需要在后臺與服務器通信,因此使用while循環(huán)
while (loop) {
try {
System.out.println("客戶端線程,等待讀取來自服務器端的消息...");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
/*
如果服務端沒有發(fā)送Message對象到數(shù)據(jù)通道中,線程就會阻塞在這里。
*/
Message message = (Message) ois.readObject();
/**
判斷客戶端讀取到的Message的類型,并做出相應的業(yè)務處理。
*/
if (message.getMesType().equals(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS)) {
//若Message的類型是返回的在線用戶列表,取出在線列表并顯示,使用空格分隔不同用戶的id
String[] onlineUsers = message.getContent().split(" ");
System.out.println("\n===========在線用戶列表如下:===========");
for (int i = 0; i < onlineUsers.length; i++) {
System.out.println("用戶: " + onlineUsers[i]);
}
} else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES_ALL)) {
System.out.println("\n" + message.getSender() + " 對所有在線的??說 \"" +
message.getContent() + "\"");
} else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES)) {
System.out.println("\n" + message.getSender() + " 對 " +
message.getReceiver() + " 說 \"" + message.getContent() + "\"");
} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_TRANSMISSION)) {
System.out.println("\n" + message.getSender() + " 給 " + message.getReceiver() + " 發(fā)送 " + message.getSouPath() +
" 到對方電腦的目錄 " + message.getDesPath() + "下...");
FileOutputStream fileOutputStream = new FileOutputStream(message.getDesPath());
fileOutputStream.write(message.getFile());
fileOutputStream.close();
System.out.println("\n保存文件成功!");
} else {
System.out.println("...other content");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
? ? ? ? ? ? ? ? 6°?ControlClientConnectServiceThread
package client.service;
import java.util.HashMap;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 管理客戶端的線程
*/
public class ControlClientConnectServiceThread {
/*
使用HashMap類來管理多個線程(模擬數(shù)據(jù)庫),key表示用戶的id,value表示線程。
*/
private static HashMap<String, ClientConnectServiceThread> hashMap = new HashMap<>();
//添加線程的方法
public static void addClientConnectServiceThread(String userID, ClientConnectServiceThread ccst) {
hashMap.put(userID, ccst);
}
//取出線程的方法
public static ClientConnectServiceThread getClientConnectServiceThread(String userID) {
return hashMap.get(userID);
}
}
? ? ? ? 2.服務端 :?
? ? ? ? ? ? ? ? 1°?ChatServer
package server.service;
import mutual.Message;
import mutual.MessageType;
import mutual.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 服務端
*/
public class ChatServer {
//將ServerSocket設置為屬性,寫在main函數(shù)外
private ServerSocket serverSocket = null;
/**
將合法的用戶放入集合中(使用“id + user”的泛型),
建議使用ConcurrentHashMap集合,線程同步,可在多線程程序下安全使用。
*/
private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
static { //在靜態(tài)代碼塊中初始化validUsers集合對象
validUsers.put("Cyan", new User("Cyan", "RA9"));
validUsers.put("Rain", new User("Rain", "flo"));
validUsers.put("Ice", new User("Ice", "ais"));
validUsers.put("Five", new User("Five", "55555"));
validUsers.put("Kyrie", new User("Kyrie", "lrving"));
}
public boolean checkUser(String userID, String password) {
User user = validUsers.get(userID);
if (user == null) { //如果合法用戶集合中不存在當前用戶,直接返回false;
return false;
}
if (!(user.getPwd().equals(password))) { //如果存在該用戶,但密碼錯誤,返回false;
return false;
}
return true;
}
public ChatServer() {
//端口也可以寫在配置文件中
try {
System.out.println("服務端正在8888端口監(jiān)聽...");
serverSocket = new ServerSocket(8888);
/*
監(jiān)聽是不間斷的,當服務端和某個客戶端建立連接后,服務端會繼續(xù)監(jiān)聽。
*/
while (true) {
//獲取Socket類對象(服務端是通過accept方法來獲取Socket對象的)
Socket socket = serverSocket.accept();
//獲取Socket對象關聯(lián)的輸入流與輸出流(對象處理流)
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//客戶端第一次傳過來的是User對象
User user = (User) ois.readObject();
//創(chuàng)建一個Message對象,用于回復客戶端是否連接成功(Message對象寫在if-else語句外)
Message message = new Message();
if (checkUser(user.getId(), user.getPwd())) { //登錄成功
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
//將包含“登錄成功與否”信息的Message對象寫入數(shù)據(jù)通道
oos.writeObject(message);
//創(chuàng)建一個線程,與客戶端保持通訊
ServerConnectClientThread scct = new ServerConnectClientThread(socket, user.getId());
//啟動線程
scct.start();
//將線程放入集合中
ControlServerConnectClientThread.addServerConnectClientThread(user.getId(), scct);
} else { //登錄失敗
System.out.println("id = " + user.getId() + ",pwd = " + user.getPwd() + " 驗證失??!");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close(); //關閉Socket
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//若退出while循環(huán),說明服務端不再監(jiān)聽,需要關閉ServerSocket對象
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
? ? ? ? ? ? ? ? 2°?ServerConnectClientThread
package server.service;
import mutual.Message;
import mutual.MessageType;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 服務端的線程,用于和客戶端保持通訊
*/
public class ServerConnectClientThread extends Thread {
private Socket socket;
private String userID; //當前連接到服務端的用戶的id
public ServerConnectClientThread(Socket socket, String userID) {
this.socket = socket;
this.userID = userID;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
while (true) {
try {
System.out.println("服務端與客戶端" + userID + "保持通訊,讀取數(shù)據(jù)中...");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
/**
判斷服務端讀取到的Message的類型,并做出相應的業(yè)務處理。
*/
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIENDS)) {
System.out.println(message.getSender() + " 請求拉取在線用戶列表。");
String onlineUsers = ControlServerConnectClientThread.getOnlineFriends();
//構建Message對象,將獲取到的在線用戶列表的信息發(fā)送給客戶端
Message message2 = new Message();
message2.setMesType(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS);
message2.setContent(onlineUsers);
message2.setReceiver(message.getSender()); //發(fā)送者 ——> 接收者
/*
對象處理流寫在相應業(yè)務里面,各是各的,各用各的,不易沖突。
*/
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2);
} else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
//刪除負責與當前用戶通信的線程
System.out.println(message.getSender() + " 退出...");
Thread.sleep(500); //刪除線程前讓當前線程休眠0.5秒,避免EOF異常
ControlServerConnectClientThread.removeServerConnectClientThread(userID);
//關閉Socket(若忽略此步驟,客戶端無異常退出,服務端仍然異常。
socket.close();
//退出while循環(huán)
break;
} else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES_ALL)) {
//遍歷管理線程的集合
HashMap<String, ServerConnectClientThread> hashMap = ControlServerConnectClientThread.getHashMap();
Iterator<String> iterator = hashMap.keySet().iterator();
while (iterator.hasNext()) {
String onlUser = iterator.next();
//排除自己
if (!onlUser.equals(message.getSender())) {
ObjectOutputStream oos =
new ObjectOutputStream(hashMap.get(onlUser).getSocket().getOutputStream());
oos.writeObject(message);
}
}
} else if (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES)) {
/*
根據(jù)message對象中的receiver信息,獲取對應的線程;
進而獲取線程持有的Socket,以及與該Socket相關聯(lián)的對象處理流,
利用對象處理流將信息發(fā)送給另一個用戶
*/
ObjectOutputStream oos =
new ObjectOutputStream(ControlServerConnectClientThread.getServerConnectClientThread(message.getReceiver()).getSocket().getOutputStream());
oos.writeObject(message); //轉發(fā)(注意:要使用正確的輸出流)
/*
拓展 : 如果用戶不在線,可以將消息保存到數(shù)據(jù)庫,實現(xiàn)離線留言/離線發(fā)文件。
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_TRANSMISSION)) {
ObjectOutputStream oos =
new ObjectOutputStream(ControlServerConnectClientThread.getServerConnectClientThread(message.getReceiver()).getSocket().getOutputStream());
oos.writeObject(message);
} else {
System.out.println("...other content");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
? ? ? ? ? ? ? ? 3°?ControlServerConnectClientThread
package server.service;
import java.util.HashMap;
import java.util.Iterator;
/**
* @author : Cyan_RA9
* @version : 21.0
* @function : 用于管理服務端的線程
*/
public class ControlServerConnectClientThread {
private static HashMap<String, ServerConnectClientThread> hashMap = new HashMap<>();
public static HashMap<String, ServerConnectClientThread> getHashMap() {
return hashMap;
}
//添加線程到集合中
public static void addServerConnectClientThread(String userID, ServerConnectClientThread scct) {
hashMap.put(userID, scct);
}
//根據(jù)用戶的id獲取對應的線程
public static ServerConnectClientThread getServerConnectClientThread(String userID) {
return hashMap.get(userID);
}
//獲取在線用戶列表
public static String getOnlineFriends() {
/*
利用hashMap集合中的key是用戶id的特點,可以對hashMap對象進行遍歷,從而獲取用戶列表。
*/
Iterator<String> iterator = hashMap.keySet().iterator();
String onlineUsers = "";
while (iterator.hasNext()) {
onlineUsers += iterator.next() + " "; //加空格對應客戶端的split方法。
}
return onlineUsers;
}
//刪除指定線程
public static void removeServerConnectClientThread(String userID) {
hashMap.remove(userID);
}
}
? ? ? ? ? ? ? ? 4°?ChatFrame
package frame;
import server.service.ChatServer;
public class ChatFrame {
public static void main(String[] args) {
new ChatServer();
}
}
? ? ? ? 3.公共部分 :?
? ? ? ? ? ? ? ? 1° Message
package mutual;
import java.io.Serializable;
/**
* @author : Cyan_RA9
* @version : 21.0
* @message : information that are transmitted
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1L; //增強兼容性
private String sendTime; //發(fā)送時間
private String sender; //發(fā)送者
private String receiver; //接收者
private String content; //消息內(nèi)容
private String mesType; //消息類型
//與文件相關的屬性
private byte[] file; //文件
private int fileLen; //文件大小
private String souPath; //數(shù)據(jù)源文件路徑
private String desPath; //目的地文件路徑
public byte[] getFile() {
return file;
}
public void setFile(byte[] file) {
this.file = file;
}
public int getFileLen() {
return fileLen;
}
public void setFileLen(int fileLen) {
this.fileLen = fileLen;
}
public String getSouPath() {
return souPath;
}
public void setSouPath(String souPath) {
this.souPath = souPath;
}
public String getDesPath() {
return desPath;
}
public void setDesPath(String desPath) {
this.desPath = desPath;
}
public String getSendTime() {
return sendTime;
}
public void setSendTime(String sendTime) {
this.sendTime = sendTime;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getMesType() {
return mesType;
}
public void setMesType(String mesType) {
this.mesType = mesType;
}
}
? ? ? ? ? ? ? ? 2° MessageType
package mutual;
/**
* @author : Cyan_RA9
* @version : 21.0
* @meaning : Types of message
*/
public interface MessageType {
//定義常量
String MESSAGE_LOGIN_SUCCESS = "1"; //表示登錄成功
String MESSAGE_LOGIN_FAIL = "0"; //表示登錄失敗
String MESSAGE_COMMON_MES = "2"; //表示普通信息包
String MESSAGE_COMMON_MES_ALL = "6"; //表示群發(fā)的信息包
String MESSAGE_GET_ONLINE_FRIENDS = "3"; //表示請求拉取在線用戶的列表
String MESSAGE_RETURN_ONLINE_FRIENDS = "4"; //表示返回在線用戶的列表
String MESSAGE_CLIENT_EXIT = "5"; //表示客戶端請求退出系統(tǒng)
String MESSAGE_FILE_TRANSMISSION = "8"; //表示文件傳輸
}
? ? ? ? ? ? ? ? 3° User
? ? ? ? ? ? ? ? User類并無改動,準備工作中的User類,即是最終的User類。文章來源:http://www.zghlxwxcb.cn/news/detail-459788.html
? ? ? ? System.out.println("END-------------------------------------------------------------------------------");文章來源地址http://www.zghlxwxcb.cn/news/detail-459788.html
到了這里,關于java 多用戶即時通信系統(tǒng)的實現(xiàn) 萬字詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!