Java TCP實現高仿版QQ聊天
前言
記錄一下這套簡陋的系統說明,把所遇到的問題和難點以及操作說明在這篇文檔中說明清楚,當個回顧吧。萬一以后那一天查看也能及時找到問題。這套系統是在本人大三時期完成的,還存在很多bug。
這套聊天程序的完成也從網上借鑒了很多經驗。
這套系統會發在本人博客園和CSDN博客https://blog.csdn.net/qq_42376617/article/details/105723697做個記錄。不過遇到一些小問題我會在CSDN博客進行更新,煩請大家移步至CSDN查看最新的文章。
環境配置說明
1、JDK用的是1.8版本
2、開發工具使用的是eclipse Version: 2019-12。
3、數據庫用的是MySQL 8.0.17 Community Server 。
4、MySQL和Java連接用的是mysql-connector-java-8.0.16.jar。
5、代碼的編碼格式為GBK。
6、此外還用到了Photoshop、Navicat等工具。
因為之前沒有了解到eclipse的swing插件畫圖,swing界面都是一個一個敲出來了,當然了,有一部分也是從網友那邊借鑒過來的。想說的是用swing的時候用插件畫圖可以省去很多時間以及可能,推薦大家使用。
一般聊天的時候使用的是UDP協議,但在本系統中全部使用TCP協議,沒有使用UDP。
代碼結構
代碼結構

詳細說明
com包

dao包

view包

數據庫
利用Navicat導入項目中的SQL文件即可。
數據庫表
users表
| Account | NickName | PassWord | Sex | Birth | Signature | Headimage | LoginIP | LocalHost | RecentLoginTime | LoginState |
|---|---|---|---|---|---|---|---|---|---|---|
| 賬號 | 昵稱 | 密碼 | 性別 | 生日 | 個性簽名 | 頭像索引 | 登錄IP | 本地IP | 最近登錄時間 | 登錄狀態 |
message表
| mid | msg | sender | getter | sendtime | type |
|---|---|---|---|---|---|
| 自增長索引 | 信息正文 | 發送方 | 接收方 | 發送時間 | 消息類型(離線/窗口震動/常規) |
friendtable
| ID | MySelf_Account | Friends_Account | Note |
|---|---|---|---|
| 自增長索引 | 我的賬號 | 朋友賬號 | 朋友的備注 |
導入Eclipse
項目是一個JavaSE項目,按普通項目導入Eclipse即可。
導入后項目若出現紅色感嘆號,可以右鍵項目->Build Path->Configure Build Path->移除JRE System Library,然后再通過Add Library把JRE System Library添加回來即可。
若用的數據庫版本不一樣,可以換成相應版本號的jar包,然后右鍵jar包->Build Path->Add Build Path->之后若是項目出現紅色感嘆號,可以在項目的文件夾中找到.classpath文件,然后用記事本打開,刪除這一行即可。
<classpathentry kind="lib" path="lib/mysql-connector-java-8.0.16.jar"/>
注意:如果沒有更換jar包或者沒有更換jar包后沒有出現感嘆號,不需要上一步操作。
修改配置
打開utils.MyTools.java文件設置QQServerPort、QQServerIP,也就是運行時的端口號和IP地址,一般端口號不需要進行更改,修改QQServerIP即可(本系統測試時就是127.0.0.1,也就是本地IP地址,一般都是這個,如果可以運行就不需要改)。
數據庫配置文件放在項目根目錄下面的db.properties文件中,這個是肯定需要改的,這個怎么操作自行百度。這些信息是通過utils.PropertiesUtils.java進行IO讀取然后讀取到 JDBCUtils中。
啟動
第一步:啟動服務端,view.ServerFrame.java,點擊按鈕“啟動服務器”按鈕。
第二步:啟動客服端,view.LoginFrame.java,這是客戶端唯一的入口。
窗體截圖
服務端界面

登錄界面

注冊界面

主界面

修改個人信息

添加好友界面

聊天窗口

左側是聊天界面,右側是聊天記錄,可以從數據庫導入之前的聊天記錄,並且聊天時的數據也會進入到聊天記錄面板。
查看好友資料/刪除好友彈窗


離線消息

