本文基于VS2015和Qt5.9实现。在实际的TCP通讯中,并不能像一些网上关于QTcpSocket和QTcpServer使用例程一样,建立连接后有数据就直接调用ReadAll方法读取内容。要考虑很多情况,比如常见的TCP拆包、粘包和丢包。所以要有一些机制来确保发送的包可以被完好的接收到,并且在丢包后可以重发。下表是我们定义一个TCP数据包的格式,我们要发的数据会被打包到这个数据格式里,以确保对方收到后可以完整地把数据解析出来。
2字节头 | 4字节ID | 1字节功能 | 4字节数据长度 | 任意字节数据 |
0xEF 0xFF |
从1开始递增。对方收到后会回复一个ID为负发送方ID 的空数据包以帮助发送方确认收到。比如:你发送一个 数据包的ID为2,对方会回复一个ID为-2的空包 |
0代表发送的是本机名称,该功能是内置功能, 只在建立连接后发送一次,用户无需关注这个 包。1代表的是发送普通数据 |
代表“你的数据”的长度 | 你的数据 |
以下是代码。头文件:
#pragma once #include "qabstractsocket.h" #include "qdatetime.h" #include "qmap.h" class QTcpSocket; class QTcpServer; class QTimer; class ClientWrapper : public QObject { Q_OBJECT public: ClientWrapper(const QString& name = u8"未命名", QObject* parent = 0); void connectToHost(const QString& ip, quint16 port); void sendData(const QByteArray& data); void disconnect(); signals: void connectStatusChanged(bool isConnected); void dataReceived(const QByteArray& data); private slots: void socketError(); void socketConnected(); void socketReadyRead(); void timerTimeout(); private: struct Message { int id; int sendCount; QDateTime lastSendTime; QByteArray data; }; struct RecData { char type; int id; int dataLength; QByteArray data; }; QByteArray constructPackage(const QByteArray& input, int id, char type = 1); bool handleReceiveData(RecData& rd); void handleResponse(const RecData& rd); void checkIfConnectFailed(const QVector<int>& timeouts); void sendMyName(); int nextId(); private: const static int retryCount; const static int msExpired; static int uniqueId; QVector<Message> pendings; QVector<Message> recIds; QString socketName; RecData rdata; QTcpSocket* client; QTimer* timer; }; class ServerWrapper : public QObject { Q_OBJECT public: ServerWrapper(QObject* parent = 0); void sendData(const QByteArray& data); void disconnect(const QString& name = QString()); signals: void dataReceived(const QString& name, const QByteArray& data); void newClientConnected(const QString& name); void clientDisconnected(const QString& name); private slots: void tcpServerNewConnection(); void clientReadyRead(); void clientError(); void timerTimeout(); private: struct Message { int id; int sendCount; QDateTime lastSendTime; QByteArray data; }; struct RecData { char type; int id; int dataLength; QByteArray data; }; QByteArray constructPackage(const QByteArray& input, int id, char type = 1); bool handleReceiveData(QTcpSocket* socket, RecData& rd); void handleNewPackage(QTcpSocket* socket, const QByteArray& package, char type); void handleResponse(int id); int nextId(); private: const static int retryCount; const static int msExpired; static int uniqueId; QTcpServer* server; QTimer* timer; QMultiMap<QString, QTcpSocket*> nameOfClients; QMap<QTcpSocket*, RecData> clients; QVector<Message> pendings; QVector<Message> recIds; };
CPP文件:
#include "TcpComm.h" #include "qtcpserver.h" #include "qtcpsocket.h" #include "qtimer.h" const int ClientWrapper::retryCount = 3; const int ClientWrapper::msExpired = 5000; int ClientWrapper::uniqueId = 1; /* [1, +无穷大) */ ClientWrapper::ClientWrapper(const QString& name, QObject* parent) : QObject(parent), socketName(name) { rdata.id = 0; rdata.dataLength = -1; client = new QTcpSocket(this); using SocketError = QAbstractSocket::SocketError; connect(client, QOverload<SocketError>::of(&QTcpSocket::error), this, &ClientWrapper::socketError); connect(client, &QTcpSocket::connected, this, &ClientWrapper::socketConnected); connect(client, &QTcpSocket::readyRead, this, &ClientWrapper::socketReadyRead); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &ClientWrapper::timerTimeout); timer->start(50); } void ClientWrapper::timerTimeout() { QVector<int> dels; QDateTime curr = QDateTime::currentDateTime(); int rcount = recIds.size(); for (int i = 0; i < rcount; i++) { /* 防止收到重复id的包 */ if (recIds[i].lastSendTime.msecsTo(curr) >= 3 * msExpired) { dels.push_back(i); } } for (auto i = dels.rbegin(); i != dels.rend(); i++) { recIds.removeAt(*i); } dels.clear(); int pcount = pendings.size(); for (int i = 0; i < pcount; i++) { if (pendings[i].lastSendTime.msecsTo(curr) >= msExpired) { if (pendings[i].sendCount >= retryCount) { dels.push_back(i); continue; } if (pendings[i].id == 0) { QList<QByteArray> strs = pendings[i].data.split(':'); client->connectToHost(strs[0], strs[1].toUShort()); } else { client->write(pendings[i].data); } pendings[i].lastSendTime = curr; pendings[i].sendCount++; } } checkIfConnectFailed(dels); for (auto i = dels.rbegin(); i != dels.rend(); i++) { pendings.removeAt(*i); } } void ClientWrapper::checkIfConnectFailed(const QVector<int>& timeouts) { int isFailed = 0; for (auto item : timeouts) { /* 连接服务器失败 */ if (pendings[item].id == 0) { isFailed = 1; break; } /* 发送自己名称失败 */ if (pendings[item].data[6] == '\0') { isFailed = 2; break; } } if (isFailed == 2) { client->disconnectFromHost(); } if (isFailed != 0) { emit connectStatusChanged(false); } } void ClientWrapper::socketError() { auto found = std::find_if(pendings.begin(), pendings.end(), [](const Message& msg) { return msg.id == 0; }); /* 是否处于连接服务器状态,不处于则触发信号 */ if (found == pendings.end()) { QTimer::singleShot(1, [this]() { emit connectStatusChanged(false); }); rdata.id = 0; rdata.dataLength = -1; } } void ClientWrapper::socketConnected() { auto found = std::find_if(pendings.begin(), pendings.end(), [](const Message& msg) { return msg.id == 0; }); if (found != pendings.end()) { pendings.erase(found); } /* 连接服务器后立即发送自己名字,发送成功转换状态为已连接 */ /* 转换状态信号在@handleResponse函数 */ sendMyName(); } void ClientWrapper::socketReadyRead() { rdata.data += client->readAll(); while (handleReceiveData(rdata)) {} } void ClientWrapper::disconnect() { client->disconnectFromHost(); } void ClientWrapper::connectToHost(const QString& ip, quint16 port) { QString data = QString(u8"%1:%2").arg(ip).arg(port); Message msg; msg.id = 0; msg.sendCount = 1; msg.lastSendTime = QDateTime::currentDateTime(); msg.data = data.toUtf8(); pendings.push_back(msg); client->connectToHost(ip, port); } void ClientWrapper::sendData(const QByteArray& data) { int nid = nextId(); QByteArray package = constructPackage(data, nid); Message msg; msg.id = nid; msg.sendCount = 1; msg.lastSendTime = QDateTime::currentDateTime(); msg.data = package; pendings.push_back(msg); client->write(package); } void ClientWrapper::sendMyName() { int nid = nextId(); QByteArray package = constructPackage(socketName.toUtf8(), nid, 0); Message msg; msg.id = nid; msg.sendCount = 1; msg.lastSendTime = QDateTime::currentDateTime(); msg.data = package; pendings.push_back(msg); client->write(package); } void ClientWrapper::handleResponse(const RecData& rd) { auto found = std::find_if(pendings.begin(), pendings.end(), [rd](const Message& msg) { return msg.id == -rd.id; }); if (found != pendings.end()) { pendings.erase(found); } if (rd.type == 0) /*名字发送成功,转换状态*/ { emit connectStatusChanged(true); } } bool ClientWrapper::handleReceiveData(RecData& rd) { while (rd.data.length() >= 2) { if (rd.data[0] != '\xef' || rd.data[1] != '\xff') { rd.data.remove(0, 1); continue; } break; } if (rd.dataLength < 0 && rd.data.length() >= 11) { rd.id = 0; rd.id |= (rd.data[2] & 0xff) << 24; rd.id |= (rd.data[3] & 0xff) << 16; rd.id |= (rd.data[4] & 0xff) << 8; rd.id |= (rd.data[5] & 0xff); rd.type = rd.data[6]; rd.dataLength = 0; rd.dataLength |= (rd.data[7] & 0xff) << 24; rd.dataLength |= (rd.data[8] & 0xff) << 16; rd.dataLength |= (rd.data[9] & 0xff) << 8; rd.dataLength |= (rd.data[10] & 0xff); } int whole = rd.dataLength + 11; if (rd.dataLength >= 0 && rd.data.length() >= whole) { if (rd.id > 0) { client->write(constructPackage(QByteArray(), -rd.id, rd.type)); /* 防止重复收到rd.id的数据 */ auto found = std::find_if(recIds.begin(), recIds.end(), [&rd](const Message& m) { return m.id == rd.id; }); if (found == recIds.end()) { Message recRecord; recRecord.id = rd.id; recRecord.lastSendTime = QDateTime::currentDateTime(); recIds.push_back(recRecord); QByteArray onePage = rd.data.mid(11, rd.dataLength); emit dataReceived(onePage); } } else { handleResponse(rd); } rd.data.remove(0, whole); rd.dataLength = -1; if (!rd.data.isEmpty()) { return true; } } return false; } QByteArray ClientWrapper::constructPackage(const QByteArray& input, int id, char type) { int whole = input.length(); QByteArray output; output += "\xef\xff"; output += char((id >> 24) & 0xff); output += char((id >> 16) & 0xff); output += char((id >> 8) & 0xff); output += char(id & 0xff); output += char(type); output += char((whole >> 24) & 0xff); output += char((whole >> 16) & 0xff); output += char((whole >> 8) & 0xff); output += char(whole & 0xff); output += input; return output; } int ClientWrapper::nextId() { int x = uniqueId; uniqueId = qMax(uniqueId + 1, 1); return x; } ///////////////////////////////////////////////////////////////////////////////////////// const int ServerWrapper::retryCount = 3; const int ServerWrapper::msExpired = 5000; int ServerWrapper::uniqueId = 1; // [1,+∞) ServerWrapper::ServerWrapper(QObject* parent) : QObject(parent) { server = new QTcpServer(this); connect(server, &QTcpServer::newConnection, this, &ServerWrapper::tcpServerNewConnection); server->listen(QHostAddress::Any, 8901); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &ServerWrapper::timerTimeout); timer->start(50); } void ServerWrapper::timerTimeout() { QVector<int> dels; QDateTime curr = QDateTime::currentDateTime(); int rcount = recIds.size(); for (int i = 0; i < rcount; i++) { if (recIds[i].lastSendTime.msecsTo(curr) >= 3 * msExpired) /* 这里是收到数据的时间 */ { dels.push_back(i); } } /* 倒序删除 */ for (auto i = dels.rbegin(); i != dels.rend(); i++) { recIds.removeAt(*i); } dels.clear(); int pcount = pendings.size(); for (int i = 0; i < pcount; i++) { if (pendings[i].lastSendTime.msecsTo(curr) >= msExpired) { if (pendings[i].sendCount >= retryCount) { dels.push_back(i); continue; } for (auto them : clients.keys()) { them->write(pendings[i].data); } pendings[i].lastSendTime = curr; pendings[i].sendCount++; } } /*倒序删除*/ for (auto i = dels.rbegin(); i != dels.rend(); i++) { pendings.removeAt(*i); } } void ServerWrapper::tcpServerNewConnection() { QTcpSocket* peer = server->nextPendingConnection(); using SocketError = QAbstractSocket::SocketError; connect(peer, &QTcpSocket::readyRead, this, &ServerWrapper::clientReadyRead); connect(peer, QOverload<SocketError>::of(&QTcpSocket::error), this, &ServerWrapper::clientError); RecData dataStruct; dataStruct.dataLength = -1; clients.insert(peer, dataStruct); } void ServerWrapper::disconnect(const QString& name) { QList<QTcpSocket*> toDels = name.isEmpty() ? nameOfClients.values() : nameOfClients.values(name); for (auto item : toDels) { item->disconnectFromHost(); clients.remove(item); delete item; } nameOfClients.remove(name); } void ServerWrapper::sendData(const QByteArray& data) { int nid = nextId(); QByteArray package = constructPackage(data, nid); Message msg; msg.id = nid; msg.sendCount = 1; msg.lastSendTime = QDateTime::currentDateTime(); msg.data = package; pendings.push_back(msg); for (auto them : clients.keys()) { them->write(package); } } void ServerWrapper::clientReadyRead() { QTcpSocket* src = dynamic_cast<QTcpSocket*>(sender()); if (!clients.contains(src)) { qDebug() << u8"错误的代码"; return; } RecData& rd = clients[src]; rd.data += src->readAll(); while (handleReceiveData(src, rd)) {} } void ServerWrapper::clientError() { QTcpSocket* src = dynamic_cast<QTcpSocket*>(sender()); QString name = nameOfClients.key(src); nameOfClients.remove(name, src); clients.remove(src); emit clientDisconnected(name); } void ServerWrapper::handleResponse(int id) { auto found = std::find_if(pendings.begin(), pendings.end(), [id](const Message& msg) { return msg.id == -id; }); if (found != pendings.end()) { pendings.erase(found); } } void ServerWrapper::handleNewPackage(QTcpSocket* socket, const QByteArray& package, char type) { if (type == 0) /* 0为发送客户端名称的包 */ { nameOfClients.insert(package, socket); emit newClientConnected(package); } else { QString name = nameOfClients.key(socket); emit dataReceived(name, package); } } bool ServerWrapper::handleReceiveData(QTcpSocket* socket, RecData& rd) { while (rd.data.length() >= 2) { if (rd.data[0] != '\xef' || rd.data[1] != '\xff') { rd.data.remove(0, 1); continue; } break; } if (rd.dataLength < 0 && rd.data.length() >= 11) { rd.id = 0; rd.id |= (rd.data[2] & 0xff) << 24; rd.id |= (rd.data[3] & 0xff) << 16; rd.id |= (rd.data[4] & 0xff) << 8; rd.id |= (rd.data[5] & 0xff); rd.type = rd.data[6]; rd.dataLength = 0; rd.dataLength |= (rd.data[7] & 0xff) << 24; rd.dataLength |= (rd.data[8] & 0xff) << 16; rd.dataLength |= (rd.data[9] & 0xff) << 8; rd.dataLength |= (rd.data[10] & 0xff); } int whole = rd.dataLength + 11; if (rd.dataLength >= 0 && rd.data.length() >= whole) { if (rd.id > 0) { socket->write(constructPackage(QByteArray(), -rd.id, rd.type)); /* 防止重复收到rd.id的数据 */ auto found = std::find_if(recIds.begin(), recIds.end(), [&rd](const Message& m) { return m.id == rd.id; }); if (found == recIds.end()) { Message recRecord; recRecord.id = rd.id; recRecord.lastSendTime = QDateTime::currentDateTime(); recIds.push_back(recRecord); QByteArray onePage = rd.data.mid(11, rd.dataLength); handleNewPackage(socket, onePage, rd.type); } } else { handleResponse(rd.id); } rd.data.remove(0, whole); rd.dataLength = -1; if (!rd.data.isEmpty()) { return true; } } return false; } QByteArray ServerWrapper::constructPackage(const QByteArray& input, int id, char type) { int whole = input.length(); QByteArray output; output += "\xef\xff"; output += char((id >> 24) & 0xff); output += char((id >> 16) & 0xff); output += char((id >> 8) & 0xff); output += char(id & 0xff); output += char(type); output += char((whole >> 24) & 0xff); output += char((whole >> 16) & 0xff); output += char((whole >> 8) & 0xff); output += char(whole & 0xff); output += input; return output; } int ServerWrapper::nextId() { int x = uniqueId; uniqueId = qMax(uniqueId + 1, 1); return x; }
客户端在建立TCP连接之后发送自己的名称的好处是可以通过名称控制客户端。这比通过IP地址识别客户端要好得多,因为IP可以改变。ClientWrapper类是客户端;ServerWrapper类是服务器。他们公开的成员和信号并不多很容易使用。在客户端和服务器内部都实现了对数据的超时自动重发功能,直到收到对方回复的确认包,但最多重发3次。为了防止自动重发导致的同一个包被多次收到,他们还会记录一定时间内收到的所有数据包的ID,如果短时间内收到多次,则只触发一次dataReceived信号。
这两个类的使用方式简单。只需要创建对象,然后连接对应的信号槽。发送数据只需在点击按钮的槽函数中调用sendData函数就行。我测试是在同一个进程测试的,但完全可以把客户端和服务器放到不同的进程中。下面是我写的测试Demo的截图,客户端的名字是“客户端的名字”: