多用戶即時通訊系統02 4.編碼實現01 4.1功能實現-用戶登錄 4.1.1功能說明 因為還沒有學習資料庫,我們人為規定 用戶名/id = 100,密碼為 123456 就可以登錄,其他用戶不能登錄,後面使用HashMap模擬資料庫,這樣就可以多個用戶登錄。 4.1.2思路分析+框架圖 用戶的登錄 ...
多用戶即時通訊系統02
4.編碼實現01
4.1功能實現-用戶登錄
4.1.1功能說明
因為還沒有學習資料庫,我們人為規定 用戶名/id = 100,密碼為 123456 就可以登錄,其他用戶不能登錄,後面使用HashMap模擬資料庫,這樣就可以多個用戶登錄。
4.1.2思路分析+框架圖
用戶的登錄功能的流程:
-
用戶進入系統界面,選擇登錄
-
輸入登錄信息之後,客戶端與服務端建立連接,把信息發送給服務端
-
服務端接收信息,在資料庫中進行校驗,作出判斷
-
服務端將判斷返回客戶端
-
客戶端接收信息後,進行下一步操作(成功則進入二級菜單,失敗則請求用戶重新輸入)
4.1.3代碼實現
4.1.3.1客戶端代碼
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服務端代碼
服務端的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);
}
}
運行截圖:
- 先運行服務端:
- 運行客戶端,並輸入信息:
此時服務端:
可以看到服務端成功地從客戶端獲取用戶登錄信息,匹配相應用戶後返回了信息,客戶端成功獲取到了服務端返回的信息,併進入了二級菜單。