day02-程式碼實現01

多用戶即時通訊系統02

4.編碼實現01

4.1功能實現-用戶登錄

4.1.1功能說明

因為還沒有學習資料庫,我們人為規定 用戶名/id = 100,密碼為 123456 就可以登錄,其他用戶不能登錄,後面使用HashMap模擬資料庫,這樣就可以多個用戶登錄。

image-20220920184736385

4.1.2思路分析+框架圖

image-20220921230655619

用戶的登錄功能的流程:

  1. 用戶進入系統介面,選擇登錄

  2. 輸入登錄資訊之後,客戶端與服務端建立連接,把資訊發送給服務端

  3. 服務端接收資訊,在資料庫中進行校驗,作出判斷

  4. 服務端將判斷返回客戶端

  5. 客戶端接收資訊後,進行下一步操作(成功則進入二級菜單,失敗則請求用戶重新輸入)

4.1.3程式碼實現

4.1.3.1客戶端程式碼

image-20220921233227770

1.User類

用戶輸入登錄資訊後,在客戶端發送資訊給服務端的過程中,為了方便數據的解析(比如用戶id、用戶密碼等),使用對象來進行數據的傳輸

package qqcommon;

import java.io.Serializable;

/**
 * @author 李
 * @version 1.0
 * 表示一個用戶資訊
 */
public class User implements Serializable {//要序列化某個對象,實現介面Serializable
    private static final long serialVersionUID = 1L;//聲明序列化版本號,提高兼容性
    private String userId;//用戶id/用戶名
    private String password;//用戶密碼

    public User() {
    }

    public User(String uerId, String password) {
        this.userId = uerId;
        this.password = password;
    }

    public String getUerId() {
        return userId;
    }