如果是離線消息或者當對方給我發消息的時候我沒有打開和對方的聊天窗口,那么實現彈窗提醒。
有待完善
發送文件
發送圖片
重點部分代碼說明
com.Server.java
@Override
public void run() {
try {
//1.設置服務器套接字 ServerSocket(int port)創建綁定到指定端口的服務器套接字
server = new ServerSocket(MyTools.QQServerPort);
while(isRunning) {
//2.阻塞式等待客戶端連接 (返回值)Socket accept()偵聽要連接到此套接字的客戶端並接受它。
client = server.accept();
System.out.println("一個客戶端已連接....");
input = new ObjectInputStream(client.getInputStream());
output = new ObjectOutputStream(client.getOutputStream());
String text = (String)input.readObject();//將客服端發來的信息轉換為String
String[] temp = text.split(MyTools.FLAGEND);//對客戶端發來的信息進行分割
Flag flag = MyTools.stringToFlagEnum(temp[0]);//獲得標志
String congtent = temp[1];//客服端發送過來的正文
dealWithMessage(flag,congtent);
}
} catch (IOException e) {
close(output,input,client,server);//釋放資源
} catch(ClassNotFoundException e){
e.printStackTrace();
}
}
/**
* 按照flag,進行對應的函數操作
* @param flag 標志
* @param message 文本正文
*/
public void dealWithMessage(Flag flag,String message) {
switch (flag) {
case REGISTER:doUserRegister(message);break;//處理注冊請求
case LOGIN:doUserLogin(message);break;//處理登錄請求
case ADD_Query:doAddUsersQuery(message);break;//處理添加好友查詢請求
case ADD:doAddUsersAdd(message);break;//處理添加好友請求
case DeleteUsers:doDeleteUsers(message);break;//處理刪除好友請求
case GET_FRIEND_INFO:doGetFriendInfo(message);break;//處理查看好友資料
// case SettingFriendNote:doSettingFriendNote(message);break;//設置備注
case GET_HeadImage:dogetUsersInfo(message);break;//處理獲取頭像請求
case SHOWHISTORY:doShowHistory(message);break;//將數據庫中的聊天記錄設置在聊天記錄面板
case UpdateUsersInfo:doUpdateUsers(message);break;//修改用戶資料
case UpdateUsersPass:doUpdateUsersPass(message);break;//修改用戶密碼
case GetNotOnlineMsg:dogetUnReadMsg(message);break;//獲取離線消息
default:break;
}
}
這部分代碼是開啟一個線程從com.LoginUser.java讀取IO流發送過來的數據,因為com.LoginUser.java發送來的數據進行了字符串拼接,也就是每個方法對應的處理有一個枚舉,然后在本方法中進行字符串分割,傳入dealWithMessage(Flag flag,String message)方法中,然后進行switch判斷,調用相應的方法。
utils.MyTools.java
public enum Flag{
REGISTER,//注冊
REGISTER_SUCCEED,//注冊成功
REGISTER_FAILED, //注冊失敗
ALREADY_REGISTER,//已經注冊
LOGIN,//登錄
LOGIN_SUCCEED,//登錄成功
LOGIN_FAILED,//登錄失敗
ALREADY_LOGIN,//已登錄
UNLOAD_LOGIN,//退出登錄
....
....
....
};
這是定義枚舉,也就是相應的操作數據定義一個枚舉,方便判斷IO流中的數據是需要進行什么操作。相當於一個標志了,在發送IO流數據的前面加上這些字段,可以讓IO流接收方判斷字段信息是准備進行什么操作。
utils.PropertiesUtils.java
public class PropertiesUtils {
private static Properties pro;
static{
pro = new Properties();
try {
//把配置文件的信息加載到 pro對象中
pro.load(new FileInputStream("db.properties"));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//根據屬性名來獲取屬性值
public static String getPropertiesValue(String key){
return pro.getProperty(key);
}
}
讀取db.properties文件中數據庫的配置文件,把數據庫信息放置在文件中的好處是當改數據庫信息的時候不需要對代碼進行改動。
view.ChatFrame.java
public void showMessage(JTextPane jtp, Message msg, boolean fromSelf) {
//設置顯示格式
SimpleAttributeSet attrset = new SimpleAttributeSet();
StyleConstants.setFontFamily(attrset, "仿宋");
StyleConstants.setFontSize(attrset,14);
Document docs = jtp.getDocument();
String info = null;
try {
if(fromSelf){//發出去的消息內容,發送的內容從本類中獲取
info = msg.getSendTime()+" ";//發送時間:綠色
StyleConstants.setForeground(attrset, Color.blue);
StyleConstants.setFontSize(attrset,16);
StyleConstants.setBold(attrset, false);
StyleConstants.setItalic(attrset, false);
docs.insertString(docs.getLength(), info, attrset);
info = "我:\n";//自己賬號:紫色
StyleConstants.setForeground(attrset, Color.blue);
StyleConstants.setFontSize(attrset,16);
StyleConstants.setBold(attrset, false);
StyleConstants.setItalic(attrset, false);
docs.insertString(docs.getLength(), info, attrset);
info = " "+msg.getContent()+"\n";//發送內容:黑色
StyleConstants.setFontSize(attrset,20);
StyleConstants.setForeground(attrset, Color.black);
StyleConstants.setBold(attrset, true);
StyleConstants.setItalic(attrset, true);
docs.insertString(docs.getLength(), info, attrset);
}else{//接收到的消息內容,接收的內容是ClientToServerThread類調用該方法
info = msg.getSendTime()+" ";//發送時間:綠色
StyleConstants.setForeground(attrset, Color.red);
StyleConstants.setFontSize(attrset,16);
StyleConstants.setBold(attrset, false);
StyleConstants.setItalic(attrset, false);
docs.insertString(docs.getLength(), info, attrset);
info = msg.getSenderName()+"("+msg.getSenderId()+"):\n";//對方賬號:紅色
StyleConstants.setForeground(attrset, Color.red);
StyleConstants.setFontSize(attrset,16);
StyleConstants.setBold(attrset, false);
StyleConstants.setItalic(attrset, false);
docs.insertString(docs.getLength(), info, attrset);
info = " "+msg.getContent()+"\n";//發送內容:藍色
StyleConstants.setFontSize(attrset,20);
StyleConstants.setForeground(attrset, Color.black);
StyleConstants.setBold(attrset, true);
StyleConstants.setItalic(attrset, true);
docs.insertString(docs.getLength(), info, attrset);
}
} catch (BadLocationException e) {
e.printStackTrace();
}
}
傳入的參數為(JTextPane jtp,Message msg,boolean fromSelf),jtp表示的是在哪個JTextPane中展示,msg表示聊天內容,fromSelf(true表示發送方,false表示接收方),然后下面的代碼是對接收方和發送方的格式進行設置,代碼中表示發送方為藍色,接收發為紅色。便於判斷信息是發送還是接收。
com.ManageFriendListFrame.java
public class ManageFriendListFrame {
private static Hashtable<String, MainFrame> friendListFrames = new Hashtable<>();
public static void addFriendListFrame(String frameName,MainFrame fl){
friendListFrames.put(frameName,fl);
}
public static MainFrame getFriendListFrame(String frameName){
return friendListFrames.get(frameName);
}
public static MainFrame removeFriendListFrame(String frameName){
return friendListFrames.remove(frameName);
}
}
這個類是管理用戶的聊天界面的,也就是說一個用戶只能打開一個主界面。主要是用Hashtable來管理,后來網上查了一下,Hashtable的父類已經過時,現在使用HashMap比較好。
Hashtable和HashMap的區別主要在於:
1、繼承的父類不同
HashMap繼承自AbstractMap類。但二者都實現了Map接口。
Hashtable繼承自Dictionary類,Dictionary類是一個已經被廢棄的類(見其源碼中的注釋)。父類都被廢棄,自然而然也沒人用它的子類Hashtable了。
2、HashMap線程不安全,HashTable線程安全
........
TCP協議
com.LoginUser.java
客戶端調用本類實現TCP發送,由於篇幅有限,就只截取部分代碼示例。構造方法初始化了Socket,ObjectOutputStream和ObjectInputStream,然后在sendLoginInfoToServer方法中把需要發送的方法組成字符串,在通過對象輸出流output.writeObject(text)輸出給Server.java;
再利用對象輸入流接收結果並轉換為實體類對象,Message msg = (Message) input.readObject(),就可以直接使用get方法獲取到服務端發送過來的值。
public class LoginUser {
private Socket client;
private ObjectOutputStream output;
private ObjectInputStream input;
public LoginUser(){
try {
client = new Socket(MyTools.QQServerIP, MyTools.QQServerPort);
output = new ObjectOutputStream(client.getOutputStream());
input = new ObjectInputStream(client.getInputStream());
} catch (IOException e) {
System.out.println("連接服務器失敗!");
e.printStackTrace();
}
}
/**
* 將通過校驗的登錄信息發送到服務器
* 並將得到的消息包返回(包含當前用戶的所有好友)
*/
public Message sendLoginInfoToServer(JFrame f,Users users){
//把要發送的信息組成字符串
String text = Flag.LOGIN+MyTools.FLAGEND
+users.getAccount()+MyTools.SPLIT1
+users.getPassword()+MyTools.SPLIT1
+users.getLoginState()+MyTools.SPLIT1
+users.getRecentLoginTime()+MyTools.SPLIT1
+users.getLoginIP()+MyTools.SPLIT1
+users.getLocalHost();
//檢測用戶輸入信息是否合理
Users u = checkLoginInfo(f,users.getAccount(),users.getPassword());
if(u != null){
try {
output.writeObject(text);//發送到服務器
Message msg = (Message) input.readObject();//接收返回結果
if(msg.getType() == Flag.LOGIN_SUCCEED){//登錄成功
ClientToServerThread th = new ClientToServerThread(client);
th.start();//創建與服務器通信線程
ManageThread.addThread(users.getAccount(),th);//為登錄者開啟一個線程
return msg;
} else if(msg.getType() == Flag.LOGIN_FAILED){
JOptionPane.showMessageDialog(f, "賬號或密碼輸入錯誤,請重新輸入!");
} else if(msg.getType() == Flag.ALREADY_LOGIN){
JOptionPane.showMessageDialog(f, "該用戶已登錄,請勿重復操作!");
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
return null;
}
com.Server.java
實現Runnable接口,定義ServerSocket,ObjectInputStream,ObjectOutputStream。開啟一個線程接收LoginUser.java發送過來的數據,派出客服代表與之聯系client = server.accept();然后把輸入流轉換為字符串,再把字符串分割為字符串數組,看服務端發送的是什么請求,之后調用dealWithMessage()方法進行處理。
在相對應的方法中把數據庫操作返回的結果通過輸出流發送給LoginUser.java。
這邊完成了TCP之間的溝通和交流。
public class Server implements Runnable{
private ServerSocket server;
private Socket client;
private ObjectInputStream input;
private ObjectOutputStream output;
private volatile boolean isRunning;
Message msg = new Message();
UserDao ud = new UserDao();
MsgDao md = new MsgDao();
public Server(){
isRunning = true;
new Thread(this).start();
}
@Override
public void run() {
try {
//1.設置服務器套接字 ServerSocket(int port)創建綁定到指定端口的服務器套接字
server = new ServerSocket(MyTools.QQServerPort);
while(isRunning) {
//2.阻塞式等待客戶端連接 (返回值)Socket accept()偵聽要連接到此套接字的客戶端並接受它。
client = server.accept();
input = new ObjectInputStream(client.getInputStream());
output = new ObjectOutputStream(client.getOutputStream());
String text = (String)input.readObject();//將客服端發來的信息轉換為String
String[] temp = text.split(MyTools.FLAGEND);//對客戶端發來的信息進行分割
Flag flag = MyTools.stringToFlagEnum(temp[0]);//獲得標志
String congtent = temp[1];//客服端發送過來的正文
dealWithMessage(flag,congtent);
}
} catch (IOException e) {
close(output,input,client,server);//釋放資源
} catch(ClassNotFoundException e){
e.printStackTrace();
}
}
public void dealWithMessage(Flag flag,String message) {
switch (flag) {
case REGISTER:doUserRegister(message);break;//處理注冊請求
case LOGIN:doUserLogin(message);break;//處理登錄請求
case ADD_Query:doAddUsersQuery(message);break;//處理添加好友查詢請求
case ADD:doAddUsersAdd(message);break;//處理添加好友請求
case DeleteUsers:doDeleteUsers(message);break;//處理刪除好友請求
case GET_FRIEND_INFO:doGetFriendInfo(message);break;//處理查看好友資料
case GET_HeadImage:dogetUsersInfo(message);break;//處理獲取頭像請求
case SHOWHISTORY:doShowHistory(message);break;//將數據庫的聊天記錄放在聊天記錄面板
case UpdateUsersInfo:doUpdateUsers(message);break;//修改用戶資料
case UpdateUsersPass:doUpdateUsersPass(message);break;//修改用戶密碼
case GetNotOnlineMsg:dogetUnReadMsg(message);break;//獲取離線消息
default:break;
}
代碼整理不易,需要源碼的可以聯系企鵝號863772270,請作者喝瓶水即可
