QT基於TCP網絡聊天室
1.客戶端
1.1UI設計
分兩個部分,第一部分是消息區里面包含QPlainTextEdit
和QListWidget
,要顯示接收的消息和在線的成員。第二部分QLineEdit發生字符。
1.2 子模塊
1.2.1 登錄界面
登錄界面主要就是要有驗證碼,防止惡意程序的攻擊。通過paintEvent畫出一個白色矩形,在白色矩形里面顯示四個不同顏色的字母以及隨機出現的噪點。
代碼:
QLoginDialog.h
#ifndef _QLOGINDIALOG_H_
#define _QLOGINDIALOG_H_
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
//繼承自Dialog
class QLoginDialog : public QDialog
{
Q_OBJECT
public:
typedef bool (*ValFunc)(QString);
private:
QLabel UserLabel;
QLabel PwdLabel;
QLabel CaptLabel;
QLineEdit UserEdit;
QLineEdit PwdEdit;
QLineEdit CaptEdit;
QPushButton LoginBtn;
QPushButton CancelBtn;
QString m_user;
QString m_pwd;
QString m_captcha;
Qt::GlobalColor* m_colors;
QTimer m_timer;
ValFunc m_vf;
private slots:
void LoginBtn_Clicked();
void CancelBtn_Clicked();
void Timer_Timeout();
protected:
void paintEvent(QPaintEvent *);
QString getCaptcha();
Qt::GlobalColor* getColors();
void showEvent(QShowEvent *);
public:
QLoginDialog(QWidget *parent = 0);
QString getUser();
QString getPwd();
void setValFunc(ValFunc);
~QLoginDialog();
};
#endif
QLoginDialog.cpp
#include "QLoginDialog.h"
#include <QPainter>
#include <QTime>
#include <QMessageBox>
QLoginDialog::QLoginDialog(QWidget* parent) : QDialog(parent, Qt::WindowCloseButtonHint),
UserLabel(this), PwdLabel(this), CaptLabel(this),
UserEdit(this), PwdEdit(this), CaptEdit(this),
LoginBtn(this), CancelBtn(this),
m_vf(NULL)
{
UserLabel.setText("用戶名:");
UserLabel.move(20, 30);
UserLabel.resize(60, 25);
UserEdit.move(85, 30);
UserEdit.resize(180, 25);
PwdLabel.setText("密 碼:");
PwdLabel.move(20, 65);
PwdLabel.resize(60,25);
PwdEdit.move(85, 65);
PwdEdit.resize(180, 25);
PwdEdit.setEchoMode(QLineEdit::Password);
CaptLabel.setText("驗證碼:");
CaptLabel.move(20, 100);
CaptLabel.resize(60, 25);
CaptEdit.move(85, 100);
CaptEdit.resize(85, 25);
CancelBtn.setText("取消");
CancelBtn.move(85, 145);
CancelBtn.resize(85, 30);
LoginBtn.setText("登錄");
LoginBtn.move(180, 145);
LoginBtn.resize(85, 30);
m_timer.setParent(this);
setWindowTitle("登錄...");
setFixedSize(285, 205);
connect(&m_timer, SIGNAL(timeout()), this, SLOT(Timer_Timeout()));
connect(&LoginBtn, SIGNAL(clicked()), this, SLOT(LoginBtn_Clicked()));
connect(&CancelBtn, SIGNAL(clicked()), this, SLOT(CancelBtn_Clicked()));
//以時間作為種子,獲取隨機數
qsrand(QTime::currentTime().second() * 1000 + QTime::currentTime().msec());
m_timer.start(100);
}
void QLoginDialog::LoginBtn_Clicked()
{
//去除空格
QString captcha = CaptEdit.text().replace(" ", "");
//校驗驗證碼
if( m_captcha.toLower() == captcha.toLower() )
{
m_user = UserEdit.text().trimmed();
m_pwd = PwdEdit.text();
if( m_user == "" )
{
QMessageBox::information(this, "消息", "用戶名不能為空!");
}
else if( m_pwd == "" )
{
QMessageBox::information(this, "消息", "密碼不能為空!");
}
else if( (m_vf != NULL) && !(m_vf(m_user))) //一些非法字符不可輸入
{
QMessageBox::information(this, "消息", "用戶名非法,請重新輸入!");
}
else
{
done(Accepted);
}
}
else
{
QMessageBox::critical(this, "錯誤", "驗證碼輸入錯誤!");
m_captcha = getCaptcha();
CaptEdit.selectAll();
}
}
void QLoginDialog::setValFunc(ValFunc vf)
{
m_vf = vf;
}
void QLoginDialog::CancelBtn_Clicked()
{
done(Rejected);
}
QString QLoginDialog::getUser()
{
return m_user;
}
QString QLoginDialog::getPwd()
{
return m_pwd;
}
//獲取四個隨機的顏色
Qt::GlobalColor* QLoginDialog::getColors()
{
static Qt::GlobalColor colors[4];
for(int i=0; i<4; i++)
{
colors[i] = static_cast<Qt::GlobalColor>(2 + qrand() % 16);
}
return colors;
}
void QLoginDialog::Timer_Timeout()
{
//每100毫秒獲取四種顏色
m_colors = getColors();
//更新畫板
update();
}
void QLoginDialog::showEvent(QShowEvent* event)
{
//每次顯示之前,獲取驗證碼和顏色
m_captcha = getCaptcha();
m_colors = getColors();
QDialog::showEvent(event);
}
void QLoginDialog::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
//獲取一個矩形
painter.fillRect(180, 100, 84, 24, Qt::white);
painter.setFont(QFont("Comic Sans MS", 12));
//填充噪點,150個點隨機顯示
for(int i=0; i<150; i++)
{
painter.setPen(m_colors[i%4]);
painter.drawPoint(180 + qrand() % 84, 100 + qrand() % 24);
}
//驗證碼四個顏色
for(int i=0; i<4; i++)
{
painter.setPen(m_colors[i]);
painter.drawText(180 + 20 * i, 100, 20, 24, Qt::AlignCenter, QString(m_captcha[i]));
}
QDialog::paintEvent(event);
}
QString QLoginDialog::getCaptcha()
{
QString ret = "";
for(int i=0; i<4; i++)
{
int c = (qrand() % 2) ? 'a' : 'A';
ret += static_cast<QChar>(c + qrand() % 26);
}
return ret;
}
QLoginDialog::~QLoginDialog()
{
}
1.2.2 協議
1.2.2.1 協議的制訂
客戶端與服務端之間的操作需要用到協議,能夠方便解析客戶端需要的操作。
操作類型+數據長度+數據
TextMessage.h
#ifndef TEXTMESSAGE_H
#define TEXTMESSAGE_H
#include <QObject>
#include <QByteArray>
class TextMessage : public QObject
{
Q_OBJECT
QString m_type;
QString m_data;
public:
TextMessage(QObject *parent = 0);
TextMessage(QString type,QString data,QObject* parent = NULL);
QString type();
int length();
QString data();
QByteArray serizlize();
bool unserialize(QByteArray ba);
};
#endif // TEXTMESSAGE_H
TextMessage.cpp
#include "TextMessage.h"
#include <QString>
#include <QDebug>
TextMessage::TextMessage(QObject *parent) : QObject(parent)
{
m_type = "";
m_data = "";
}
TextMessage::TextMessage(QString type,QString data,QObject* parent)
{
m_type = type.trimmed();
if(m_type.length() < 4)
{
m_type += QString(4-m_type.length(),' ');
}
m_data = data.mid(0, 15000);
}
QString TextMessage::type()
{
return m_type.trimmed();
}
int TextMessage::length()
{
return m_data.length();
}
QString TextMessage::data()
{
return m_data;
}
//把需要發送的數據轉換成協議
QByteArray TextMessage::serizlize()
{
QByteArray ret;
QByteArray dba = m_data.toUtf8();
QString len = QString::asprintf("%X",dba.length());
if(len.length() < 4)
{
len += QString(4-len.length(),' ');
}
ret.append(m_type.toStdString().c_str(),4);
ret.append(len.toStdString().c_str(),4);
ret.append(dba);
return ret;
}
//把接收的協議轉換為具體的數據
bool TextMessage::unserialize(QByteArray ba)
{
bool ret = (ba.length() >= 8);
if(ret)
{
QString type = QString(ba.mid(0,4));
QString len = QString(ba.mid(4,4)).trimmed();
int l = len.toInt(&ret , 16);
ret = ret && (l == (ba.length() - 8));
if(ret)
{
m_type = type;
m_data = QString(ba.mid(8));
}
}
return ret;
}
1.2.2.2 協議裝配器
為什么需要裝配器,原因從服務端發來的數據可能是多個操作,可能出現粘包、數據不足一個包等情況,可以使用裝配器來進行數據的裝配。
TxtMsgAssmbler.h
#ifndef TXTMSGASSEMBLER_H
#define TXTMSGASSEMBLER_H
#include <QObject>
#include <QQueue>
#include <QSharedPointer>
#include "TextMessage.h"
class TxtMsgAssembler : public QObject
{
QQueue<char> m_queue;
QString m_type;
int m_length;
QByteArray m_data;
void clear();
QByteArray fetch(int n);
bool makeTypeAndLength();
TextMessage* makeMessage();
public:
TxtMsgAssembler(QObject *parent = 0);
void prepare(const char* data,int len);
QSharedPointer<TextMessage> assemble();
QSharedPointer<TextMessage> assemble(const char* data, int len);
void reset();
};
#endif // TXTMSGASSEMBLER_H
TxtMsgAssembler.cpp
#include "TxtMsgAssembler.h"
#include <QSharedPointer>
TxtMsgAssembler::TxtMsgAssembler(QObject *parent) : QObject(parent)
{
}
void TxtMsgAssembler::clear()
{
m_type = "";
m_data.clear();
m_length = 0;
}
//把數據從隊列中取出
QByteArray TxtMsgAssembler::fetch(int n)
{
QByteArray ret;
for(int i = 0; i < n;i++)
{
ret.append(m_queue.dequeue());
}
return ret;
}
//把數據放入隊列中
void TxtMsgAssembler::prepare(const char* data,int len)
{
if(data != NULL)
{
for(int i = 0; i < len; i++)
{
m_queue.enqueue(data[i]);
}
}
}
//把數據進行處理,識別出操作類型和獲取數據長度
bool TxtMsgAssembler::makeTypeAndLength()
{
bool ret = (m_queue.length() >= 8);
if(ret)
{
QString len = "";
m_type = QString(fetch(4));
len = QString(fetch(4));
m_length = len.trimmed().toInt(&ret,16);
if(!ret)
{
clear();
}
}
return ret;
}
//獲取數據
TextMessage* TxtMsgAssembler::makeMessage()
{
TextMessage* ret = NULL;
if(m_type != "")
{
int needed = m_length - m_data.length();
int n = (needed <= m_queue.length()) ? needed : m_queue.length();
m_data.append(fetch(n));
if(m_length == m_data.length())
{
ret = new TextMessage(m_type, QString(m_data));
}
}
return ret;
}
QSharedPointer<TextMessage> TxtMsgAssembler::assemble(const char* data, int len)
{
prepare(data, len);
return assemble();
}
//只要從網絡中接收到數據就調用該函數
QSharedPointer<TextMessage> TxtMsgAssembler::assemble()
{
TextMessage* ret = NULL;
bool tryMakeMsg = false;
if(m_type == "")
{
tryMakeMsg = makeTypeAndLength();
}
else
{
tryMakeMsg = true;
}
if(tryMakeMsg)
{
ret = makeMessage();
}
if(ret != NULL)
{
clear();
}
return QSharedPointer<TextMessage>(ret);
}
void TxtMsgAssembler::reset()
{
}
1.2.3 TCP客戶端
客戶端使用sokect通信,要提供read、send、connect、close等接口,還要提供當連接、關閉上服務器,要發送給服務端一些信息。
接收到信息時,要處理服務端傳入的數據。
ClientDemo.h
#ifndef CLIENTDEMO_H
#define CLIENTDEMO_H
#include <QObject>
#include <QTcpSocket>
#include "TextMessage.h"
#include "TxtMsgAssembler.h"
#include "txtmsghandler.h"
class ClientDemo : public QObject
{
Q_OBJECT
QTcpSocket m_client;
TxtMsgAssembler m_assmbler;
TxtMsgHandler *m_handler;
protected slots:
void onConnected();
void onDisconnected();
void onDataReady();
void onBytesWritten(qint64 bytes);
public:
explicit ClientDemo(QObject *parent = 0);
bool connectTo(QString ip, int port);
qint64 send(TextMessage& message);
qint64 available();
void setHandler(TxtMsgHandler* handler);
void close();
bool isValid();
signals:
public slots:
};
#endif // CLIENTDEMO_H
ClientDemo.cpp
#include "ClientDemo.h"
#include <QSharedPointer>
#include <QHostAddress>
#include <QDebug>
#include <QByteArray>
ClientDemo::ClientDemo(QObject *parent) : QObject(parent)
{
//當連接上的時,就會調用槽函數onConnected
connect(&m_client,SIGNAL(connected()),this,SLOT(onConnected()));
//當斷開連接時,就會調用onDisconnected
connect(&m_client,SIGNAL(disconnected()),this,SLOT(onDisconnected()));
//接收到服務端發送來的數據,調用prepare把數據保存到隊列中,調用assemble對數據進行解析
//調用m_handler->handle處理對應發送來的操作
connect(&m_client,SIGNAL(readyRead()),this,SLOT(onDataReady()));
//發送成功后,並沒有做什么
connect(&m_client,SIGNAL(bytesWritten(qint64)),this,SLOT(onBytesWritten(qint64)));
}
void ClientDemo::onConnected()
{
if(m_handler != NULL)
{
TextMessage conn("CONN",m_client.peerAddress().toString() + ":" + QString::number(m_client.peerPort()));
m_handler->handle(m_client,conn);
}
}
void ClientDemo::onDisconnected()
{
m_assmbler.reset();
if(m_handler != NULL)
{
TextMessage dscn("DSCN","");
m_handler->handle(m_client,dscn);
}
}
void ClientDemo::onDataReady()
{
char buf[256] = {0};
int len = 0;
while((len = m_client.read(buf,sizeof(buf))) > 0 )
{
QSharedPointer<TextMessage> ptm;
m_assmbler.prepare(buf,len);
while( (ptm = m_assmbler.assemble()) != NULL )
{
if((m_handler != NULL) )
{
//根據具體的type,處理不同的事件。
m_handler->handle(m_client, *ptm);
}
}
}
}
void ClientDemo::onBytesWritten(qint64 bytes)
{
(void)bytes;
}
bool ClientDemo::connectTo(QString ip, int port)
{
m_client.connectToHost(ip,port);
return m_client.waitForConnected();
}
qint64 ClientDemo::send(TextMessage& message)
{
QByteArray ba = message.serizlize();
return m_client.write(ba.data(),ba.length());
}
qint64 ClientDemo::available()
{
return m_client.bytesAvailable();
}
void ClientDemo::close()
{
m_client.close();
}
bool ClientDemo::isValid()
{
return m_client.isValid();
}
void ClientDemo::setHandler(TxtMsgHandler* handler)
{
m_handler = handler;
}
1.2.4 客戶端界面
1.在沒有登錄的時候,發送框和發送按鈕不能使用,只有登錄按鈕可以用。
2.管理員可以通過選擇群友,點擊右鍵對群友進行權限操作(禁言、恢復、封禁)。
3.被禁言、恢復、封禁的群友要出現提示。
4.通過選擇群友來進行私聊
5.群友上線或下線時,消息框內要有系統提示和及時刷新Listwidget
6.對於非法符號,要拒絕注冊
7.當客戶端接收到消息時,窗口要閃爍
8.按下回車可以發送消息
MainWinUI.h
#ifndef MAINWIN_H
#define MAINWIN_H
#include <QWidget>
#include <QVBoxLayout>
#include <QGroupBox>
#include <QPlainTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QListWidget>
#include "QLoginDialog.h"
#include "ClientDemo.h"
#include "txtmsghandler.h"
#include <QMap>
#include <QMenu>
class MainWin : public QWidget ,public TxtMsgHandler
{
Q_OBJECT
typedef void(MainWin::*MSGHandler)(QTcpSocket&,TextMessage&);
QVBoxLayout vMainLayout;
QGroupBox msgGrpBx;
QListWidget listWidget;
QGroupBox inputGrpBx;
QPlainTextEdit msgEditor;
QMenu listWidgetMenu;
QLineEdit inputEdit;
QPushButton logInOutBtn;
QPushButton sendBtn;
QLabel statusLbl;
QLoginDialog loginDlg;
QString m_level;
ClientDemo m_client;
//用鍵值保存type類型與對應的操作函數
QMap<QString,MSGHandler> m_handlerMap;
void initMember();
void initMsgGrpBx();
void initInputGrpBx();
void initListWidgetMenu();
void connectSlots();
void setCtrlEnabled(bool enabled);
QString getCheckedUserId();
//對應類型操作的函數
void CONN_Handler(QTcpSocket&,TextMessage&);
void DSCN_Handler(QTcpSocket&,TextMessage&);
void LIOK_Handler(QTcpSocket&,TextMessage&);
void LIER_Handler(QTcpSocket&,TextMessage&);
void MSGA_Handler(QTcpSocket&,TextMessage&);
void USER_Handler(QTcpSocket&,TextMessage&);
void CTRL_Handler(QTcpSocket&,TextMessage&);
private slots:
void sendBtnClicked();
void logInOutBtnClicked();
void listWidgetMenuClicked();
void listWidgetContextMenu(const QPoint&);
//重寫事件過濾器,為了處理回車鍵
bool eventFilter(QObject *, QEvent *);
public:
MainWin(QWidget *parent = 0);
void handle(QTcpSocket& obj,TextMessage& message);
~MainWin();
};
#endif // MAINWIN_H
MainWinUI.cpp
#include "MainWinUI.h"
#include <QHBoxLayout>
#include <QGridLayout>
#include <QAction>
MainWin::MainWin(QWidget *parent)
: QWidget(parent) , loginDlg(this)
{
initMember();
initMsgGrpBx();
initInputGrpBx();
initListWidgetMenu();
connectSlots();
vMainLayout.setSpacing(10);
vMainLayout.addWidget(&msgGrpBx);
vMainLayout.addWidget(&inputGrpBx);
setWindowTitle("R1CHIE聊天室");
setLayout(&vMainLayout);
setMinimumSize(550,400);
resize(550,400);
}
void MainWin::connectSlots()
{
connect(&sendBtn,SIGNAL(clicked(bool)),this,SLOT(sendBtnClicked()));
connect(&logInOutBtn,SIGNAL(clicked(bool)),this,SLOT(logInOutBtnClicked()));
//對群友點擊右鍵后出現的菜單的槽函數連接
connect(&listWidget,SIGNAL(customContextMenuRequested(QPoint)),this,SLOT(listWidgetContextMenu(QPoint)));
}
void MainWin::initMsgGrpBx()
{
QHBoxLayout* hbl = new QHBoxLayout();
hbl->setContentsMargins(2,5,2,2);
hbl->addWidget(&msgEditor,7);
hbl->addWidget(&listWidget,3);
msgEditor.setReadOnly(true);
msgEditor.setFocusPolicy(Qt::NoFocus);
listWidget.setFocusPolicy(Qt::NoFocus);
listWidget.setContextMenuPolicy(Qt::CustomContextMenu);
msgGrpBx.setLayout(hbl);
msgGrpBx.setTitle("聊天消息");
}
void MainWin::initInputGrpBx()
{
QGridLayout* gl = new QGridLayout();
gl->setSpacing(10);
gl->addWidget(&inputEdit,0,0,1,5);
gl->addWidget(&statusLbl,1,0,1,3);
gl->addWidget(&logInOutBtn,1,3);
gl->addWidget(&sendBtn,1,4);
inputEdit.setFixedHeight(23);
inputEdit.setEnabled(false);
inputEdit.installEventFilter(this);
statusLbl.setText("狀態: 未登錄");
logInOutBtn.setFixedHeight(30);
logInOutBtn.setText("登錄");
sendBtn.setFixedHeight(30);
sendBtn.setText("發送");
sendBtn.setEnabled(false);
inputGrpBx.setFixedHeight(100);
inputGrpBx.setLayout(gl);
inputGrpBx.setTitle("用戶名");
}
//對群友點擊右鍵后出現的菜單
void MainWin::initListWidgetMenu()
{
QAction* act = NULL;
act = listWidgetMenu.addAction("禁言",this,SLOT(listWidgetMenuClicked()));
act->setObjectName("silent");
act = listWidgetMenu.addAction("恢復",this,SLOT(listWidgetMenuClicked()));
act->setObjectName("recover");
act = listWidgetMenu.addAction("封禁",this,SLOT(listWidgetMenuClicked()));
act->setObjectName("kick");
}
void MainWin::setCtrlEnabled(bool enabled)
{
inputEdit.setEnabled(enabled);
statusLbl.setText(enabled ? "狀態: 連接成功" : "狀態: 未登錄");
logInOutBtn.setText(enabled ? "退出":"登錄");
sendBtn.setEnabled(enabled);
if(enabled)
{
inputEdit.setFocus();
}
else
{
msgEditor.clear();
listWidget.clear();
inputEdit.clear();
}
}
MainWin::~MainWin()
{
m_client.close();
}
MainWinSlot.cpp
#include "MainWinUI.h"
#include <QMessageBox>
#include <QDebug>
//當出現以下符號時,認定為非法用戶名
static bool ValidateUserID(QString id)
{
bool ret = true;
QString invalid = "~`!@#$%^&*()_+[]:?><,./;";
for(int i = 0; i < invalid.length(); i++)
{
if(id.contains(invalid[i]))
{
ret = false;
break;
}
}
return ret;
}
void MainWin::initMember()
{
#define MapToHandler(MSG) m_handlerMap.insert(#MSG,&MainWin::MSG##_Handler)
//把對應type類型的處理函數,用鍵值QMap保存
MapToHandler(CONN);
MapToHandler(DSCN);
MapToHandler(LIOK);
MapToHandler(LIER);
MapToHandler(MSGA);
MapToHandler(USER);
MapToHandler(CTRL);
m_client.setHandler(this);
}
//獲取listwidget選中群友
QString MainWin::getCheckedUserId()
{
QString ret = "";
for(int i = 0; i < listWidget.count(); i++)
{
QListWidgetItem *item = listWidget.item(i);
if(item->checkState() == Qt::Checked)
{
ret += item->text() + '\r';
}
}
return ret;
}
void MainWin::sendBtnClicked()
{
QString input = inputEdit.text().trimmed();
if(input != "")
{
QString self = inputGrpBx.title();
QString text = self + ":\n" + " " + input + "\n";
QString uid = getCheckedUserId();
bool ok = true;
//如果沒有選中群友,則認為是公聊
if(uid == "")
{
TextMessage tm("MSGA",text);
ok = m_client.send(tm);
}
else
{ //如果選中群友,則發給對應的群友
QString sid = (uid.indexOf(self) >= 0) ? uid : (uid + self + '\r');
TextMessage tm("MSGP",sid+text);
ok = m_client.send(tm);
}
if(ok )
{
inputEdit.clear();
}
}
}
void MainWin::listWidgetMenuClicked()
{
QAction *act = dynamic_cast<QAction*>(sender());
if(act != NULL)
{
const QList<QListWidgetItem*>& sl = listWidget.selectedItems();
if(sl.length() > 0)
{
QString user = sl.at(0)->text();
QString tip = "確認對用戶[" + user + "]進行 "+ act->text() + "操作嗎?";
//管理員對群友進行權限操作
if(QMessageBox::question(this,"提示",tip,QMessageBox::Yes,QMessageBox::No) == QMessageBox::Yes)
{
QString data =act->objectName() + '\r' + user;
TextMessage tm("ADMN",data);
m_client.send(tm);
}
}
else
{
QMessageBox::information(this,"提示","請選擇用戶!");
}
}
}
void MainWin::listWidgetContextMenu(const QPoint&)
{
//只有管理員可以操作群友
if(m_level == "admin")
listWidgetMenu.exec(QCursor::pos());
}
void MainWin::logInOutBtnClicked()
{
if(!m_client.isValid())
{
loginDlg.setValFunc(ValidateUserID);
if(loginDlg.exec() == QDialog::Accepted)
{
QString usr = loginDlg.getUser().trimmed();
QString pwd = loginDlg.getPwd();
if(m_client.connectTo("127.0.0.1",8890))
{
//setCtrlEnabled(true);
//連接服務器成功后,向服務器發送登錄的數據
TextMessage tm("LGIN" , usr + '\r' + pwd);
m_client.send(tm);
}
else
{
QMessageBox::critical(this,"失敗","連接不到遠程服務器");
}
}
}
else
{
m_client.close();
}
}
void MainWin::handle(QTcpSocket& obj,TextMessage& message)
{
if(m_handlerMap.contains(message.type()))
{
MSGHandler handler = m_handlerMap.value(message.type());
(this->*handler)(obj,message);
}
}
void MainWin::CONN_Handler(QTcpSocket& ,TextMessage& )
{
}
//自己或其它群友發送的消息
void MainWin::MSGA_Handler(QTcpSocket& ,TextMessage& message)
{
msgEditor.appendPlainText(message.data());
//接收到信息后,窗口閃爍
activateWindow();
}
//斷開連接
void MainWin::DSCN_Handler(QTcpSocket& ,TextMessage& )
{
setCtrlEnabled(false);
inputGrpBx.setTitle("用戶名");
m_level = "";
}
//這是服務器發來的登錄成功數據
void MainWin::LIOK_Handler(QTcpSocket& ,TextMessage& message)
{
QStringList rl = message.data().split("\r",QString::SkipEmptyParts);
QString id = rl[0];
QString status = rl[1];
m_level = rl[2];
//當前為禁言狀態
if(status == "slient")
{
setCtrlEnabled(true);
inputEdit.setEnabled(false);
sendBtn.setEnabled(false);
inputGrpBx.setTitle(id);
}
//當前為封禁狀態
else if(status == "kick")
{
m_client.close();
QMessageBox::information(this,"提示","賬號 [" + id + "]已被封禁");
}
else
{
setCtrlEnabled(true);
inputGrpBx.setTitle(id);
}
}
//這是登錄失敗的操作
void MainWin::LIER_Handler(QTcpSocket& ,TextMessage& )
{
QMessageBox::critical(this,"錯誤","身份驗證失敗");
m_client.close();
}
//每當有群友上線或下線時,刷新listwidget列表,由客戶端發送過來
void MainWin::USER_Handler(QTcpSocket&,TextMessage& message)
{
QStringList users = message.data().split("\r",QString::SkipEmptyParts);
//保存勾選狀態
QStringList checked = getCheckedUserId().split("\r",QString::SkipEmptyParts);
listWidget.clear();
//添加發送過來的用戶
for(int i = 0; i < users.length();i++)
{
QListWidgetItem *item = new QListWidgetItem();
if(item != NULL)
{
item->setText(users[i]);
item->setCheckState(Qt::Unchecked);
listWidget.addItem(item);
}
}
//勾選的狀態恢復
for(int i = 0; i < listWidget.count(); i++)
{
QListWidgetItem* item = listWidget.item(i);
for(int j = 0; j<checked.length(); j++)
{
if(checked.at(j) == item->text())
{
item->setCheckState(Qt::Checked);
}
}
}
}
//這是由服務器發送來的數據,管理員操作后的結果
void MainWin::CTRL_Handler(QTcpSocket&,TextMessage& message)
{
if(message.data() == "silent")
{
QMessageBox::information(this,"提示","你已被禁言!");
inputEdit.clear();
inputEdit.setEnabled(false);
sendBtn.setEnabled(false);
}
else if(message.data() == "recover" )
{
QMessageBox::information(this,"提示","你已被解除禁言!");
inputEdit.setEnabled(true);
sendBtn.setEnabled(true);
}
else if(message.data() == "kick")
{
QMessageBox::information(this,"提示","你已被封禁!");
m_client.close();
}
}
//事件過濾器的重寫,處理回車鍵
bool MainWin::eventFilter(QObject *obj, QEvent *evt)
{
if( (obj == &inputEdit ) && (evt->type() == QEvent::KeyPress))
{
QKeyEvent *key = dynamic_cast<QKeyEvent*>(evt);
if(key->text() == "\r")
{
sendBtnClicked();
return true;
}
}
return QWidget::eventFilter(obj,evt);
}
txtmsghandler.h
#ifndef TXTMSGHANDLER_H
#define TXTMSGHANDLER_H
#include <QTcpSocket>
#include "TextMessage.h"
class TxtMsgHandler
{
public:
virtual void handle(QTcpSocket&,TextMessage&) = 0;
};
#endif // TXTMSGHANDLER_H
1.2.5 main
main.cpp
#include "MainWinUI.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWin w;
w.show();
return a.exec();
}
2.服務端
2.1 子模塊
2.1.1 協議的訂制
與客戶端相同
2.1.2 協議裝配器
與客戶端相同
2.1.3 TCP客戶端
1.每當有客戶端連接進來時,要保存
2.每當有客戶端連接或斷開時,要有系統消息提示
ServerDemo.h
#ifndef SERVERDEMO_H
#define SERVERDEMO_H
#include <QObject>
#include <QTcpServer>
#include <QMap>
#include "TextMessage.h"
#include "TxtMsgAssembler.h"
#include "txtmsghandler.h"
class ServerDemo : public QObject
{
Q_OBJECT
QTcpServer m_server;
QMap<QTcpSocket*,TxtMsgAssembler*> m_map;
TxtMsgHandler* m_handler;
public:
explicit ServerDemo(QObject *parent = 0);
bool start(int port);
void stop();
void setHandler(TxtMsgHandler* handler);
~ServerDemo();
protected slots:
void onNewconnection();
void onConnected();
void onDisconnected();
void onDataReady();
void onBytesWritten(qint64 bytes);
};
#endif // SERVERDEMO_H
ServerDemo.cpp
#include "ServerDemo.h"
#include "TextMessage.h"
#include "TxtMsgAssembler.h"
#include <QSharedPointer>
#include <QHostAddress>
#include <QTcpSocket>
#include <QObject>
#include <QDebug>
ServerDemo::ServerDemo(QObject *parent) : QObject(parent)
{
//有新的客戶端連接
connect(&m_server,SIGNAL(newConnection()),this,SLOT(onNewconnection()));
}
//開始監聽,
bool ServerDemo::start(int port)
{
bool ret = true;
if(!m_server.isListening())
{
ret = m_server.listen(QHostAddress("127.0.0.1"),port);
}
return ret;
}
void ServerDemo::stop()
{
if(m_server.isListening())
m_server.close();
}
void ServerDemo::onNewconnection()
{
QTcpSocket *tcp = m_server.nextPendingConnection();
//給每一個客戶端創建一個裝配器
TxtMsgAssembler *as = new TxtMsgAssembler();
//保存每一個socket對應的裝配器
m_map.insert(tcp,as);
//給該socket建立連接
connect(tcp,SIGNAL(connected()),this,SLOT(onConnected()));
connect(tcp,SIGNAL(disconnected()),this,SLOT(onDisconnected()));
connect(tcp,SIGNAL(readyRead()),this,SLOT(onDataReady()));
connect(tcp,SIGNAL(bytesWritten(qint64)),this,SLOT(onBytesWritten(qint64)));
if(m_handler != NULL)
{
TextMessage msg("CONN",tcp->peerAddress().toString() + ":" + QString::number(tcp->peerPort()));
m_handler->handle(*tcp,msg);
}
}
void ServerDemo::onConnected()
{
}
void ServerDemo::onDisconnected()
{
//獲取斷開連接的客戶端
QTcpSocket *tcp = dynamic_cast<QTcpSocket*>(sender());
if(tcp != NULL)
{
//取出對應tcp與裝配器的映射,並且刪除該結點
m_map.take(tcp);
if(m_handler != NULL)
{
//調用斷開的handler函數
TextMessage msg("DSCN","");
m_handler->handle(*tcp,msg);
}
}
}
void ServerDemo::onDataReady()
{
QTcpSocket *tcp = dynamic_cast<QTcpSocket*>(sender());
char buf[256] = {0};
int len = 0;
if(tcp != NULL)
{
//取出tcp對應的裝配器
TxtMsgAssembler* assembler = m_map.value(tcp);
while( (len = tcp->read(buf,sizeof(buf))) > 0)
{
if(assembler != NULL)
{
QSharedPointer<TextMessage> ptm;
assembler->prepare(buf,len);
while( (ptm = assembler->assemble()) != NULL)
{
if(m_handler != NULL)
{
//處理對應類型的操作
m_handler->handle(*tcp,*ptm);
}
}
}
}
}
}
void ServerDemo::onBytesWritten(qint64 bytes)
{
(void)bytes;
}
ServerDemo::~ServerDemo()
{
const QObjectList& list = m_server.children();
//關閉所有連接的客戶端
for(int i = 0; i < list.length(); i++)
{
QTcpSocket *tcp = dynamic_cast<QTcpSocket*>(list[i]);
if(tcp != NULL)
{
tcp->close();
}
}
//對應的裝配器也刪除
const QList<TxtMsgAssembler*>& al = m_map.values();
for(int i = 0; i < al.length(); i++)
{
delete al.at(i);
}
}
void ServerDemo::setHandler(TxtMsgHandler* handler)
{
m_handler = handler;
}
ServerHandler.cpp
#include "ServerHandler.h"
#include <QDebug>
#include <QMap>
ServerHandler::ServerHandler()
{
#define MapToHandler(MSG) m_handlerMap.insert(#MSG,&ServerHandler::MSG##_Handler)
//連接各種類型的操作函數
MapToHandler(CONN);
MapToHandler(DSCN);
MapToHandler(LGIN);
MapToHandler(MSGA);
MapToHandler(MSGP);
MapToHandler(ADMN);
//添加管理員賬號
static Node admin;
admin.id = "admin";
admin.pwd = "123";
admin.level = "admin";
m_nodeList.append(&admin);
// m_handlerMap.insert("CONN",&ServerHandler::CONN_Handler);
// m_handlerMap.insert("DSCN",&ServerHandler::DSCN_Handler);
// m_handlerMap.insert("LGIN",&ServerHandler::LGIN_Handler);
// m_handlerMap.insert("MSGA",&ServerHandler::MSGA_Handler);
}
//抽象出來的獲取所有在線的群友
QString ServerHandler::getOnlineUserId()
{
QString ret = "";
for(int i = 0; i < m_nodeList.length(); i++)
{
Node* n = m_nodeList.at(i);
if(n->socket != NULL)
{
ret += n->id + '\r';
}
}
return ret;
}
void ServerHandler::handle(QTcpSocket& obj,TextMessage& message)
{
if(m_handlerMap.contains(message.type()))
{
MSGHandler handler = m_handlerMap.value(message.type());
(this->*handler)(obj,message);
}
}
//發送消息給所有在線的群友
void ServerHandler::MSGA_Handler(QTcpSocket&,TextMessage& message)
{
sendToAllOnlineUser(message);
}
void ServerHandler::CONN_Handler(QTcpSocket& ,TextMessage& )
{
}
//接收到客戶端發來的斷開連接操作
void ServerHandler::DSCN_Handler(QTcpSocket& obj,TextMessage& )
{
Node* n = NULL;
//
for(int i = 0; i < m_nodeList.length();i++)
{
n = m_nodeList.at(i);
if(n->socket == &obj)
{
n->socket = NULL;
break;
}
}
//發送給客戶端,客戶端用於更新在線列表
TextMessage tm("USER",getOnlineUserId());
sendToAllOnlineUser(tm);
//發送給客戶端,用於顯示系統消息
if(n != NULL)
{
TextMessage tm("MSGA", "[系統消息]: " + n->id + "退出聊天室");
sendToAllOnlineUser(tm);
}
}
//客戶端發送的上線數據
void ServerHandler::LGIN_Handler(QTcpSocket& obj,TextMessage& message)
{
QString data = message.data();
int index = data.indexOf('\r');
QString id = data.mid(0,index);
QString pwd = data.mid(index+1);
QString result = "";
QString status ="";
QString level = "";
index = -1;
//遍歷是否存在該用戶
for(int i = 0; i < m_nodeList.length(); i++)
{
if(id == m_nodeList.at(i)->id)
{
index = i;
break;
}
}
//如果不存在就注冊新用戶
if(index == -1)
{
Node* newNode = new Node();
if(newNode != NULL)
{
newNode->id = id;
newNode->pwd = pwd;
newNode->socket = &obj;
m_nodeList.append(newNode);
result = "LIOK";
status = newNode->status;
level = newNode->level;
}
else
{
result = "LIER";
}
}
else //如果存在就校驗密碼
{
Node* n = m_nodeList.at(index);
if(pwd == n->pwd)
{
n->socket = &obj;
result = "LIOK";
status = n->status;
level = n->level;
}
else
{
result = "LIER";
}
}
//發送給客戶端,當前是登錄成功還是失敗
obj.write(TextMessage(result,id + '\r' + status + '\r' + level).serizlize());
//登錄成功
if(result == "LIOK")
{
//發送給客戶端用於更新在線列表
TextMessage user("USER",getOnlineUserId());
sendToAllOnlineUser(user);
//發送系統消息
TextMessage msga("MSGA", "[系統消息]: " + id + "進入聊天室");
sendToAllOnlineUser(msga);
}
}
//私聊操作
void ServerHandler::MSGP_Handler(QTcpSocket&,TextMessage& message)
{
//分隔消息,在協議制訂時,最后被\r分開的是具體信息
QStringList tl = message.data().split("\r",QString::SkipEmptyParts);
const QByteArray& ba = TextMessage("MSGA",tl.last()).serizlize();
tl.removeLast();
//遍歷用戶,查看是否存在該用戶
for(int i = 0; i < tl.length(); i++)
{
for(int j = 0; j < m_nodeList.length(); j++)
{
Node *n = m_nodeList.at(j);
//如果存在,就發給對應的用戶
if( (tl[i] == n->id) && (n->socket != NULL))
{
n->socket->write(ba);
break;
}
}
}
}
//管理員權限操作
void ServerHandler::ADMN_Handler(QTcpSocket&,TextMessage& message)
{
//協議制訂:第一個為操作,第二個為用戶
QStringList data = message.data().split("\r",QString::SkipEmptyParts);
QString op = data[0];
QString id = data[1];
//遍歷查看用戶是否存在
for(int i = 0; i < m_nodeList.length();i++)
{
Node *n = m_nodeList.at(i);
//如果存在,並且被操作的用戶不是管理員身份才能被操作
if( (id == n->id) && (n->socket != NULL) && (n->level != "admin"))
{
n->socket->write(TextMessage("CTRL",op).serizlize());
n->status = op;
break;
}
}
}
//發送消息給所有在線的用戶
void ServerHandler::sendToAllOnlineUser(TextMessage& message)
{
const QByteArray& ba = message.serizlize();
for(int i = 0; i < m_nodeList.length();i++)
{
Node* n = m_nodeList.at(i);
if(n->socket != NULL)
{
n->socket->write(ba);
}
}
}
2.1.4 main
main.c
#include <QCoreApplication>
#include "ServerHandler.h"
#include "ServerDemo.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
ServerHandler handler;
ServerDemo server;
server.setHandler(&handler);
//開始監聽
server.start(8890);
return a.exec();
}