1.分析
1.1 在客戶端:
功能:
1.數據發送
2.數據接收
技術:
1.socket
2.輸入流和輸出流
3.多線程,客戶端功能模塊有兩個線程
聊天:
1.羣聊
2.私聊
私聊格式:@服務器用戶ID號:msg
1.2在服務器:
功能:
1.數據轉發
2.用戶註冊
技術:
1.ServerSocket
2.每一個用戶對應的Socket對象
3.多線程同時在線
4.HashMap<Integer,用戶>
數據轉發:
1.私聊前綴判斷
2.羣聊所有人發送
2.客戶端實現
數據發送:
使用【輸出流】發送數據給服務器
遵從Runnable接口
數據接收:
使用【輸入流】從服務器端接收數據
客戶端主方法:
用戶名提交
數據發送
數據接收
多線程啓動
3.資源關閉問題
代碼中操作了大量的輸入流和輸出流,這裏都需要進行【關閉】操作。
DataInputStream、DataOutputStream、BufferedReader、Socket
以上這些資源都是Closeable接口的實現類,都有對應的Close方法
於是可以封裝一個工具類:
提供一個closeAll方法,參數爲符合Closeable接口的實現類對象。
這裏使用可變長參數:Closeable… closeable
可變長參數在方法中使用的過程裏面是對應一個數組,這裏可以使用增強for循環來使用
工具類:
CloseUtil
public static void closeAll(Closeable… closeable)
4.功能拓展
1.用戶退出
用戶輸入指定字段之後可以退出
客戶端Socket服務
服務端Socket服務
涉及資源關閉,線程關閉
2.用戶異常退出
在運行過程中發現問題,需要及時處理,關閉對應的資源,終止對應的線程
3.服務器保存所有的聊天記錄
代碼:
1. client:
1.1Client
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Anonymous
* @description
* @date 2020/3/9 10:01
*
* 完成內容
* 1. 連接服務器
* 2. 啓動接收端線程和發送端線程
* 需要注意:
* 提供給服務器一個用戶名,這裏是在連接Socket獲取之後,第一次發送數據
* 給服務器時需要提供的,也就是第一次啓動Send發送線程完成的
*/
public class Client {
public static void main(String[] args) {
// 從鍵盤上獲取用戶輸入的數據
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Socket socket = null;
String name = null;
try {
System.out.println("請輸入你的用戶名:");
name = br.readLine();
if ("".equals(name)) {
return;
}
// 存在連接異常情況,考慮捕獲異常處理
socket = new Socket("192.168.31.154", 8888);
} catch (IOException e) {
System.err.println("連接失敗!!!");
try {
br.close();
} catch (IOException ex) {
ex.printStackTrace();
}
System.exit(0);
}
// 使用線程池啓動兩個線程,一個是發送一個接受
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(new ClientSend(socket, name));
pool.submit(new ClientReceive(socket));
}
}
1.2 ClientReceive
import com.qfedu.a_charoom.util.CloseUtil;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* @author Anonymous
* @description
* @date 2020/3/9 10:01
*
* 客戶端接收數據線程
*
* 這裏需要輸入流
* 輸入流是通過Socket對象獲取的,也就是在客戶端連接服務器之後纔可以獲取到輸入流
* 成員變量:
* 輸入流
* DataInputStream
* 標記是否連接
* 構造方法:
* 需要Socket作爲當前構造的參數
* 成員方法:
* 從服務器接收數據,展示
*/
public class ClientReceive implements Runnable {
/**
* 用於接收數據的輸入流對象
*/
private DataInputStream inputStream;
/**
* 是否連接狀態標記
*/
private boolean connection;
/**
* 根據客戶端連接服務器對應的Socket對象獲取輸入流對象
*
* @param socket 客戶端連接服務器對應的Socket
*/
public ClientReceive(Socket socket) {
try {
inputStream = new DataInputStream(socket.getInputStream());
connection = true;
} catch (IOException e) {
e.printStackTrace();
connection = false;
}
}
/**
* 接收數據並且展示
*/
public void receive() {
String msg = null;
try {
msg = inputStream.readUTF();
} catch (IOException e) {
e.printStackTrace();
/*
接收數據出現異常:
連接標記修改
不是null關閉資源
*/
connection = false;
CloseUtil.closeAll(inputStream);
}
System.out.println(msg);
}
/**
* 線程核心方法,只要連接狀態OK,始終保存接收狀態
*/
@Override
public void run() {
while (connection) {
receive();
}
}
}
1.3 ClientSend
import com.qfedu.a_charoom.util.CloseUtil;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* @author Anonymous
* @description 發送端
* @date 2020/3/9 10:01
*
* 這裏需要輸出流
* 輸出流對象需要通過Socket獲取,在當前客戶端連接到服務器之後,Socket對象存在
* 的情況下纔可以啓動的
* 成員變量:
* 輸出流
* DataOutputStream
* 需要一個從鍵盤上輸入數據使用的輸入流,獲取用戶輸入信息
* BufferedReader
* 標記是否連接
* 構造方法:
* 需要Socket作爲當前構造的參數,第一次訪問服務器需要帶有用戶名,用於註冊
* 成員方法:
* 發送數據給服務器
* 從鍵盤上獲取用戶的數據
*/
public class ClientSend implements Runnable {
/**
* 基於Socket獲取的輸出流對象,用於發送數據給服務器
*/
private DataOutputStream outputStream;
/**
* 從鍵盤上獲取用戶輸入的BufferedReader字符緩衝輸入流
*/
private BufferedReader console;
/**
* 是否連接
*/
private boolean connection;
/**
* 使用客戶端和服務器連接使用的Socket對象,和用戶指定的用戶名創建
* ClientSend線程對象,同時初始化輸出流和鍵盤錄入輸入流對象
*
* @param socket 客戶端連接服務器對應的Socket對象
* @param userName 用戶指定的用戶名,用於服務器註冊
*/
public ClientSend(Socket socket, String userName) {
// 初始化輸出流和輸出流
try {
outputStream = new DataOutputStream(socket.getOutputStream());
console = new BufferedReader(new InputStreamReader(System.in));
// 發送用戶名給服務器註冊 需要完成一個send方法
send(userName);
connection = true;
} catch (IOException e) {
e.printStackTrace();
// 連接標記關閉,同時處理對應的輸入流和鍵盤錄入流
connection = false;
CloseUtil.closeAll(outputStream, console);
}
}
/**
* 從鍵盤上獲取用戶輸入的數據
*
* @return 用戶輸入的數據字符串形式
*/
public String getMsgFromConsole() {
String msg = null;
try {
// 從鍵盤上讀取一行數據
msg = console.readLine();
} catch (IOException e) {
e.printStackTrace();
/*
發生異常:
1. connection連接標記改成false
2. 不是null需要關閉資源
outputStream, console
*/
connection = false;
CloseUtil.closeAll(outputStream, console);
}
return msg;
}
/**
* 發送數據給服務器
*
* @param msg 需要發送給服務器的數據
*/
public void send(String msg) {
// 如果這裏數據爲null,或者"" 不發送
try {
if (msg != null && !"".equals(msg)) {
outputStream.writeUTF(msg);
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
/*
發生異常:
1. connection連接標記改成false
2. 不是null需要關閉資源
outputStream, console
*/
connection = false;
CloseUtil.closeAll(outputStream, console);
}
}
/**
* 線程代碼,只要當前connection是連接狀態,一直執行send和getMsgFromConsole
*/
@Override
public void run() {
while (connection) {
send(getMsgFromConsole());
}
}
}
2. server
Server
import com.qfedu.a_charoom.util.CloseUtil;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;
/**
* @author Anonymous
* @description
* @date 2020/3/9 11:12
*
* 服務器需要轉發數據
* 1. 數據轉發
* 羣聊
* 私聊
* 2. 用戶註冊
* 用戶 ==> class
*
* 私聊,羣聊屬於用戶功能
* 用戶這裏看做是一個類
* 成員變量
* 輸入流
* 輸出流
* 用戶ID
* 用戶名
* 連接狀態標記
* 成員方法:
* 接收方法
* 利用客戶端連接服務器對應的Socket得到輸入流接收用戶發送的數據
* 發送方法
* 羣聊
* 遍歷整個有效用戶
* 私聊
* 找到對應用戶
* 利用客戶端連接服務器對應的Socket得到輸出流發送數據
* 【成員內部類】
* 用戶做出一個成員內部類
* 作爲Server服務器類的一個成員變量內部類
*
* 用戶註冊流程
* 1. ServerSocket Accept客戶端連接,獲取對應Socket對象
* 2. 記錄在線人數,創建一個新的UserSocket
* 3. Map中映射對應的UserSocket,Key 爲ID, Value是UserSocket
*
*/
public class Server {
/**
* 用戶ID和UserSocket映射
*/
private HashMap<Integer, UserSocket> userMap;
/**
* 累加訪客人數
*/
private static int count = 0;
/**
* Server構造方法,用於初始化底層保存數據的HashMap雙邊隊列
*/
public Server() {
userMap = new HashMap<>();
}
/**
* 服務器啓動方法
*
* @throws IOException IO異常
*/
public void start() throws IOException {
// 啓動服務器,同時監聽8888端口
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服務器啓動");
// 服務器始終處於一個保存連接的狀態
while (true) {
// 接收客戶端請求,得到一個Socket對象
Socket socket = serverSocket.accept();
// 創建UserSocket用於註冊,並且保存到userMap當中
count += 1;
UserSocket userSocket = new UserSocket(count, socket);
userMap.put(count, userSocket);
// 啓動當前UserSocket服務
new Thread(userSocket).start();
}
}
/**
* @author Anonymous
* @description 用戶Socket類,需要完成綁定操作,並且是一個線程類
* @date 2020/3/9 11:23
*/
class UserSocket implements Runnable {
/*
* 對應當前客戶端連接服務器對應Socket生成輸入流和輸出流
*/
private DataInputStream inputStream;
private DataOutputStream outputStream;
/**
* 用戶ID號,是當前用戶的唯一表示,不可以重複
*/
private int userId;
/**
* 用戶名,用於註冊,同時在發送數據時給予其他用戶標記
*/
private String userName;
/**
* 是否連接狀態標記
*/
private boolean connetion;
/**
* 創建UserSocket對象,需要的參數使用userID,和對應的Socket對象
*
* @param userId 當前用戶的ID號
* @param socket 客戶端連接服務器對應的Socket
*/
public UserSocket(int userId, Socket socket) {
this.userId = userId;
try {
inputStream = new DataInputStream(socket.getInputStream());
outputStream = new DataOutputStream(socket.getOutputStream());
connetion = true;
} catch (IOException e) {
e.printStackTrace();
connetion = false;
}
try {
// 用戶在創建Send線程時,首先會將用戶的名字發送給服務器
assert inputStream != null;
this.userName = inputStream.readUTF();
// 廣播告知所有人,ID:XXX 姓名: XXX 上線 【羣聊,系統廣播】
sendOther("ID:" + this.userId + " " + this.userName + "來到直播間", true);
// 服務器告訴當前客戶端,你已經進入聊天室
send("歡迎來到聊天室");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 接收客戶端發送的數據,用於轉發操作
*
* @return 用戶發送的數據
*/
public String receive() {
String msg = null;
try {
// 接收用戶發送到服務器需要服務器轉發的數據
msg = inputStream.readUTF();
} catch (IOException e) {
e.printStackTrace();
/*
發生異常:
1. 連接狀態修改
2. 關閉資源
3. 刪除在userMap中對應的數據【留下】
對號是書籤 快捷鍵 F11
查看當前項目所有的書籤 ALT + 2
*/
connetion = false;
CloseUtil.closeAll(inputStream, outputStream);
}
return msg;
}
/**
* 發送數據到客戶端
*
* @param msg 需要發送的數據
*/
public void send(String msg) {
try {
outputStream.writeUTF(msg);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
/*
發生異常:
1. 連接狀態修改
2. 關閉資源
3. 刪除在userMap中對應的數據
*/
connetion = false;
CloseUtil.closeAll(inputStream, outputStream);
}
}
/*
這裏需要一個方法
1. 私聊判斷
2. 羣聊
這裏需要根據msg進行判斷
1. 如果是@數字: 前綴
私聊
通過HashMap --> ID --> UserSocket --> 轉發消息
2. 非@數字:開頭羣聊
a. 系統播報
b. 私人發送
這裏需要做一個標記
獲取所有在線用戶,判斷除自己之外,其他人轉發消息
*/
/**
* 轉發數據判斷方法,msg需要處理,選擇對應的私聊和羣發,同時要判斷是否是系統發送消息
*
* @param msg 需要轉發的消息
* @param sys 系統標記
*/
public void sendOther(String msg, boolean sys) {
if (msg.startsWith("@") && msg.contains(":")) {
// 私聊
// @1:XXXXXX
Integer id = Integer.parseInt(msg.substring(1, msg.indexOf(":")));
String newMsg = msg.substring(msg.indexOf(":"));
UserSocket userSocket = userMap.get(id);
// 如果沒有對應的UserSocket用戶存在,無法發送消息
if (userSocket != null) {
// ID:1 小磊磊悄悄的對你說:XXXX
userSocket.send("ID:" + this.userId + " " + this.userName + "悄悄的對你說" + msg);
}
} else {
// 羣聊
// 從userMap中獲取對應的所有value,也就是所有UserSocket對象Collection集合
Collection<UserSocket> values = userMap.values();
for (UserSocket userSocket : values) {
// 不需要將消息發送給自己
if (userSocket != this) {
// 判斷是不是系統消息
if (sys) {
userSocket.send("系統公告:" + msg);
} else {
userSocket.send("ID:" + this.userId + " " + this.userName + msg);
}
}
}
}
}
/**
* 線程代碼
*/
@Override
public void run() {
while (connetion) {
// 使用receive收到的消息作爲參數,同時標記非系統消息,調用sendOther
sendOther(receive(), false);
}
}
}
public static void main(String[] args) {
// 啓動服務器!!!
Server server = new Server();
try {
server.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. util
CloseUtil
import java.io.Closeable;
import java.io.IOException;
/**
* @author Anonymous
* @description 關閉資源的工具類
* @date 2020/3/9 15:02
*/
public class CloseUtil {
/**
* 關閉代碼中所需資源的close工具類方法
*
* @param closeable 要求爲符合Closeable接口的實現類對象,爲不定長參數
*/
public static void closeAll(Closeable... closeable) {
/*
不定長參數可以按照數組的形式來處理
*/
try {
for (Closeable source : closeable) {
if (source != null) {
source.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}