使用Qt的进行TCP通讯,主要用到两个类,QTcpServer和QTcpSocket。前者主要用于服务端的监听,相当于原始socket中由socket函数创建的监听socket;后者主要用于读写数据,相当于原始socket中由accept函数返回的连接socket。
需要注意的是,一般需要在disconnected的槽函数中通过deleteLater函数删除该连接的QTcpSocket对象,如果不进行手动删除,Qt会在QTcpServer对象销毁时自动消除所有连接的QTcpSocket对象,在此之前,那些断开的QTcpSocket对象会一直占用内存,造成内存没必要的浪费。
相对于服务端,客户端的disconnected倒是没那么重要,是否需要处理看情况而定。
首先需要了解的是readyRead信号的行为,该信号只有在有新数据到来时发射一次,与缓冲区中是否还有未读数据无关。比如发送端一次发来了十个字节的数据,但接收端一次只能读取五个字节,需要在一个信号响应过程中读两次,因为该信号只会发射一次。通常情况下要在槽函数中写一个循环来读取数据,如果缓冲区中的所有数据都读完了,再次调用read函数会返回0字节。
发送端处理起来比较简单,write函数会将指定的数据无脑写入缓冲区,由Qt自行处理封包拆包问题,比较麻烦的是接收端。
我们每次发送的数据长度是不确定的,而Qt又不管我们调用过几次write,也不管每次发送多少字节,都是无脑写入缓冲区,这样就可能回造成一个问题,如果每次发送的数据很小,接收端会一次接收到多次write的数据,如果发送的数据很大,接收端一次接收的数据又很可能不完整,需要接收几次才行,甚至还很可能在接收端的一次接收时会接收到两次不完整的数据,第一次的后半段和第二次的前半段。因此在接收端需要进行拆包封包的操作,自行拼凑我们想要的数据。
我们首先定义一个接收端和发送端共用的数据协议
enum ProtocolType
{
TEXT = 0,
PICTURE,
END,
MAX
};
struct MYBSProtocol
{
int length;
ProtocolType type;
char data[0];
};
然后是服务端
class Server : public QObject
{
Q_OBJECT
public:
explicit Server(QObject *parent = nullptr);
~Server();
void startListen(int nPort);
private:
QTcpServer *m_server;
public slots :
void onNewConnection();
void onReadyRead();
void onDisconnected();
};
Server::Server(QObject *parent)
: QObject(parent)
{
m_server = new QTcpServer;
}
Server::~Server()
{
delete m_server;
}
void Server::startListen(int nPort)
{
if (m_server->listen(QHostAddress::Any, nPort))
qDebug() << "listen port "<< nPort << " ok";
else
qDebug() << "listen err";
connect(m_server, SIGNAL(newConnection()), this, SLOT(onNewConnection()));
}
void Server::onNewConnection()
{
QTcpSocket *socket = m_server->nextPendingConnection();
QString ip = socket->peerAddress().toString();
quint16 port = socket->peerPort();
qDebug() << "new client connect:" << ip << ":" << port;
connect(socket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(socket, SIGNAL(disconnected()), this, SLOT(onDisconnected()));
}
void Server::onReadyRead()
{
qDebug() << "read client message";
QTcpSocket *socket = dynamic_cast<QTcpSocket *>(sender());
if (socket)
{
QByteArray buff;
buff = socket->readAll();
qDebug() << buff;
QFile file(buff);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return;
while (!file.atEnd())
{
MYBSProtocol *mp = nullptr;
// 处理中文乱码
QString line = QString::fromLocal8Bit(file.readLine());
QString key = line.left(line.indexOf(':'));
QString value = line.mid(line.indexOf(':') + 1);
if (key == "text")
{
QByteArray ba = value.toLocal8Bit();
mp = (MYBSProtocol *)malloc(sizeof(mp) + ba.size());
if (!mp)
continue;
memset(mp, 0, sizeof(mp) + value.size());
mp->type = TEXT;
mp->length = ba.size();
memcpy(mp->data, ba.data(), mp->length);
}
else if (key == "picture")
{
QFile picture(value);
if (!picture.open(QIODevice::ReadOnly))
continue;
QByteArray ba = picture.readAll();
picture.close();
mp = (MYBSProtocol *)malloc(sizeof(mp) + ba.size());
if (!mp)
continue;
memset(mp, 0, sizeof(mp) + ba.size());
mp->type = PICTURE;
mp->length = ba.count();
memcpy(mp->data, ba.data(), mp->length);
}
else
{
continue;
}
socket->write((char *)mp, sizeof(mp) + mp->length);
free(mp);
mp = nullptr;
}
file.close();
MYBSProtocol mp;
mp.type = END;
mp.length = 0;
socket->write(QByteArray(reinterpret_cast<const char *>(&mp), sizeof(mp)));
}
}
void Server::onDisconnected()
{
QTcpSocket *socket = dynamic_cast<QTcpSocket *>(sender());
if (socket)
{
QString ip = socket->peerAddress().toString();
quint16 port = socket->peerPort();
qDebug() << "client disconnect:" << ip << ":" << port;
socket->deleteLater();
}
}
最后是客户端,客户端的重点是拆包组包的操作
void MainWindow::onPushButtonClicked(bool checked)
{
QString url = ui->lineEdit->text();
QString ip = url.left(url.indexOf(':'));
QString port = url.mid(url.indexOf(':') + 1, url.indexOf('/') - url.indexOf(':') - 1);
QString file = url.mid(url.indexOf('/') + 1);
QTcpSocket *socket = new QTcpSocket;
connect(socket, &QTcpSocket::readyRead, this, &MainWindow::onReadyRead);
connect(socket, &QTcpSocket::disconnected, this, &MainWindow::onDisconnected);
socket->connectToHost(ip, port.toUInt());
if (socket->waitForConnected(-1))
{
socket->write(file.toUtf8());
socket->waitForBytesWritten(-1);
}
else
{
QAbstractSocket::SocketError error = socket->error();
QString str = socket->errorString();
delete socket;
socket = nullptr;
}
}
void MainWindow::onReadyRead()
{
QTcpSocket *socket = dynamic_cast<QTcpSocket *>(sender());
if (socket)
{
while (true)
{
QByteArray buff;
int buffLen = 0;
MYBSProtocol *mp = nullptr;
// 如果临时缓冲区为空指针,说明是每条指令的第一次读
if (!m_tempBuff)
{
buff = socket->read(sizeof(MYBSProtocol));
buffLen = buff.size();
if(buffLen == 0)
break;
mp = (MYBSProtocol *)buff.data();
// 防止上次内存申请失败读到抛弃的数据
if (mp->type >= MAX)
return;
// 如果内存分配失败,直接关闭socket,这一次的数据全不要了
m_tempBuff = (char *)malloc(sizeof(mp) + mp->length);
if (!m_tempBuff)
{
socket->close();
socket->waitForDisconnected(-1);
m_surplus = 0;
return;
}
memset(m_tempBuff, 0, sizeof(mp) + mp->length);
memcpy(m_tempBuff, buff.data(), buffLen);
m_cursor += buffLen;
m_surplus = mp->length;
}
else
{
buff = socket->read(m_surplus > m_maxRead ? m_maxRead : m_surplus);
buffLen = buff.size();
// buffLen和m_surplus等于0是数据段长度为0的情况
// 防止数据段长度等于零时造成内存泄露
if (buffLen == 0 && m_surplus != 0)
break;
memcpy(m_tempBuff + m_cursor, buff.data(), buffLen);
m_cursor += buffLen;
m_surplus -= buffLen;
if (m_surplus == 0)
{
mp = (MYBSProtocol *)m_tempBuff;
switch (mp->type)
{
case TEXT:
{
QByteArray ba(mp->data, mp->length);
QString text = QString::fromLocal8Bit(ba);
ui->browseArea->setText(text);
}break;
case PICTURE:
{
QPixmap imageresult;
QByteArray ba(mp->data, mp->length);
imageresult.loadFromData(ba);
ui->browseArea->setPixmap(imageresult);
}break;
case END:
{
socket->close();
socket->waitForDisconnected(-1);
}break;
default:
break;
}
// 函数的最后判断一下是否完整处理过一个组包过程
// 如果处理过,释放内存
if (m_tempBuff)
{
free(m_tempBuff);
m_tempBuff = nullptr;
m_cursor = 0;
}
}
}
}
}
}
void MainWindow::onDisconnected()
{
QTcpSocket *socket = dynamic_cast<QTcpSocket *>(sender());
if (socket)
{
QString ip = socket->peerAddress().toString();
quint16 port = socket->peerPort();
qDebug() << "client disconnect:" << ip << ":" << port;
socket->deleteLater();
}
}