QTcpSocket 和 QTcpServer类实现了Qt的Tcp客户端和服务器。
http://linluan55.blog.163.com/blog/static/1755423992011215641781/
tcp是一个流式协议。对于应用程序来说,数据是一个很长的流,有点像一个巨大的文件。
搞成此的协议建立在面向块的tcp协议(Block-oriented)或面向行(Line-oriented )的tcp协议上。
面向块的tcp协议,数据被当作一个2进制的块来传输。没每一个块被当作一个定义了大小的,后面跟随了数据的字段。
面向行的tcp协议,数据被当作一个文本文件的一行。一个传输终止于一个新的行的到来。
QTcpSocket 继承自 QIODevice,所以它可以从 QDataStream 或 QTextStream中读取或写入数据。
从文件读数据和从网络上读数据有一个明显的不同点: 我们必须保证用“>> ”操作符读取数据时 ,已经从另一方接收了足够的数据。如果你这样做了,那么一个失败的结果是:行为未定义。
我们来看一个使用block-oriented tcp协议的服务器和客户端的代码。
用户填写行程的起始地,目的地,日期等,服务器返回符合要求的行程。
界面用QDesigner设计的。叫做“tripplanner.ui”。
请使用uic工具转换。
include "ui_tripplanner.h"
class TripPlanner : public QDialog, public Ui::TripPlanner
{
Q_OBJECT
public:
TripPlanner(QWidget *parent = 0);
private slots:
void connectToServer();
void sendRequest();
void updateTableWidget();
void stopSearch();
void connectionClosedByServer();
void error();
private:
void closeConnection();
QTcpSocket tcpSocket; //tcpSocket变量是QTcpSocket 类型,用来建立一个tcp连接。
quint16 nextBlockSize; //当需要提起从服务器传递来的数据块时,nextBlockSize将被使用。
};
TripPlanner::TripPlanner(QWidget *parent) : QDialog(parent)
{
setupUi(this);
QDateTime dateTime = QDateTime::currentDateTime(); //取当前时间
dateEdit->setDate(dateTime.date());//将当前日期设置到相应控件中
timeEdit->setTime(QTime(dateTime.time().hour(), 0));
progressBar->hide();//隐藏什么???
progressBar->setSizePolicy(QSizePolicy::Preferred,
QSizePolicy::Ignored);
tableWidget->verticalHeader()->hide();//又隐藏??
tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
connect(searchButton, SIGNAL(clicked()),this, SLOT(connectToServer()));//search单击时,连接服务器
connect(stopButton, SIGNAL(clicked()), this, SLOT(stopSearch()));//stop停止连接服务器
connect(&tcpSocket, SIGNAL(connected()), this, SLOT(sendRequest()));//如果连接好则发送返回信息??
connect(&tcpSocket, SIGNAL(disconnected()),this, SLOT(connectionClosedByServer()));//若没连接好,则关闭连接
connect(&tcpSocket, SIGNAL(readyRead()),this, SLOT(updateTableWidget()));
connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(error()));
}
构造函数中,我们设置时间控件的默认属性,隐藏progressBar等。 连接tcpSocket的connected(), disconnected(), readyRead(), error(QAbstractSocket::SocketError)信号到私有的槽。
void TripPlanner::connectToServer() { tcpSocket.connectToHost("tripserver.zugbahn.de", 6178); tableWidget->setRowCount(0); searchButton->setEnabled(false); stopButton->setEnabled(true); statusLabel->setText(tr("Connecting to server...")); progressBar->show(); nextBlockSize = 0; } 当用户点击searchButton时,connectToServer()槽将被执行。 它使用tcpSocket.connectToHost建立到
服务器的连接。connectToServer()槽立即返回。连接的动作实际发生在这之后。当连接建立成功,
QTcpSocket 触发connected() 信号。如果失败,error()信号被触发。
接着我们设置进度条以及按钮的状态。
把nextBlockSize设置为0.表示我们现在并不知道下一个接收的数据块的大小。
void TripPlanner::sendRequest() { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_1); out << quint16(0) << quint8('S') << fromComboBox->currentText() << toComboBox->currentText() << dateEdit->date() << timeEdit->time(); if (departureRadioButton->isChecked()) { out << quint8('D'); } else { out << quint8('A'); } out.device()->seek(0); out << quint16(block.size() - sizeof(quint16)); tcpSocket.write(block); statusLabel->setText(tr("Sending request...")); } 当connected()信号被触发,sendRequest() 槽被调用。sendRequest()向服务器发送一个请求(tcpSocket.write(block))。
我们需要在数据块的第一个字段写入数据块的大小。但是当我们些第一个字段时,我们不知道整个数据块的大小,
所以我们现写入0(out << quint16(0) ). 最后,当数据块填充完毕时,我们计算数据块的大小,将指针重新移动到QDataStream的开头(out.device()->seek(0)),重新写入数据块的大小out << quint16(block.size() - sizeof(quint16))。
最后,我们发送数据tcpSocket.write(block)。
void TripPlanner::updateTableWidget() { QDataStream in(&tcpSocket); in.setVersion(QDataStream::Qt_4_1); forever { int row = tableWidget->rowCount(); if (nextBlockSize == 0) { if (tcpSocket.bytesAvailable() < sizeof(quint16)) break; in >> nextBlockSize; } if (nextBlockSize == 0xFFFF) { closeConnection(); statusLabel->setText(tr("Found %1 trip(s)").arg(row)); break; } if (tcpSocket.bytesAvailable() < nextBlockSize) break; QDate date; QTime departureTime; QTime arrivalTime; quint16 duration; quint8 changes; QString trainType; in >> date >> departureTime >> duration >> changes >> trainType; arrivalTime = departureTime.addSecs(duration * 60); tableWidget->setRowCount(row + 1); QStringList fields; fields << date.toString(Qt::LocalDate) << departureTime.toString(tr("hh:mm")) << arrivalTime.toString(tr("hh:mm")) << tr("%1 hr %2 min").arg(duration / 60) .arg(duration % 60) << QString::number(changes) << trainType; for (int i = 0; i < fields.count(); ++i) tableWidget->setItem(row, i, new QTableWidgetItem(fields[i])); nextBlockSize = 0; } }
当QTcpSocket接收到数据时,readyRead()信号被触发。updateTableWidget()槽 就被调用了。
这里我们用了一个forever循环,这是必须的!因为我们无法保证一次就接到了所有的数据块。可能,我们只接收到数据块的一个部分,也可能是全部。
forever循环是如何工作的呢?如果nextBlockSize是0,表示我们没有独到数据块的大小,我们必须重新读取它。 数据块的大小字段必须至少读取sizeof(quint16))字节才能获得,如果读取的数据少于sizeof(quint16)),必须重新读取。
如果数据块大小字段为0xFFFF ,表示服务器端数据发送完毕,我们停止接收。
最后我们设置nextBlockSize 为0,表示下一个数据块的大小还不知道,我们必须接收。
void TripPlanner::closeConnection() { tcpSocket.close(); searchButton->setEnabled(true); stopButton->setEnabled(false); progressBar->hide(); } 当接收到的数据块大小字段的值为0xFFFF,我们关闭连接。
void TripPlanner::stopSearch() { statusLabel->setText(tr("Search stopped")); closeConnection(); }
如果stopServer按钮被单击,我们关闭连接。
void TripPlanner::connectionClosedByServer() { if (nextBlockSize != 0xFFFF) statusLabel->setText(tr("Error: Connection closed by server")); closeConnection(); } 当服务器断开连接时,如果我们没有读到表示数据传送完毕的 0xFFFF,我们发出一个错误。
void TripPlanner::error() { statusLabel->setText(tcpSocket.errorString()); closeConnection(); } 显示错误。
主函数:
int main(int argc, char *argv[]) { QApplication app(argc, argv); TripPlanner tripPlanner; tripPlanner.show(); return app.exec(); }
接下来,我们看看服务器端的实现。
class TripServer : public QTcpServer { Q_OBJECT public: TripServer(QObject *parent = 0); private: void incomingConnection(int socketId); }; 服务器端重新实现incomingConnection方法。当客户端尝试连接到服务器的监听端口时,incomingConnection方法被触发。
void TripServer::incomingConnection(int socketId) { ClientSocket *socket = new ClientSocket(this); socket->setSocketDescriptor(socketId); }
class ClientSocket : public QTcpSocket { Q_OBJECT public: ClientSocket(QObject *parent = 0); private slots: void readClient(); private: void generateRandomTrip(const QString &from, const QString &to, const QDate &date, const QTime &time); quint16 nextBlockSize; };
ClientSocket::ClientSocket(QObject *parent) ut << quint16(0) ). 最后,当数据块填充完毕时,我们计算数据块的大小,将指针重新 : QTcpSocket(parent)
{
connect(this, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater()));
nextBlockSize = 0;
}
void ClientSocket::readClient() { QDataStream in(this); in.setVersion(QDataStream::Qt_4_1); if (nextBlockSize == 0) { if (bytesAvailable() < sizeof(quint16)) return; in >> nextBlockSize; } if (bytesAvailable() < nextBlockSize) return; quint8 requestType; QString from; QString to; QDate date; QTime time; quint8 flag; in >> requestType; if (requestType == 'S') { in >> from >> to >> date >> time >> flag; srand(from.length() * 3600 + to.length() * 60 + time.hour()); int numTrips = rand() % 8; for (int i = 0; i < numTrips; ++i) ut << quint16(0) ). 最后,当数据块填充完毕时,我们计算数据块的大小,将指针重新 generateRandomTrip(from, to, date, time);
QDataStream out(this);
out << quint16(0xFFFF);
}
close();
}void ClientSocket::generateRandomTrip(const QString & /* from */, const QString & /* to */, const QDate &date, const QTime &time) { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_1); quint16 duration = rand() % 200; out << quint16(0) << date << time << duration << quint8(1) << QString("InterCity"); out.device()->seek(0); out << quint16(block.size() - sizeof(quint16)); write(block); }
int main(int argc, char *argv[]) { QApplication app(argc, argv); TripServer server; if (!server.listen(QHostAddress::Any, 6178)) { cerr << "Failed to bind to port" << endl; return 1; } QPushButton quitButton(QObject::tr("&Quit")); quitButton.setWindowTitle(QObject::tr("Trip Server")); QObject::connect(&quitButton, SIGNAL(clicked()), &app, SLOT(quit())); quitButton.show(); return app.exec(); }