1. 需求分析
2. 概要设计
2.1 服务器概要设计
2.2 客户端概要设计
3. 详细设计
3.1 服务端的伪代码
- QTcpServer::listen()类似于TCP编程中的bind(将套接字和ip、端口号 绑定在一起)
- QHostAddress::Any 是 0.0.0.0 代表本机任意网卡的地址
3.1.0 创建TCP服务器
3.1.1 响应客户端请求
- newConnection()信号触发自定义槽函数onNewConnection(), 槽函数中会调用 nextPendingConnection() 获取和客户端通信的套接字
- 然后把套接字(连接?)保存到容器当中。 然后当客户端通过套接字发过来消息时,将触发readyRead()信号
3.1.2 接收客户端请求
- 当消息发来时,容器中可能会有多个套接字,需要遍历检查是哪个客户端发来的消息
- readyRead()信号触发槽函数onReadyRead(), 这个槽函数去遍历检查是哪个客户端发来的消息,然后完成消息的接收。
3.2 客户端的伪代码
3.2.0 创建TCP套接字和服务器建立连接
- connectToHost() 类似TCP编程中的connect(), 向指定的IP和端口发送连接请求,建立三次握手。
- 如果希望连接成功时有什么操作,可以通过onConnected()槽函数来完成。
- 通信套接字收到对方发来的消息时,都会产生readyRead()信号, onReadyRead()槽函数完成消息的接收。
3.2.1 发送聊天消息
- 发送消息控件的槽函数
3.2.2 接收聊天消息
- readyRead()消息的槽函数, 下面if的作用是如果有消息才去读取消息,不然如果没有消息时去读取会阻塞
4. 代码编写
4.1 服务器代码示例
- 在工程文件里加上network
- list Widget控件可以显示聊天过程,然后可以把聊天消息保存到本地,这里就不写保存的操作了
遍历容器如果有删除操作需要注意的点。
容器元素 : s1 s2 s3 s4 s5
下标 : 0 1 2 3 4
通过下标遍历检查容器中保存的客户端通信套接字是否已经断开连接,如果是则删除
如果当遍历到下标为2的元素s3对应的套接字已经断开连接,那么就删除s3, s4会自动向前移动到下标为2的位置
然后从下标3开始继续遍历,这样就会跳过s4.
所以,当遍历时发现需要删除元素时,下标要自减1,才能继续遍历。
sreverdialog.h
#ifndef SREVERDIALOG_H
#define SREVERDIALOG_H
#include <QDialog>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
#include <QTimer>
namespace Ui {
class SreverDialog;
}
class SreverDialog : public QDialog
{
Q_OBJECT
public:
explicit SreverDialog(QWidget *parent = 0);
~SreverDialog();
private slots:
// slot func of "create server" button
void on_pushButton_clicked();
// slot func of corresponding the client connection request (NewConnection())
void onNewConnection();
// slot func of receiving the client message (ReadyRead)
void onReadyRead();
// slot func of forwording the message to other client
void sendMessge(const QByteArray& buf);
// slot func of timer
void onTimeout(void);
private:
Ui::SreverDialog *ui;
QTcpServer tcpServer;
quint16 port; // server port
QList <QTcpSocket*> tcpClientList; //vector: save all sockets that communicate with client
QTimer timer; // timer (ji shi qi)
};
#endif // SREVERDIALOG_H
sreverdialog.cpp
#include "sreverdialog.h"
#include "ui_sreverdialog.h"
SreverDialog::SreverDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::SreverDialog)
{
ui->setupUi(this);
// When a client sends a request to the server, newConnection() signal is generated
connect(&tcpServer, SIGNAL(newConnection()), this, SLOT(onNewConnection()));
// send timeout signal every three seconds
connect(&timer, SIGNAL(timeout()), SLOT(onTimeout()));
}
SreverDialog::~SreverDialog()
{
delete ui;
}
// slot func of "create server" button
void SreverDialog::on_pushButton_clicked()
{
// get server port
port = ui->lineEdit->text().toShort();
// set up server ip and port
if (tcpServer.listen(QHostAddress::Any, port) == true){
qDebug()<<"create server sucessfully!";
// disiable pushButton and lineEdit
ui->pushButton->setEnabled(false);
ui->lineEdit->setEnabled(false);
}
else {
qDebug()<< "faied to create server.";
}
}
// slot func of corresponding the client connection request
void SreverDialog::onNewConnection(){
// get the socket of communicating with the client
QTcpSocket* tcpClient = tcpServer.nextPendingConnection();
// save socket to vector
tcpClientList.append(tcpClient);
// when a client sent a message to server, soket send readyRead() signal
connect(tcpClient, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
}
// slot func of receiving the client message
void SreverDialog::onReadyRead(){
//
for(int i=0; i<tcpClientList.size(); i++){
// 遍历容器找到是哪个客户端发来的消息
// bytesAvailable()获取当前套接字等待读取消息字节数
// 返回0表示没有消息需要读,大于0说明有消息要来
if(tcpClientList.at(i)->bytesAvailable()){
// read message and save it
QByteArray buf = tcpClientList.at(i)->readAll();
// display message
ui->listWidget->addItem(buf);
ui->listWidget->scrollToBottom();
// start timer
timer.start(3000);
// forward message to other client
sendMessge(buf);
}
}
}
// slot func of forwording the message to other client
void SreverDialog::sendMessge(const QByteArray& buf){
for(int i =0; i<tcpClientList.size(); i++) {
tcpClientList.at(i)->write(buf);
}
}
// slot func of timer
void SreverDialog::onTimeout(void){
qDebug()<<"ni ma";
// 遍历检查容器中保存的客户端通信套接字是否已经断开连接,如果是则删除
for(int i=0; i<tcpClientList.size(); i++){
if(tcpClientList.at(i)->state() ==
QAbstractSocket::UnconnectedState){
tcpClientList.removeAt(i);
--i;
}
}
}
4.2 客户端代码示例
clientdialog.h
#ifndef CLIENTDIALOG_H
#define CLIENTDIALOG_H
#include <QDialog>
#include <QTcpSocket>
#include <QHostAddress>
#include <QMessageBox>
#include <QDebug>
namespace Ui {
class ClientDialog;
}
class ClientDialog : public QDialog
{
Q_OBJECT
public:
explicit ClientDialog(QWidget *parent = 0);
~ClientDialog();
private slots:
// slot func of sendButton
void on_sendButton_clicked();
// slot func of connectButton
void on_connectButton_clicked();
// slot func for successful connection with server (connected())
void onConnected();
// slot func for disconnection with server
void disConnected();
// slot fucn of receive message from server
void onReadyRead();
// slot fucn of network exception
void onError();
private:
Ui::ClientDialog *ui;
bool status; // client status: online/outline
QTcpSocket tcpSocket; // socket
QHostAddress serverIp; // server ip
quint16 serverPort; // server port
QString username; // username
};
#endif // CLIENTDIALOG_H
clientdialog.cpp
#include "clientdialog.h"
#include "ui_clientdialog.h"
ClientDialog::ClientDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ClientDialog)
{
status = false;
ui->setupUi(this);
connect(&tcpSocket, SIGNAL(connected()), this, SLOT(onConnected()));
connect(&tcpSocket, SIGNAL(disconnected()), this, SLOT(disConnected()));
connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError()));
}
ClientDialog::~ClientDialog()
{
delete ui;
}
// slot func of sendButton
void ClientDialog::on_sendButton_clicked()
{
// get user input message
QString msg = ui->messageEdit->text();
if(msg == ""){
return;
}
msg = username + ": " + msg;
// send message
tcpSocket.write(msg.toUtf8());
// clean messageEdit
ui->messageEdit->clear();
}
// slot func of connectButton
void ClientDialog::on_connectButton_clicked()
{
// if outline, connect to server
if (status == false){
// get server ip
serverIp.setAddress(ui->serverIpEdit->text());
// get server port
serverPort = ui->serverPortEdit->text().toShort();
// get username
username = ui->usernameEdit->text();
// send connection request
// if success, generate connected singal, esle generate error singal
tcpSocket.connectToHost(serverIp, serverPort);
}
// if online, disconnect from server
else {
// send server with message that leaving chat room
QString msg = username + ": leaved!";
tcpSocket.write(msg.toUtf8());
// disconnect from server, and generate disconnected singal
tcpSocket.disconnectFromHost();
}
}
// slot func for successful connection with server (connected())
void ClientDialog::onConnected()
{
status = true;
ui->sendButton->setEnabled(true);
ui->serverIpEdit->setEnabled(false);
ui->serverPortEdit->setEnabled(false);
ui->usernameEdit->setEnabled(false);
ui->connectButton->setText("leave chat room");
// send message to server
QString msg = username + ": entered the chat room!";
// toUtf8(): transform QString to QByteArray
tcpSocket.write(msg.toUtf8());
}
// slot func for disconnection with server
void ClientDialog::disConnected()
{
status = false;
ui->sendButton->setEnabled(true);
ui->serverIpEdit->setEnabled(true);
ui->serverPortEdit->setEnabled(true);
ui->usernameEdit->setEnabled(true);
ui->connectButton->setText("connect server");
}
// slot fucn of receive message from server
void ClientDialog::onReadyRead()
{
if(tcpSocket.bytesAvailable()){
// receive message
QByteArray buf = tcpSocket.readAll();
// display message
ui->listWidget->addItem(buf);
// dispaly bottom message
ui->listWidget->scrollToBottom();
}
}
// slot fucn of network exception
void ClientDialog::onError()
{
// errorString(): get reason of network exception
QMessageBox::critical(this, "ERROR", tcpSocket.errorString());
}