QT TCP下的socket编程

来源:互联网 发布:mac清理重复文件 编辑:程序博客网 时间:2024/05/24 03:15

QTcpSocket 和 QTcpServer类实现了Qt的Tcp客户端和服务器。
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;
    quint16 nextBlockSize;
};

tcpSocket变量是QTcpSocket 类型,用来建立一个tcp连接。

当需要提起从服务器传递来的数据块时,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()));    connect(stopButton, SIGNAL(clicked()), this, SLOT(stopSearch()));    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_OBJECTpublic:    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_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::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_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)             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();}
原创粉丝点击