    public void setUerId(String uerId) {
        this.userId = uerId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
2.Message類

表示客戶端和伺服器端通訊時的消息對象,目的同User

package qqcommon;

import java.io.Serializable;

/**
 * @author 李
 * @version 1.0
 * 表示客戶端和伺服器端通訊時的消息對象
 */
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;//聲明序列化版本號,提高兼容性
    //因為客戶端之間的通訊都要依靠服務端,因此資訊必須要寫明接收者和發送者等
    private String sender;//發送者
    private String getter;//接收者
    private String content;//消息內容
    private String sendTime;//發送時間  -因為發送時間也要被序列化,因此這裡也用String類型
    private String mesType;//消息類型[可以在介面中定義消息類型]

    public String getMesType() {
        return mesType;
    }

    public void setMesType(String mesType) {
        this.mesType = mesType;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getGetter() {
        return getter;
    }

    public void setGetter(String getter) {
        this.getter = getter;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getSendTime() {
        return sendTime;
    }

    public void setSendTime(String sendTime) {
        this.sendTime = sendTime;
    }
}
3.MessageType介面
package qqcommon;

/**
 * @author 李
 * @version 1.0
 * 表示消息類型
 */
public interface MessageType {
    //在介面中定義類一些常量,不同的常量的表示不同的消息類型
    String MESSAGE_LOGIN_SUCCEED = "1";//表示登錄成功
    String MESSAGE_LOGIN_FAIL = "2";//表示登錄失敗

}
4.QQView類

主程式入口,顯示菜單

package qqclient.view;


import qqclient.service.UserClientService;
import qqclient.utils.Utility;

/**
 * @author 李
 * @version 1.0
 */
public class QQView {
    private boolean loop = true;//控制是否顯示菜單
    private String key = "";//用來接收用戶的鍵盤輸入
    private UserClientService userClientService = new UserClientService();//該對象用於登錄服務/註冊用戶

    public static void main(String[] args) {
        new QQView().mainMenu();
        System.out.println("客戶端退出系統......");
    }

    //顯示主菜單
    public void mainMenu() {
        while (loop) {
            System.out.println("===========歡迎登陸網路通訊系統===========");
            System.out.println("\t\t 1 登錄系統");
            System.out.println("\t\t 9 退出系統");
            System.out.print("請輸入你的選擇:");
            key = Utility.readString(1);//讀取鍵盤輸入的指定長度的字元串

            //根據用戶的輸入,來處理不同的邏輯
            switch (key) {
                case "1":
                    System.out.print("請輸入用戶號:");
                    String userId = Utility.readString(50);//讀取鍵盤輸入的指定長度的字元串
                    System.out.print("請輸入密  碼:");
                    String pwd = Utility.readString(50);

                    // 到服務端去驗證用戶是否合法
                    //這裡有很多程式碼,我們這裡編寫一個類UserClientService[提供用戶登錄/註冊等功能]
                    if (userClientService.checkUser(userId, pwd)) {//驗證成功
                        System.out.println("=========歡迎(用戶 " + userId + " 登錄成功)=========");
                        //進入到二級菜單
                        while (loop) {
                            System.out.println("\n=========網路通訊系統二級菜單(用戶 " + userId + " )==========");
                            System.out.println("\t\t 1 顯示在線用戶列表");
                            System.out.println("\t\t 2 群發消息");
                            System.out.println("\t\t 3 私聊消息");
                            System.out.println("\t\t 4 發送文件");
                            System.out.println("\t\t 9 退出系統");
                            System.out.print("請輸入你的選擇:");
                            key = Utility.readString(1);
                            switch (key) {
                                case "1":
                                    System.out.println("顯示在線用戶列表");
                                    break;
                                case "2":
                                    System.out.println("群發消息");
                                    break;
                                case "3":
                                    System.out.println("私聊消息");
                                    break;
                                case "4":
                                    System.out.println("發送文件");
                                    break;
                                case "9":
                                    loop = false;//退出循環
                                    break;
                            }
                        }
                    } else {//驗證失敗
                        System.out.println("=========登錄失敗========");
                    }
                    break;
                case "9":
                    loop = false;//退出循環
                    break;
            }
        }
    }
}
5.Utility類

工具類,用於處理各種情況的用戶輸入,並且能夠按照程式設計師的需求,得到用戶的控制台輸入。

package qqclient.utils;


/**
 * 工具類的作用:
 * 處理各種情況的用戶輸入,並且能夠按照程式設計師的需求,得到用戶的控制台輸入。
 */

import java.util.Scanner;

/**


 */
public class Utility {
    //靜態屬性。。。
    private static Scanner scanner = new Scanner(System.in);


    /**
     * 功能:讀取鍵盤輸入的一個菜單選項,值:1——5的範圍
     * @return 1——5
     */
    public static char readMenuSelection() {
        char c;
        for (; ; ) {
            String str = readKeyBoard(1, false);//包含一個字元的字元串
            c = str.charAt(0);//將字元串轉換成字元char類型
            if (c != '1' && c != '2' &&
                    c != '3' && c != '4' && c != '5') {
                System.out.print("選擇錯誤,請重新輸入:");
            } else break;
        }
        return c;
    }

    /**
     * 功能:讀取鍵盤輸入的一個字元
     * @return 一個字元
     */
    public static char readChar() {
        String str = readKeyBoard(1, false);//就是一個字元
        return str.charAt(0);
    }

    /**
     * 功能:讀取鍵盤輸入的一個字元,如果直接按回車,則返回指定的默認值;否則返回輸入的那個字元
     * @param defaultValue 指定的默認值
     * @return 默認值或輸入的字元
     */

    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);//要麼是空字元串,要麼是一個字元
        return (str.length() == 0) ? defaultValue : str.charAt(0);
    }

    /**
     * 功能:讀取鍵盤輸入的整型,長度小於2位
     * @return 整數
     */
    public static int readInt() {
        int n;
        for (; ; ) {
            String str = readKeyBoard(10, false);//一個整數,長度<=10位
            try {
                n = Integer.parseInt(str);//將字元串轉換成整數
                break;
            } catch (NumberFormatException e) {
                System.out.print("數字輸入錯誤,請重新輸入:");
            }
        }
        return n;
    }

    /**
     * 功能:讀取鍵盤輸入的 整數或默認值,如果直接回車,則返回默認值,否則返回輸入的整數
     * @param defaultValue 指定的默認值
     * @return 整數或默認值
     */
    public static int readInt(int defaultValue) {
        int n;
        for (; ; ) {
            String str = readKeyBoard(10, true);
            if (str.equals("")) {
                return defaultValue;
            }

            //異常處理...
            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.print("數字輸入錯誤,請重新輸入:");
            }
        }
        return n;
    }

    /**
     * 功能:讀取鍵盤輸入的指定長度的字元串
     * @param limit 限制的長度
     * @return 指定長度的字元串
     */

    public static String readString(int limit) {
        return readKeyBoard(limit, false);
    }

    /**
     * 功能:讀取鍵盤輸入的指定長度的字元串或默認值,如果直接回車,返回默認值,否則返回字元串
     * @param limit 限制的長度
     * @param defaultValue 指定的默認值
     * @return 指定長度的字元串
     */

    public static String readString(int limit, String defaultValue) {
        String str = readKeyBoard(limit, true);
        return str.equals("") ? defaultValue : str;
    }


    /**
     * 功能:讀取鍵盤輸入的確認選項,Y或N
     * 將小的功能,封裝到一個方法中.
     * @return Y或N
     */
    public static char readConfirmSelection() {
        System.out.println("請輸入你的選擇(Y/N): 請小心選擇");
        char c;
        for (; ; ) {//無限循環
            //在這裡,將接受到字元,轉成了大寫字母
            //y => Y n=>N
            String str = readKeyBoard(1, false).toUpperCase();
            c = str.charAt(0);
            if (c == 'Y' || c == 'N') {
                break;
            } else {
                System.out.print("選擇錯誤,請重新輸入:");
            }
        }
        return c;
    }

    /**
     * 功能: 讀取一個字元串
     * @param limit 讀取的長度
     * @param blankReturn 如果為true ,表示 可以讀空字元串。
     *                   如果為false表示 不能讀空字元串。
     *
     * 如果輸入為空,或者輸入大於limit的長度,就會提示重新輸入。
     * @return
     */
    private static String readKeyBoard(int limit, boolean blankReturn) {

        //定義了字元串
        String line = "";

        //scanner.hasNextLine() 判斷有沒有下一行
        while (scanner.hasNextLine()) {
            line = scanner.nextLine();//讀取這一行

            //如果line.length=0, 即用戶沒有輸入任何內容,直接回車
            if (line.length() == 0) {
                if (blankReturn) return line;//如果blankReturn=true,可以返回空串
                else continue; //如果blankReturn=false,不接受空串,必須輸入內容
            }

            //如果用戶輸入的內容大於了 limit,就提示重寫輸入
            //如果用戶如的內容 >0 <= limit ,我就接受
            if (line.length() < 1 || line.length() > limit) {
                System.out.print("輸入長度(不能大於" + limit + ")錯誤,請重新輸入:");
                continue;
            }
            break;
        }

        return line;
    }
}
6.UserClientService類

該類完成用戶登錄驗證和用戶註冊等功能

package qqclient.service;

import qqcommon.Message;
import qqcommon.MessageType;
import qqcommon.User;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @author 李
 * @version 1.0
 * 該類完成用戶登錄驗證和用戶註冊等功能
 */
public class UserClientService {
    //因為我們可能在其他地方使用User資訊,因此做成成員屬性
    private User u = new User();
    //因為可能在其他地方使用Socket,因此也做成成員屬性
    private Socket socket;

