day02-程式碼實現01
多用戶即時通訊系統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);
}
}
運行截圖:
- 先運行服務端:
-
運行客戶端,並輸入資訊: