写TCP客户/服务器应用程序

QTcpSocket和QTcpServer类可以用来实现TCP客户端和服务器。TCP是一个传输协议,它构成了包括FTP和HTTP等很多应用程序层的因特网协议基础,它也可以用于定制用户自己的协议。

TCP是一个基于流的协议。对于应用程序,数据表现为一个长长的流,而不是一个大的平面文件。在TCP之上建立的高层协议通常是基于行的或者基于块的。

● 基于行的协议把数据作为一行文本进行传输,每一数据行都以一个换行符结尾。

●基于块的协议把数据作为二进制块进行传输。每一数据块都是 由一个大小字段及其包含的数据组成的。

QTcpSocket间接地由QIODevice派生而来(通过QAbstractSocket),所以它可以使用QDataStream或者QTextStream来进行读取和写入。与从文件中读取数据相比,从网络上读取数据会有一个值得注意的差别:在使用>>操作符之前,必须确定已经从另一端接收了足够多的数据。如果没有接收到足够多的数据就使用>>操作符,通常会导致不确定的状态发生。

这一节将查看一个客户端和服务器应用程序的代码,而该客户端和服务器应用程序使用了自定义基于块的协议。如图15.1所示的客户端称为Trip Planner,它允许用户做出下一次乘坐火车的旅行计划。而服务器称为Trip Server,它向客户端提供旅行信息。我们将从编写这个Trip Planner
客户端应用程序开始。

Trip Planner提供了一个From字段、一个To字段、一个Date字段、一个Approxiniate Time字段以及两个单选钮来选择火车出发或者到达的大概时间。当用户单击Search按钮,应用程序会向服务器发送一个请求,服务器返回一个匹配这个用户要求的火车旅行列表。这个列表被显示在Trip Planner窗口的QTableWidget中。窗口的底部是显示最后一次操作状态的QLabel和一个QProgressBar。

利用Qt设计师,Trip Planner的用户界面被创建于一个名为tripplanner.ui 的文件中。这里,我们会把注意力集中在实现应用程序功能的QDialog子类的源代码上:

#ifndef TRIPPLANNER_H
#define TRIPPLANNER_H#include <QDialog>
#include <QTcpSocket>#include "ui_tripplanner.h"class QPushButton;class TripPlanner : public QDialog, private Ui::TripPlanner
{Q_OBJECTpublic:TripPlanner(QWidget *parent = 0);private slots:void connectToServer();void sendRequest();void updateTableWidget();void stopSearch();void connectionClosedByServer();void error();private:void closeConnection();QPushButton *searchButton;QPushButton *stopButton;QTcpSocket tcpSocket;quint16 nextBlockSize;
};#endif

除了QDialog之外,TripPlanner类也派生自Ui:: TrpPlanner(它是通过uic 从tripplanner.ui中生成的)。tcpSocket成员变量封装了TCP连接。在解析从服务器接收的数据块时,则会使用到nextBlockSize变量。

TripPlanner::TripPlanner(QWidget *parent): QDialog(parent)
{setupUi(this);searchButton = buttonBox->addButton(tr("&Search"),QDialogButtonBox::ActionRole);stopButton = buttonBox->addButton(tr("S&top"),QDialogButtonBox::ActionRole);stopButton->setEnabled(false);buttonBox->button(QDialogButtonBox::Close)->setText(tr("&Quit"));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()));connect(stopButton, SIGNAL(clicked()), this, SLOT(stopSearch()));connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));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()));
}

在构造函数中,我们基于当前的日期和时间来初始化日期和时间编辑器。我们也会隐藏进度条,因为只想在连接被激活时才显示它。在Qt设计师中,滚动条的minimum和maximum属性都被设置为0,这就意昧着QProgressBar会表现得像一 个忙碌的指示器而不是一个标准的基于百分比的进度条。

在构造函数中,还把QTcpSocket的connected()、disconnected()、readyRead()和error(QAbstractSocket::SocketEror)信号与私有槽连接起来。

void TripPlanner::connectToServer()
{#if 1tcpSocket.connectToHost(QHostAddress::LocalHost, 6178);
#elsetcpSocket.connectToHost("tripserver.zugbahn.de", 6178);
#endiftableWidget->setRowCount(0);searchButton->setEnabled(false);stopButton->setEnabled(true);statusLabel->setText(tr("Connecting to server..."));progressBar->show();nextBlockSize = 0;
}

当用户单击Search按钮开始搜索的时候,就会执行connectToServer()槽。我们在QTcpSocket对
象上调用connectToHost()从而连接到服务器,这里假设在Tripserver.zugbahn.de虚拟主机上的6178端口是可以访问的。(如果想在自己的机器上尝试这个实例,请把主机名称替换为QHostAddress::LocalHost)。conectToHost()调用是异步的,它总是立即返回。连接通常会在稍后建立。当连接建立起来并运行时,QTcpSocket对象发射connected()信号;如果连接失败,QTcpSocket对象会发射error(QAbstractSocket::SocketEror)信号。

接下来,更新用户界面并且将进度条设置为可见。

最后,将nextBlockSize变量设置为0。这个变量用于存储从服务器所接收的下一个块的长度值。此处选择的是0值,这意味着还不知道下一个块的大小。

void TripPlanner::sendRequest()
{QByteArray block;QDataStream out(&block, QIODevice::WriteOnly);out.setVersion(QDataStream::Qt_4_3);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..."));
}

当QTepSocket对象发射connected()信号时,会执行sendRequest()槽,表明一个连接已经被建立。这个槽的任务是向服务器生成一个请求,其中包含用户输人的所有信息。

这个请求是如下格式的二进制数据块:

首先把这些数据写到一个称为block的QByteArray中。不能直接把数据写到QTcpSocket中,因为在把所有数据都放到这个数据块里以前,我们并不知道块的大小,而块的大小必须先发送出去。

我们一开始写入0值作为块的大小,以及其他数据。然后,对输入/输出设备(在后台是由QDataStream创建的QBuffer)调用seek(0)以重新移动到字节数组的开始处,同时利用数据块的实际尺寸值覆盖最初写入的0值。这个尺寸值是通过由数据块的尺寸减去sizeof(quint16)(即2)计算得到的,也就是去掉前面容量字段所占用的空间。在这之后,对QTcpSocket调用write()向服务器发送这个块。

void TripPlanner::updateTableWidget()
{QDataStream in(&tcpSocket);in.setVersion(QDataStream::Qt_4_3);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;}
}

updateTableWidget()槽被连接到QTcpSocket的readyRead()信号,只要QTcpSocket已经从服务器收到新数据,就会发射该信号。服务器向我们发送一个和这个用户要求匹配的可能的火车旅行列表。每一个匹配的旅行都作为一个单独的块发送,并且每一个块的开始都是块的大小尺寸值。图15.2举例说明了这类数据块流。因为我们不需要每次从服务器得到一整个块的数据,所以
forever循环是非常必要的。我们也许只是收到一个完整的块、一个块的一部分、一个块和一个块的一部分,甚至还可能一次收到所有的块数据。

forever循环是如何工作的呢?如果nextBlockSize变量为0,则意味着还没有读取到下一个块的大小。我们尝试去读取它(假设至少已经有两个字节读取)。服务器使用一个大小为0xFFFF的值来表示没有更多的数据可以接收,所以如果读取到该值,就表明已经读取到数据块的末尾了。

如果块的大小不是0xFFF,则尝试去下一个块中读取。首先,我们查看是否有块的容量大小这么多字节可以读取;如果没有,就先在这里停止。当有更多数据可以读取的时候,readyRead()信号将会被再次发射,然后就可以再次尝试读取。

一旦确认一个完整的块已经被读取到,就可以在QDataStream上安全地使用>>操作符以提取与一个旅行相关的信息,并且可以使用这些信息来创建QTableWidgetItems。 从服务器上接收的块具有如下格式:

最后,重新将nextBlockSize变量设置为0,以表示下一个块的大小是未知的并且需要读取。

void TripPlanner::closeConnection()
{tcpSocket.close();searchButton->setEnabled(true);stopButton->setEnabled(false);progressBar->hide();
}

closeConnection()私有函数关闭到TCP服务器的连接,并且更新了用户界面。当读到0xFFFF时,它会被updateTableWidget()调用,并且在稍候要介绍的几个槽中,它也会被调用到。

void TripPlanner::stopSearch()
{statusLabel->setText(tr("Search stopped"));closeConnection();
}

stopSearch()槽被连接到Stop按钮的clicked()信号。从本质上来说,它只不过是调用closeConnection()而已。

void TripPlanner::connectionClosedByServer()
{if (nextBlockSize != 0xFFFF)statusLabel->setText(tr("Error: Connection closed by server"));closeConnection();
}

connectionCloseByServer()槽被连接到QTcpSocket的disconnected()信号。如果服务器关闭这个连接且我们还没有收到0xFFFF数据终止符,就告诉用户有一个错误发生了。我们会像往常一样调用closeConnection()来更新用户界面。

void TripPlanner::error()
{statusLabel->setText(tcpSocket.errorString());closeConnection();
}

error()槽被连接到QTcpSocket 的error(QAbstractSocket::SocketError)信号。我们忽略错误代码而使用QTcpSocket::errorString(),它将为最后一次发生的错误返回一个用户可读的出错信息。

所有这些都是为了TripPlanner类。Trip Planner应用程序的main()函数正如我们派生自预期的那样:

int main(int argc, char *argv[])
{QApplication app(argc, argv);TripPlanner tripPlanner;tripPlanner.show();return app.exec();
}

tripplanner.h

#ifndef TRIPPLANNER_H
#define TRIPPLANNER_H#include <QDialog>
#include <QTcpSocket>#include "ui_tripplanner.h"class QPushButton;class TripPlanner : public QDialog, private Ui::TripPlanner
{Q_OBJECTpublic:TripPlanner(QWidget *parent = 0);private slots:void connectToServer();void sendRequest();void updateTableWidget();void stopSearch();void connectionClosedByServer();void error();private:void closeConnection();QPushButton *searchButton;QPushButton *stopButton;QTcpSocket tcpSocket;quint16 nextBlockSize;
};#endif

tripplanner.cpp

#include <QtGui>
#include <QtNetwork>#include "tripplanner.h"TripPlanner::TripPlanner(QWidget *parent): QDialog(parent)
{setupUi(this);searchButton = buttonBox->addButton(tr("&Search"),QDialogButtonBox::ActionRole);stopButton = buttonBox->addButton(tr("S&top"),QDialogButtonBox::ActionRole);stopButton->setEnabled(false);buttonBox->button(QDialogButtonBox::Close)->setText(tr("&Quit"));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()));connect(stopButton, SIGNAL(clicked()), this, SLOT(stopSearch()));connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));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()));
}void TripPlanner::connectToServer()
{#if 1tcpSocket.connectToHost(QHostAddress::LocalHost, 6178);
#elsetcpSocket.connectToHost("tripserver.zugbahn.de", 6178);
#endiftableWidget->setRowCount(0);searchButton->setEnabled(false);stopButton->setEnabled(true);statusLabel->setText(tr("Connecting to server..."));progressBar->show();nextBlockSize = 0;
}void TripPlanner::sendRequest()
{QByteArray block;QDataStream out(&block, QIODevice::WriteOnly);out.setVersion(QDataStream::Qt_4_3);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..."));
}void TripPlanner::updateTableWidget()
{QDataStream in(&tcpSocket);in.setVersion(QDataStream::Qt_4_3);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;}
}void TripPlanner::stopSearch()
{statusLabel->setText(tr("Search stopped"));closeConnection();
}void TripPlanner::connectionClosedByServer()
{if (nextBlockSize != 0xFFFF)statusLabel->setText(tr("Error: Connection closed by server"));closeConnection();
}void TripPlanner::error()
{statusLabel->setText(tcpSocket.errorString());closeConnection();
}void TripPlanner::closeConnection()
{tcpSocket.close();searchButton->setEnabled(true);stopButton->setEnabled(false);progressBar->hide();
}

main.cpp

#include <QApplication>#include "tripplanner.h"int main(int argc, char *argv[])
{QApplication app(argc, argv);TripPlanner tripPlanner;tripPlanner.show();return app.exec();
}

tripplanner.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"><class>TripPlanner</class><widget class="QDialog" name="TripPlanner"><property name="geometry"><rect><x>0</x><y>0</y><width>382</width><height>369</height></rect></property><property name="windowTitle"><string>Trip Planner</string></property><layout class="QGridLayout"><item row="0" column="0"><widget class="QGroupBox" name="tripInfoGroupBox"><property name="title"><string>Trip Information</string></property><layout class="QGridLayout"><item row="3" column="0"><widget class="QLabel" name="timeLabel"><property name="text"><string>Appro&amp;ximate Time:</string></property><property name="buddy"><cstring>timeEdit</cstring></property></widget></item><item row="2" column="1"><widget class="QDateTimeEdit" name="dateEdit"><property name="displayFormat"><string>yyyy-MM-dd</string></property></widget></item><item row="3" column="1"><widget class="QDateTimeEdit" name="timeEdit"><property name="displayFormat"><string>hh:mm:ss</string></property></widget></item><item row="2" column="0"><widget class="QLabel" name="dateLabel"><property name="text"><string>&amp;Date:</string></property><property name="buddy"><cstring>dateEdit</cstring></property></widget></item><item row="1" column="0"><widget class="QLabel" name="toLabel"><property name="text"><string>&amp;To:</string></property><property name="buddy"><cstring>toComboBox</cstring></property></widget></item><item row="4" column="0" colspan="2"><layout class="QHBoxLayout"><item><widget class="QRadioButton" name="departureRadioButton"><property name="text"><string>D&amp;eparture</string></property><property name="shortcut"><string>Alt+E</string></property><property name="checked"><bool>true</bool></property></widget></item><item><widget class="QRadioButton" name="arrivalRadioButton"><property name="text"><string>&amp;Arrival</string></property><property name="shortcut"><string>Alt+A</string></property></widget></item></layout></item><item row="0" column="1"><widget class="QComboBox" name="fromComboBox"><property name="currentIndex"><number>0</number></property><item><property name="text"><string>Berlin</string></property></item><item><property name="text"><string>Bonn</string></property></item><item><property name="text"><string>Bremen</string></property></item><item><property name="text"><string>Dresden</string></property></item><item><property name="text"><string>Düsseldorf</string></property></item><item><property name="text"><string>Dortmund</string></property></item><item><property name="text"><string>Frankfurt am Main</string></property></item><item><property name="text"><string>Hannover</string></property></item><item><property name="text"><string>Hamburg</string></property></item><item><property name="text"><string>München</string></property></item><item><property name="text"><string>Nürnberg</string></property></item><item><property name="text"><string>Rostock</string></property></item><item><property name="text"><string>Stuttgart</string></property></item></widget></item><item row="1" column="1"><widget class="QComboBox" name="toComboBox"><property name="currentIndex"><number>0</number></property><item><property name="text"><string>Berlin</string></property></item><item><property name="text"><string>Bonn</string></property></item><item><property name="text"><string>Bremen</string></property></item><item><property name="text"><string>Dresden</string></property></item><item><property name="text"><string>Düsseldorf</string></property></item><item><property name="text"><string>Dortmund</string></property></item><item><property name="text"><string>Frankfurt am Main</string></property></item><item><property name="text"><string>Hannover</string></property></item><item><property name="text"><string>Hamburg</string></property></item><item><property name="text"><string>München</string></property></item><item><property name="text"><string>Nürnberg</string></property></item><item><property name="text"><string>Rostock</string></property></item><item><property name="text"><string>Stuttgart</string></property></item></widget></item><item row="0" column="0"><widget class="QLabel" name="fromLabel"><property name="text"><string>&amp;From:</string></property><property name="buddy"><cstring>fromComboBox</cstring></property></widget></item></layout></widget></item><item row="0" column="1" rowspan="3"><widget class="QDialogButtonBox" name="buttonBox"><property name="orientation"><enum>Qt::Vertical</enum></property><property name="standardButtons"><set>QDialogButtonBox::Close</set></property></widget></item><item row="1" column="0"><widget class="QTableWidget" name="tableWidget"><property name="columnCount"><number>6</number></property><column><property name="text"><string>Date</string></property></column><column><property name="text"><string>Departure</string></property></column><column><property name="text"><string>Arrival</string></property></column><column><property name="text"><string>Duration</string></property></column><column><property name="text"><string>Changes</string></property></column><column><property name="text"><string>Train type</string></property></column></widget></item><item row="2" column="0"><layout class="QHBoxLayout"><item><widget class="QLabel" name="statusLabel"><property name="sizePolicy"><sizepolicy hsizetype="Expanding" vsizetype="Preferred"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><property name="text"><string>Ready</string></property></widget></item><item><widget class="QProgressBar" name="progressBar"><property name="minimum"><number>0</number></property><property name="maximum"><number>100</number></property><property name="orientation"><enum>Qt::Horizontal</enum></property></widget></item></layout></item></layout></widget><resources/><connections/>
</ui>


