採用TCP協議實現多人聊天室

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();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章