1、实现的效果
2、需求分析
2.1服务器
- 基于TCP协议创建服务器
- 响应客户端的连接请求
- 实时接收所有客户端发来的聊天消息 保存(显示)聊天消息,并转发消息给其它客户端
- 注:服务器要及时检查和客户端连接状态,如发现有客户端已经断开连接,需要删除通信套接字
2.1客户端
- 创建TCP套接字
- 配置服务器的IP、端口和聊天室昵称(可选)
- 向服务器发送连接请求
- 获取用户输入的聊天消息,发送到服务器
- 实时接收服务器转发的聊天消息,并显示
3、项目框架
4、UI界面设计
服务器界面
客户端界面
5、相关代码
5.1服务器
Service.pro
QT += network // 网络相关模块
serviceDialog.h 头文件
#ifndef SERVICEDIALOG_H
#define SERVICEDIALOG_H
#include <QDialog>
#include <QTcpServer> // tcp连接
#include <QTcpSocket> // tcp套接字
#include <QTimer> // 定时器
#include <QDebug>
namespace Ui {
class ServiceDialog;
}
class ServiceDialog : public QDialog
{
Q_OBJECT
public:
explicit ServiceDialog(QWidget *parent = 0);
~ServiceDialog();
private slots:
// 创建服务器按钮对应的槽函数
void on_createButton_clicked();
// 响应客户端连接请求的槽函数
void onNewConnection();
// 接收客户端消息的槽函数
void onReadyRead();
// 转发聊天消息给其它客户端
void sendMessage(const QByteArray & buf);
// 定时器到时后将执行的槽函数
void onTimeout(void);
private:
Ui::ServiceDialog *ui;
QTcpServer tcpService; // 服务器对象
quint16 port; // 服务器端口
QList<QTcpSocket *> tcpClientList; //容器:保存所有和客户端通信的套接字
QTimer timer ; //定时器
};
#endif // SERVICEDIALOG_H
serviceDialog.cpp 源文件
#include "servicedialog.h"
#include "ui_servicedialog.h"
ServiceDialog::ServiceDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ServiceDialog)
{
ui->setupUi(this);
//当有客户端向服务器发送连接请求,发送信号:newConnection
connect(&tcpService, SIGNAL(newConnection()),
this, SLOT(onNewConnection()));
//定时器到时发送信号:timeout
connect(&timer, SIGNAL(timeout()),
this, SLOT(onTimeout()));
}
ServiceDialog::~ServiceDialog()
{
delete ui;
}
// 创建服务器按钮对应的槽函数
void ServiceDialog::on_createButton_clicked()
{
// 获取服务器端口
port = ui->protEdit->text().toShort();
// 设置服务器IP和端口
if(tcpService.listen(QHostAddress::Any, port) == true)
{
qDebug() << "创建服务器成功!";
// 显示创建服务器消息
ui->listWidget->addItem("创建服务器成功");
ui->listWidget->scrollToBottom();
// 禁用"创建服务器按钮"和"端口输入"
ui->createButton->setEnabled(false);
ui->protEdit->setEnabled(false);
// 开启定时器
timer.start(3000);
}
else
{
qDebug() << "创建服务器失败";
}
}
// 响应客户端连接请求的槽函数
void ServiceDialog::onNewConnection()
{
// 获取和客户端通信的套接字
QTcpSocket * tcpClient = tcpService.nextPendingConnection();
// 保存套接字到容器
tcpClientList.append(tcpClient);
// 当客户端向服务器发送消息时,通信套接字发送信号:readyRead
connect(tcpClient, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
}
// 接收客户端消息的槽函数
void ServiceDialog::onReadyRead()
{
// 遍历容器哪个客户端给服务器发送了消息
for(int i=0; i<tcpClientList.size(); i++)
{
// bytesAvailable:获取当前套接字等待读取消息字节数
// 返回0表示没有消息; 返回>0,说明当前套接字有消息到来
if(tcpClientList.at(i)->bytesAvailable())
{
// 读取消息并保存
QByteArray buf = tcpClientList.at(i)->readAll();
// 显示聊天消息
ui->listWidget->addItem(buf);
ui->listWidget->scrollToBottom();
// 转发消息给所有在线客户端
sendMessage(buf);
}
}
}
// 转发聊天消息给其它客户端
void ServiceDialog::sendMessage(const QByteArray & buf)
{
for(int i=0; i<tcpClientList.size(); i++)
{
tcpClientList.at(i)->write(buf);
}
}
// 定时器到时后将执行的槽函数
void ServiceDialog::onTimeout(void)
{
// 遍历检查容器中保存的客户端通信套接字是否已经断开链接,如果是则删除
for(int i=0; i<tcpClientList.size(); i++)
{
// 客户端通信套接字是否已经断开链接
if(tcpClientList.at(i)->state() == QAbstractSocket::UnconnectedState)
{
tcpClientList.removeAt(i); // 删除
--i ;
}
}
}
/*
--i 的原因: 容器中的每一个套接字都读取到
容器: s1 s2 s4 s5
下标: 0 1 2 3 4
*/
5.2客户端
Client.pro
QT += network // 网络相关模块
clientDialog.h 头文件
#ifndef CLIENTDIALOG_H
#define CLIENTDIALOG_H
#include <QDialog>
#include <QTcpSocket> // 创建套接字
#include <QHostAddress> // 服务器IP地址
#include <QMessageBox>
#include <QDebug>
namespace Ui {
class ClientDialog;
}
class ClientDialog : public QDialog
{
Q_OBJECT
public:
explicit ClientDialog(QWidget *parent = 0);
~ClientDialog();
private slots:
// 发送按钮对应的槽函数
void on_sendButton_clicked();
// 连接服务器按钮对应的槽函数
void on_connectButton_clicked();
// 和服务器连接成功时执行的槽函数
void onConnected();
// 和服务器断开连接时执行的槽函数
void onDisConnected();
// 接收聊天消息的槽函数
void onReadyRead();
// 网络异常执行的槽函数
void onError();
private:
Ui::ClientDialog *ui;
bool status ; // 标识客户端状态: 在线/离线
QTcpSocket tcpSocket; // 和服务器通信的套接字
QHostAddress serviceIP; // 服务器地址
quint16 servicePort ; // 服务器端口
QString username ; // 聊天室昵称
};
#endif // CLIENTDIALOG_H
clientDialog.cpp 源文件
#include "clientdialog.h"
#include "ui_clientdialog.h"
ClientDialog::ClientDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ClientDialog)
{
ui->setupUi(this);
// 初始化
status = false ; //离线
connect(&tcpSocket, SIGNAL(connected()), this, SLOT(onConnected()));
connect(&tcpSocket, SIGNAL(disconnected()), this, SLOT(onDisConnected()));
connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead())) ;
connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError()));
}
ClientDialog::~ClientDialog()
{
delete ui;
}
// 发送按钮对应的槽函数
void ClientDialog::on_sendButton_clicked()
{
// 获取用户输入聊天消息
QString msg = ui->messageEdit->text();
if(msg == "")
{
return ;
}
msg = username + ": " + msg;
// 发送聊天消息
tcpSocket.write(msg.toUtf8());
// 清空消息输入框
ui->messageEdit->clear();
}
// 连接服务器按钮对应的槽函数
void ClientDialog::on_connectButton_clicked()
{
// 如果当前是离线状态,则建立和服务器连接
if(status == false)
{
// 获取服务器IP
serviceIP.setAddress(ui->serviceIpEdit->text());
// 获取服务器端口
servicePort = ui->servicePortEdit->text().toShort() ;
// 获取聊天室昵称
username = ui->usernameEdit->text();
// 向服务器发送连接请求
// 成功发送信号:connected
// 失败发送信号:error
tcpSocket.connectToHost(serviceIP, servicePort);
}
else // 如果当前是在线状态,则断开和服务器连接
{
//向服务发送离开聊天室的提示消息
QString msg = username + ":离开了聊天室!";
tcpSocket.write(msg.toUtf8());
// 关闭和服务器连接,发送信号: disconnected
tcpSocket.disconnectFromHost();
}
}
// 和服务器连接成功时执行的槽函数
void ClientDialog::onConnected()
{
status = true ; // 在线
ui->sendButton->setEnabled(true); // 恢复按钮状态
ui->serviceIpEdit->setEnabled(false); // 禁用IP
ui->servicePortEdit->setEnabled(false); // 禁用Port
ui->usernameEdit->setEnabled(false); // 禁用昵称
ui->connectButton->setText("离开聊天室");
// 向服务器发送进入聊天室提示消息
QString msg = username + ":进入了聊天室";
// toUtf8:QString转换QByteArray
tcpSocket.write(msg.toUtf8());
}
// 和服务器断开连接时执行的槽函数
void ClientDialog::onDisConnected()
{
status = false; // 离线
ui->sendButton->setEnabled(false); // 禁用按钮
ui->serviceIpEdit->setEnabled(true); // 恢复IP
ui->servicePortEdit->setEnabled(true); // 恢复Port
ui->usernameEdit->setEnabled(true); // 恢复昵称
ui->connectButton->setText("连接服务器");
}
// 接收聊天消息的槽函数
void ClientDialog::onReadyRead()
{
if(tcpSocket.bytesAvailable())
{
// 接收消息
QByteArray buf = tcpSocket.readAll();
// 显示消息
ui->listWidget->addItem(buf);
ui->listWidget->scrollToBottom();
}
}
// 网络异常执行的槽函数
void ClientDialog::onError()
{
// errorString():获取网络异常的原因
QMessageBox::critical(this,"ERROR",tcpSocket.errorString());
}
6、补充
有兴趣的小伙伴可以添加以下功能,完善项目。
- 优化界面
- 容错处理
- 增加功能:登录窗口、私聊、文件发送等
未完待续
有疑问或者有更好的项目,小伙伴们可以留言一起交流讨论!!