现在来实现服务器。服务器包含两个类:TripServer和ClientSocket。TripServer类派生自QTcpServer,这是一个允许接收来访的TCP连接的类。ClientSocket 重新实现了QTcpSocket,处理一个单独的连接。在任何时候,在内存中ClientSocket对象的数量和正在被服务的客户端数量都是一样多的。

#ifndef TRIPSERVER_H
#define TRIPSERVER_H#include <QTcpServer>class TripServer : public QTcpServer
{Q_OBJECTpublic:TripServer(QObject *parent = 0);private:void incomingConnection(int socketId);
};#endif

tipServer类通过QTcpServer重新实现incomingConnection()函数。 只要有一个客户端试图连接到服务器正监听的端口,这个函数就会被调用。

TripServer::TripServer(QObject *parent): QTcpServer(parent)
{}

tripServer构造函数则非常普通。

void TripServer::incomingConnection(int socketId)
{ClientSocket *socket = new ClientSocket(this);socket->setSocketDescriptor(socketId);
}

在incomingConnection()中,创建了一个ClientSocket对象作为tripServer对象的子对象,并且将它的套接字描述符设置成提供给我们的数字。当连接终止时,ClientSocket对象将自动删除。

class ClientSocket : public QTcpSocket
{Q_OBJECTpublic: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类派生自QTcpSocket并且封装了一个单独客户端的状态。

ClientSocket::ClientSocket(QObject *parent): QTcpSocket(parent)
{connect(this, SIGNAL(readyRead()), this, SLOT(readClient()));connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater()));nextBlockSize = 0;
}

在构造函数中,我们建立了必要的信号槽的连接,并且将nextBlockSize变量设置为0,这表示还不知道由客户端发送的块的大小。

disconnected()信号被连接到deleteLater(),这是一个从QObject继承的函数,当控制权返回到Qt的事件循环时,它会删除对象。这样就确保当关闭套接字连接时,ClientSocket对象会被删除。

void ClientSocket::readClient()
{QDataStream in(this);in.setVersion(QDataStream::Qt_4_3);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;std::srand(from.length() * 3600 + to.length() * 60+ time.hour());int numTrips = std::rand() % 8;for (int i = 0; i < numTrips; ++i)generateRandomTrip(from, to, date, time);QDataStream out(this);out << quint16(0xFFFF);}close();
}

readClient()槽被连接到QTcpSocket 的readyRead()信号。如果nextBlockSize为0,就首先读取块的大小;否则,就表明已经开始读取它了,并且还要检查是否读取一个完整的块的时机已经到来。

一旦一个完整的块已经为读取做好准备,就一次读取它。我们直接在QTcpSocket(this对象)上使用QDataSream,并且利用>>操作符来读取各个字段。

一旦读取了客户端的请求,就生成一个应答。如果这是一个真正的应用程序,我们就会在一个火车时刻表数据库中查找信息并且试图找到相匹配的火车车次,但是这里我们使用一个称为generateRandomTnip()的函数来生成一次随机的旅行就够了。我们任意次地调用这个函数,然后发送0xFFF表示这个数据的结束。最后,关闭连接。

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_3);quint16 duration = std::rand() % 200;out << quint16(0) << date << time << duration << quint8(1)<< QString("InterCity");out.device()->seek(0);out << quint16(block.size() - sizeof(quint16));write(block);
}