    //根據用戶輸入的 userId 和 pwd,到伺服器去驗證該用戶是否合法
    public boolean checkUser(String userId, String pwd) {
        boolean b = false;
        //創建User對象
        u.setUerId(userId);
        u.setPassword(pwd);

        try {
            //連接伺服器,發送u對象
            socket = new Socket(InetAddress.getByName("192.168.1.6"), 9999);//指定服務端的ip和埠
            //獲取ObjectOutputStream對象(對象輸出流)
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(u);//向服務端發送User對象,伺服器會進行驗證

            //socket.shutdownOutput();

            //伺服器驗證後,客戶端讀取從服務端回送的Message對象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message ms = (Message) ois.readObject();//強轉為Message類型

            /**取出服務端返回的Message對象中的getMesType屬性
             * 如果為MESSAGE_LOGIN_SUCCEED則說明登錄成功,
             * 否則登錄失敗
             * */
            if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//登錄成功
                //創建一個伺服器保持通訊的執行緒
                // -->創建一個類 ClientConnectServerThread,
                // 把socket傳到該執行緒裡面,然後把執行緒放到一個集合裡面去管理
                ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
                //啟動客戶端的執行緒
                clientConnectServerThread.start();
                //這裡為了後面客戶端的擴展,我們將執行緒放入到集合裡面
                ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);
                b = true;
            } else {//登錄失敗
                //如果登錄失敗,就不啟動和伺服器通訊的執行緒,直接關閉socket
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return b;
    }
}
7.ClientConnectServerThread類

客戶端與服務端通過socket連接,考慮到一個客戶端會有多個socket的情況(服務端同此),將socket放在執行緒內

package qqclient.service;

import qqcommon.Message;

import java.io.ObjectInputStream;
import java.net.Socket;

/**
 * @author 李
 * @version 1.0
 */
public class ClientConnectServerThread extends Thread {
    //該執行緒需要持有socket
    private Socket socket;