generateRandomTrip()函数展示了如何在一个TCP连接之上发送一个数据块。这与客户端程序的sendRequest()函数中的做法非常相似。我们再一次把这个块写入到QByteAray,这样就可以在使用write()发送数据之前知道它的大小了。

int main(int argc, char *argv[])
{QApplication app(argc, argv);TripServer server;if (!server.listen(QHostAddress::Any, 6178)) {std::cerr << "Failed to bind to port" << std::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();
}

在main()中,我们创建了tripServer对象和允许用户停止服务器的QPushButton。我们通过调用QTcpSocket::listen()来启动服务器,它将具有我们想接收的连接的IP地址和端口号。专门的地址0.0.0.0 (QHostAddress::Any)表示在本地主机上的任意的IP接口。

在程序开发中,使用QPushButton来代表服务器是非常方便的。然而,一个配备过的服务器应该运行于没有图形用户界面的情况下,就像Windows服务或者UNIX的端口监控程序一样。为此,TrolItech公司提供了一个名为QtService的商用扩展软件来辅助实现这一功能。

现在就完成了这个客户/服务器实例。在这个实例中,我们使用了一个基于块的协议,它允许使用QDataStream来读取和写入。如果想使用基于行的协议,最简单的方式是在一个连接到readyRead()信号的槽中使用QTcpSocket的canReadLine()和readLine()函数:

QStringList lines;
while(tcpSocket.canReadLine())lines.append(tcpSocket.readLine());

然后,处理已经读取的每一行。至于发送数据;可以通过在QTcpSocket上使用QTextStream来完成。

这里使用的服务器实现,在同时有较多连接的时候,不能很好地并行工作。其原因在于:当处理某一个请求时,并没有同时处理其他连接。一个更好的方式是为每一个连接启动一个新的线程。

tripserver.h

#ifndef TRIPSERVER_H
#define TRIPSERVER_H#include <QTcpServer>class TripServer : public QTcpServer
{Q_OBJECTpublic:TripServer(QObject *parent = 0);private:void incomingConnection(int socketId);
};#endif

clientsocket.h

#ifndef CLIENTSOCKET_H
#define CLIENTSOCKET_H#include <QTcpSocket>class QDate;
class QTime;class ClientSocket : public QTcpSocket
{Q_OBJECTpublic: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;
};#endif

tripserver.cpp

#include <QtCore>#include "clientsocket.h"
#include "tripserver.h"TripServer::TripServer(QObject *parent): QTcpServer(parent)
{}void TripServer::incomingConnection(int socketId)
{ClientSocket *socket = new ClientSocket(this);socket->setSocketDescriptor(socketId);
}

clientsocket.cpp

#include <QtNetwork>#include "clientsocket.h"ClientSocket::ClientSocket(QObject *parent): 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_3);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;std::srand(from.length() * 3600 + to.length() * 60+ time.hour());int numTrips = std::rand() % 8;for (int i = 0; i < numTrips; ++i)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_3);quint16 duration = std::rand() % 200;out << quint16(0) << date << time << duration << quint8(1)<< QString("InterCity");out.device()->seek(0);out << quint16(block.size() - sizeof(quint16));write(block);
}

main.cpp

#include <QtGui>
#include <iostream>#include "tripserver.h"int main(int argc, char *argv[])
{QApplication app(argc, argv);TripServer server;if (!server.listen(QHostAddress::Any, 6178)) {std::cerr << "Failed to bind to port" << std::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();
}

Qt4_写TCP客户/服务器应用程序相关推荐

  1. 客户和服务器之间响应的序列,网络编程-第五讲-TCP客户-服务器程序例子.pdf-原创力文档...

    网络编程 第五讲TCP客户-服务器程序例子 多进程并发服务器基本架构 pid_t pid; int listenfd, connfd; listenfd = Socket( ... ); /* fil ...

  2. 第四章 基本TCP套接字编程 第五章 TCP客户/服务器程序实例

    TCP客户与服务器进程之间发生的重大事件时间表 TCP服务器 socket() --- bind() --- listen() --- accept() --- read() --- write -- ...

  3. network programming-简单的TCP客户服务器编程

    简单的TCP(Transport Control Pr)程序客户端流程:创建套接字(套接字用IP地址:端口号)表示)socket()->请求连接connect()->交换数据 send() ...

  4. 【UNIX网络编程】| 【03】TCP客户/服务器程序示例

    文章目录 1.概述 2.TCP回射服务器程序 3.TCP回射客户程序 3.正常启动 4.正常终止 5.POSIX信号处理 5.1 signal函数 5.2 POSIX信号语义 6.处理SIGCHID信 ...

  5. unix网络编程各种TCP客户-服务器程序设计实例(三)

    第五种  TCP预先派生子进程服务器程序: 对预先派生子进程服务器的最后一种改动就是由父进程调用accept,然后再将所接受的已连接描述字传递给子进程.父进程必须跟踪子进程的忙闲状态,以便给空闲子进程 ...

  6. C++基于TCP/IP简单的客户端、服务器通信程序实例

    本篇文章实现了一个基于TCP 的一个非常简单的客户/服务器通信程序实例.该程序中通讯协议使用的是面向连接的TCP协议SOCK_STREAM, 服务器的ip地址为本地地址即: 127.0.0.1,端口号 ...

  7. linux守护进程中多线程实现,Linux下实现多线程客户/服务器

    在传统的Unix模型中,当一个进程需要由另一个实体执行某件事时,该进程派生(fork)一个子进程,让子进程去进行处理. Unix下的大多数网络服务器程序都是这么编写的,即父进程接受连接,派生子进程,子 ...

  8. 第十篇:基于TCP的一对回射客户/服务器程序及其运行过程分析( 上 )

    前言 本文将讲解一对经典的客户/服务器回射程序,感受网络编程的大致框架( 该程序稍作改装即可演变成各种提供其他服务的程序 ):同时,还将对其运行过程加以分析,观察程序背后协议的执行细节,学习调试网络程 ...

  9. UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式

    本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 下面我会介绍同一个使用 TCP 协议的客户端程序的几个不同版本,分别是停等版本.select ...

最新文章

  1. GPU端吊打RegNet、EfficientNet的强悍担当:GENet
  2. python3 import导入模块
  3. 修复远程过程调用 (RPC) 时发生的各种问题KB908521
  4. Spring Bean的配置及常用属性
  5. Unity5.3官方VR教程重磅登场-系列7 优化VR体验
  6. 一步步实现SDDC-vSphere Auto Deploy的妙用
  7. siwper vue 上下滑动分页_支持移动端的vue滑动轮播图插件vueswiper
  8. 英文教材《FPGA-Prototyping-By-Verilog-Examples》下载
  9. vue中的阿里巴巴矢量图标使用
  10. 内存测试软件rst,RST内存检测软件使用方法.doc
  11. offer来了java面试百度云版,精心整理
  12. R语言 substitute
  13. 【论文阅读】【3d目标检测】Sparse Fuse Dense: Towards High Quality 3D Detection with Depth Completion
  14. win11磁盘分区怎么分?手把手教会你
  15. python pie图
  16. 2020年2月26日训练日记
  17. 【如何编码实现一个随机点名器】
  18. C/C++中int的取值范围
  19. 大一计算机专业学期计划范文,大一学期的个人学习计划范文(精选5篇)
  20. Keil uVision5 MDK(ARM)软件的介绍、下载、安装与注册

热门文章

  1. server多笔记录拼接字符串 sql_前台传入多个参数(数组格式),拼接成字符串中间用逗号隔开,传入到sql中用in查询....
  2. mui实现分享功能_继MIUI之后,华为EMUI更新,深度实现万物互联
  3. 2021年重庆市高考成绩查询时间复核,2021年重庆高考怎么查询是否被录取,具体录取时间安排...
  4. itunes备份包括哪些内容_建筑工程的招标包括哪些内容?
  5. 设计模式教程(Design Patterns Tutorial)笔记之一 创建型模式(Creational Patterns)...
  6. [转]Centos 安装Sublime text 3
  7. Android开发之蓝牙(Bluetooth)操作(一)--扫描已经配对的蓝牙设备
  8. FT1248开发笔记
  9. Shader编程学习笔记(二)—— Shader和渲染管线
  10. 二维数组中最大连通子数组