    //構造器可以接收一個Socket對象
    public ClientConnectServerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //因為Thread需要在後台和伺服器通訊,因此我們使用while循環
        while (true) {
            try {
                System.out.println("客戶端執行緒,等待讀取從服務端發送的消息");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

                //如果伺服器沒有發送Message對象,執行緒會阻塞在這裡
                Message message = (Message) ois.readObject();
                //注意,後面我們需要使用message

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //為了更方便地得到socket,提供get方法
    public Socket getSocket() {
        return socket;
    }

    public void setSocket(Socket socket) {
        this.socket = socket;
    }
}
8.ManageClientConnectServerThread類

將執行緒都放入集合中,便於管理

package qqclient.service;

import java.util.HashMap;

/**
 * @author 李
 * @version 1.0
 * 該類管理客戶端連接到伺服器端的執行緒的類
 */
public class ManageClientConnectServerThread {
    //把多個執行緒放入到HashMap集合,key就是用戶id,value就是執行緒
    private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();

    //將某個執行緒加入到集合
    public static void addClientConnectServerThread(String userId, ClientConnectServerThread clientConnectServerThread) {
        hm.put(userId, clientConnectServerThread);
    }

    //通過userId可以得到一個對應的執行緒
    public static ClientConnectServerThread getClientConnectServerThread(String userId) {
        return hm.get(userId);
    }
}
4.1.3.2服務端程式碼

image-20220921234743331

服務端的User、Message、MessageType和客戶端一致,不再贅述

1.QQFrame
package qqframe;

import qqserver.server.QQServer;

/**
 * @author 李
 * @version 1.0
 * 該類創建QQServer,啟動後台的服務
 */
public class QQFrame {
    public static void main(String[] args) {
        new QQServer();
    }
}
2.QQServer
package qqserver.server;

import qqcommon.Message;
import qqcommon.MessageType;
import qqcommon.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author 李
 * @version 1.0
 * 這是服務端,在監聽埠9999,等待有客戶端連接,並保持通訊
 */
public class QQServer {

    private ServerSocket ss = null;
    //創建一個集合,存放多個用戶數據,如果是在集合裡面的用戶登錄,就認為是合法的(模擬資料庫)
    //這裡也可以使用 ConcurrentHashMap,可以處理並發的集合,沒有執行緒安全問題
    // HashMap 沒有處理執行緒安全,因此在多執行緒的情況下是不安全的
    // ConcurrentHashMap 處理的執行緒安全,即執行緒同步處理,在多執行緒的情況下是安全的
    private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();

    static {//在靜態程式碼塊,初始化 validUsers
        validUsers.put("100", new User("100", "123456"));
        validUsers.put("200", new User("200", "123456"));
        validUsers.put("300", new User("300", "123456"));
        validUsers.put("至尊寶", new User("至尊寶", "123456"));
        validUsers.put("紫霞仙子", new User("紫霞仙子", "123456"));
    }

    //驗證用戶是否有效的方法
    public boolean checkUser(String userId, String password) {
        User user = validUsers.get(userId);//在HashMap(模擬資料庫)裡面找key=userId對應的value=User對象
        //過關的驗證方式
        if (user == null) {//如果User為空(即Value為空)就說明 userId對應的key不存在
            return false;
        }
        if (!user.getPassword().equals(password)) {//如果userId正確,但是密碼錯誤
            return false;
        }
        return true;//如果userId和密碼都正確
    }

    public QQServer() {

        //注意:埠可以寫在配置文件裡面
        System.out.println("服務端在9999埠監聽...");
        try {
            ss = new ServerSocket(9999);

            while (true) {//循環監聽,當和某個客戶端建立連接後,會繼續監聽,因此使用while
                Socket socket = ss.accept();//如果沒有客戶端連接,就會阻塞在這裡,直到有新的客戶端來連接

                //得到socket關聯的對象輸入流
                ObjectInputStream ois =
                        new ObjectInputStream(socket.getInputStream());
                User u = (User) ois.readObject();//讀取客戶端發送的User對象

                /***
                 * 下面這裡其實是要到資料庫區驗證User的資訊,但是因為還沒學資料庫,先用規定的數據進行校驗
                 * HashMap模擬資料庫,可以多個用戶登錄
                 */
                //創建一個Message對象,用來回復客戶端
                Message message = new Message();
                //得到socket關聯的對象輸出流
                ObjectOutputStream oos =
                        new ObjectOutputStream(socket.getOutputStream());
                //驗證
                if (checkUser(u.getUserId(), u.getPassword())) {//登錄通過
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                    //將Message對象回復給客戶端
                    oos.writeObject(message);
                    //創建一個執行緒,和客戶端保持通訊,該執行緒需要持有socket對象
                    ServerConnectClientThread serverConnectClientThread =
                            new ServerConnectClientThread(socket, u.getUserId());
                    //啟動該執行緒
                    serverConnectClientThread.start();
                    //把該執行緒對象放入到一個集合中,進行管理
                    ManageClientThreads.addClientThread(u.getUserId(), serverConnectClientThread);

                } else {//登錄失敗
                    System.out.println("用戶 id=" + u.getUserId() + " pwd=" + u.getPassword() + " 驗證失敗");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    //關閉socket
                    socket.close();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            //如果伺服器退出了while循環,說明伺服器不再監聽,因此關閉ServerSock
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
3.ServerConnectClientThread

執行緒類,與客戶端的執行緒類同理

package qqserver.server;

import qqcommon.Message;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;

/**
 * @author 李
 * @version 1.0
 * 該類的一個對象和某個客戶端保持通訊
 */
public class ServerConnectClientThread extends Thread {
    private Socket socket;
    private String userId;//連接到服務端的用戶id


    public ServerConnectClientThread(Socket socket, String userId) {
        this.socket = socket;
        this.userId = userId;
    }

    @Override
    public void run() {//這裡執行緒處於run的狀態,可以發送/接收消息

        while (true) {
            try {
                System.out.println("服務端和客戶端" + userId + "保持通訊,讀取數據...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject();
                //後面會使用Message
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
4.ManageClientThreads

使用集合來存放執行緒,便於管理

package qqserver.server;

import java.util.HashMap;

/**
 * @author 李
 * @version 1.0
 * 該類用於管理和客戶端通訊的執行緒
 */
public class ManageClientThreads {
    private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();

    //添加執行緒對象到 hm集合中
    public static void addClientThread(String userId, ServerConnectClientThread serverConnectClientThread) {
        hm.put(userId, serverConnectClientThread);
    }

    //根據userId返回ServerConnectClientThread執行緒
    public static ServerConnectClientThread getServerConnectClientThread(String userId) {
        return hm.get(userId);
    }
}

運行截圖:

  1. 先運行服務端:

image-20220921235610095

  1. 運行客戶端,並輸入資訊:

    image-20220921235756369