diff options
Diffstat (limited to 'examples/network/torrent')
37 files changed, 6225 insertions, 0 deletions
diff --git a/examples/network/torrent/addtorrentdialog.cpp b/examples/network/torrent/addtorrentdialog.cpp new file mode 100644 index 0000000..fb43f59 --- /dev/null +++ b/examples/network/torrent/addtorrentdialog.cpp @@ -0,0 +1,170 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "addtorrentdialog.h" +#include "metainfo.h" + +#include <QFile> +#include <QFileDialog> +#include <QLineEdit> +#include <QMetaObject> + +static QString stringNumber(qint64 number) +{ + QString tmp; + if (number > (1024 * 1024 * 1024)) + tmp.sprintf("%.2fGB", number / (1024.0 * 1024.0 * 1024.0)); + else if (number > (1024 * 1024)) + tmp.sprintf("%.2fMB", number / (1024.0 * 1024.0)); + else if (number > (1024)) + tmp.sprintf("%.2fKB", number / (1024.0)); + else + tmp.sprintf("%d bytes", int(number)); + return tmp; +} + +AddTorrentDialog::AddTorrentDialog(QWidget *parent) + : QDialog(parent, Qt::Sheet) +{ + ui.setupUi(this); + + connect(ui.browseTorrents, SIGNAL(clicked()), + this, SLOT(selectTorrent())); + connect(ui.browseDestination, SIGNAL(clicked()), + this, SLOT(selectDestination())); + connect(ui.torrentFile, SIGNAL(textChanged(const QString &)), + this, SLOT(setTorrent(const QString &))); + + ui.destinationFolder->setText(destinationDirectory = QDir::current().path()); + ui.torrentFile->setFocus(); +} + +void AddTorrentDialog::selectTorrent() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Choose a torrent file"), + lastDirectory, + tr("Torrents (*.torrent);; All files (*.*)")); + if (fileName.isEmpty()) + return; + lastDirectory = QFileInfo(fileName).absolutePath(); + setTorrent(fileName); +} + +void AddTorrentDialog::selectDestination() +{ + QString dir = QFileDialog::getExistingDirectory(this, tr("Choose a destination directory"), + lastDestinationDirectory); + if (dir.isEmpty()) + return; + lastDestinationDirectory = destinationDirectory = dir; + ui.destinationFolder->setText(destinationDirectory); + enableOkButton(); +} + +void AddTorrentDialog::enableOkButton() +{ + ui.okButton->setEnabled(!ui.destinationFolder->text().isEmpty() + && !ui.torrentFile->text().isEmpty()); +} + +void AddTorrentDialog::setTorrent(const QString &torrentFile) +{ + if (torrentFile.isEmpty()) { + enableOkButton(); + return; + } + + ui.torrentFile->setText(torrentFile); + if (!torrentFile.isEmpty()) + lastDirectory = QFileInfo(torrentFile).absolutePath(); + + if (lastDestinationDirectory.isEmpty()) + lastDestinationDirectory = lastDirectory; + + MetaInfo metaInfo; + QFile torrent(torrentFile); + if (!torrent.open(QFile::ReadOnly) || !metaInfo.parse(torrent.readAll())) { + enableOkButton(); + return; + } + + ui.torrentFile->setText(torrentFile); + ui.announceUrl->setText(metaInfo.announceUrl()); + if (metaInfo.comment().isEmpty()) + ui.commentLabel->setText("<unknown>"); + else + ui.commentLabel->setText(metaInfo.comment()); + if (metaInfo.createdBy().isEmpty()) + ui.creatorLabel->setText("<unknown>"); + else + ui.creatorLabel->setText(metaInfo.createdBy()); + ui.sizeLabel->setText(stringNumber(metaInfo.totalSize())); + if (metaInfo.fileForm() == MetaInfo::SingleFileForm) { + ui.torrentContents->setHtml(metaInfo.singleFile().name); + } else { + QString html; + foreach (MetaInfoMultiFile file, metaInfo.multiFiles()) { + QString name = metaInfo.name(); + if (!name.isEmpty()) { + html += name; + if (!name.endsWith('/')) + html += '/'; + } + html += file.path + "<br>"; + } + ui.torrentContents->setHtml(html); + } + + QFileInfo info(torrentFile); + ui.destinationFolder->setText(info.absolutePath()); + + enableOkButton(); +} + +QString AddTorrentDialog::torrentFileName() const +{ + return ui.torrentFile->text(); +} + +QString AddTorrentDialog::destinationFolder() const +{ + return ui.destinationFolder->text(); +} diff --git a/examples/network/torrent/addtorrentdialog.h b/examples/network/torrent/addtorrentdialog.h new file mode 100644 index 0000000..89cb77c --- /dev/null +++ b/examples/network/torrent/addtorrentdialog.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef ADDTORRENTDIALOG_H +#define ADDTORRENTDIALOG_H + +#include <QDialog> + +#include "ui_addtorrentform.h" + +class AddTorrentDialog : public QDialog +{ + Q_OBJECT + +public: + AddTorrentDialog(QWidget *parent = 0); + + QString torrentFileName() const; + QString destinationFolder() const; + +public slots: + void setTorrent(const QString &torrentFile); + +private slots: + void selectTorrent(); + void selectDestination(); + void enableOkButton(); + +private: + Ui_AddTorrentFile ui; + QString destinationDirectory; + QString lastDirectory; + QString lastDestinationDirectory; +}; + +#endif diff --git a/examples/network/torrent/bencodeparser.cpp b/examples/network/torrent/bencodeparser.cpp new file mode 100644 index 0000000..9311c0c --- /dev/null +++ b/examples/network/torrent/bencodeparser.cpp @@ -0,0 +1,235 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "bencodeparser.h" + +#include <QList> +#include <QMetaType> + +BencodeParser::BencodeParser() +{ +} + +bool BencodeParser::parse(const QByteArray &content) +{ + if (content.isEmpty()) { + errString = QString("No content"); + return false; + } + + this->content = content; + index = 0; + infoStart = 0; + infoLength = 0; + return getDictionary(&dictionaryValue); +} + +QString BencodeParser::errorString() const +{ + return errString; +} + +QMap<QByteArray, QVariant> BencodeParser::dictionary() const +{ + return dictionaryValue; +} + +QByteArray BencodeParser::infoSection() const +{ + return content.mid(infoStart, infoLength); +} + +bool BencodeParser::getByteString(QByteArray *byteString) +{ + const int contentSize = content.size(); + int size = -1; + do { + char c = content.at(index); + if (c < '0' || c > '9') { + if (size == -1) + return false; + if (c != ':') { + errString = QString("Unexpected character at pos %1: %2") + .arg(index).arg(c); + return false; + } + ++index; + break; + } + if (size == -1) + size = 0; + size *= 10; + size += c - '0'; + } while (++index < contentSize); + + if (byteString) + *byteString = content.mid(index, size); + index += size; + return true; +} + +bool BencodeParser::getInteger(qint64 *integer) +{ + const int contentSize = content.size(); + if (content.at(index) != 'i') + return false; + + ++index; + qint64 num = -1; + bool negative = false; + + do { + char c = content.at(index); + if (c < '0' || c > '9') { + if (num == -1) { + if (c != '-' || negative) + return false; + negative = true; + continue; + } else { + if (c != 'e') { + errString = QString("Unexpected character at pos %1: %2") + .arg(index).arg(c); + return false; + } + ++index; + break; + } + } + if (num == -1) + num = 0; + num *= 10; + num += c - '0'; + } while (++index < contentSize); + + if (integer) + *integer = negative ? -num : num; + return true; +} + +bool BencodeParser::getList(QList<QVariant> *list) +{ + const int contentSize = content.size(); + if (content.at(index) != 'l') + return false; + + QList<QVariant> tmp; + ++index; + + do { + if (content.at(index) == 'e') { + ++index; + break; + } + + qint64 number; + QByteArray byteString; + QList<QVariant> tmpList; + QMap<QByteArray, QVariant> dictionary; + + if (getInteger(&number)) + tmp << number; + else if (getByteString(&byteString)) + tmp << byteString; + else if (getList(&tmpList)) + tmp << tmpList; + else if (getDictionary(&dictionary)) + tmp << qVariantFromValue<QMap<QByteArray, QVariant> >(dictionary); + else { + errString = QString("error at index %1").arg(index); + return false; + } + } while (index < contentSize); + + if (list) + *list = tmp; + return true; +} + +bool BencodeParser::getDictionary(QMap<QByteArray, QVariant> *dictionary) +{ + const int contentSize = content.size(); + if (content.at(index) != 'd') + return false; + + QMap<QByteArray, QVariant> tmp; + ++index; + + do { + if (content.at(index) == 'e') { + ++index; + break; + } + + QByteArray key; + if (!getByteString(&key)) + break; + + if (key == "info") + infoStart = index; + + qint64 number; + QByteArray byteString; + QList<QVariant> tmpList; + QMap<QByteArray, QVariant> dictionary; + + if (getInteger(&number)) + tmp.insert(key, number); + else if (getByteString(&byteString)) + tmp.insert(key, byteString); + else if (getList(&tmpList)) + tmp.insert(key, tmpList); + else if (getDictionary(&dictionary)) + tmp.insert(key, qVariantFromValue<QMap<QByteArray, QVariant> >(dictionary)); + else { + errString = QString("error at index %1").arg(index); + return false; + } + + if (key == "info") + infoLength = index - infoStart; + + } while (index < contentSize); + + if (dictionary) + *dictionary = tmp; + return true; +} diff --git a/examples/network/torrent/bencodeparser.h b/examples/network/torrent/bencodeparser.h new file mode 100644 index 0000000..b4fc74b --- /dev/null +++ b/examples/network/torrent/bencodeparser.h @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BENCODEPARSER_H +#define BENCODEPARSER_H + +#include <QByteArray> +#include <QMap> +#include <QString> +#include <QVariant> +#include <QList> + +typedef QMap<QByteArray,QVariant> Dictionary; +Q_DECLARE_METATYPE(Dictionary) + +class BencodeParser +{ +public: + BencodeParser(); + + bool parse(const QByteArray &content); + QString errorString() const; + + QMap<QByteArray, QVariant> dictionary() const; + QByteArray infoSection() const; + +private: + bool getByteString(QByteArray *byteString); + bool getInteger(qint64 *integer); + bool getList(QList<QVariant> *list); + bool getDictionary(QMap<QByteArray, QVariant> *dictionary); + + QMap<QByteArray, QVariant> dictionaryValue; + + QString errString; + QByteArray content; + int index; + + int infoStart; + int infoLength; +}; + +#endif diff --git a/examples/network/torrent/connectionmanager.cpp b/examples/network/torrent/connectionmanager.cpp new file mode 100644 index 0000000..3b78537 --- /dev/null +++ b/examples/network/torrent/connectionmanager.cpp @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "connectionmanager.h" + +#include <QByteArray> +#include <QDateTime> + +static const int MaxConnections = 250; + +Q_GLOBAL_STATIC(ConnectionManager, connectionManager) + +ConnectionManager *ConnectionManager::instance() +{ + return connectionManager(); +} + +bool ConnectionManager::canAddConnection() const +{ + return (connections.size() < MaxConnections); +} + +void ConnectionManager::addConnection(PeerWireClient *client) +{ + connections << client; +} + +void ConnectionManager::removeConnection(PeerWireClient *client) +{ + connections.remove(client); +} + +int ConnectionManager::maxConnections() const +{ + return MaxConnections; +} + +QByteArray ConnectionManager::clientId() const +{ + if (id.isEmpty()) { + // Generate peer id + int startupTime = int(QDateTime::currentDateTime().toTime_t()); + + QString s; + s.sprintf("-QT%04x-", (QT_VERSION % 0xffff00) >> 8); + id += s.toLatin1(); + id += QByteArray::number(startupTime, 10); + id += QByteArray(20 - id.size(), '-'); + } + return id; +} diff --git a/examples/network/torrent/connectionmanager.h b/examples/network/torrent/connectionmanager.h new file mode 100644 index 0000000..3b791e3 --- /dev/null +++ b/examples/network/torrent/connectionmanager.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef CONNECTIONMANAGER_H +#define CONNECTIONMANAGER_H + +class PeerWireClient; + +#include <QByteArray> +#include <QSet> + +class ConnectionManager +{ +public: + static ConnectionManager *instance(); + + bool canAddConnection() const; + void addConnection(PeerWireClient *connection); + void removeConnection(PeerWireClient *connection); + int maxConnections() const; + QByteArray clientId() const; + + private: + QSet<PeerWireClient *> connections; + mutable QByteArray id; +}; + +#endif diff --git a/examples/network/torrent/filemanager.cpp b/examples/network/torrent/filemanager.cpp new file mode 100644 index 0000000..07d5cca --- /dev/null +++ b/examples/network/torrent/filemanager.cpp @@ -0,0 +1,447 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "filemanager.h" +#include "metainfo.h" + +#include <QByteArray> +#include <QDir> +#include <QFile> +#include <QTimer> +#include <QTimerEvent> +#include <QCryptographicHash> + +FileManager::FileManager(QObject *parent) + : QThread(parent) +{ + quit = false; + totalLength = 0; + readId = 0; + startVerification = false; + wokeUp = false; + newFile = false; + numPieces = 0; + verifiedPieces.fill(false); +} + +FileManager::~FileManager() +{ + quit = true; + cond.wakeOne(); + wait(); + + foreach (QFile *file, files) { + file->close(); + delete file; + } +} + +int FileManager::read(int pieceIndex, int offset, int length) +{ + ReadRequest request; + request.pieceIndex = pieceIndex; + request.offset = offset; + request.length = length; + + QMutexLocker locker(&mutex); + request.id = readId++; + readRequests << request; + + if (!wokeUp) { + wokeUp = true; + QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection); + } + + return request.id; +} + +void FileManager::write(int pieceIndex, int offset, const QByteArray &data) +{ + WriteRequest request; + request.pieceIndex = pieceIndex; + request.offset = offset; + request.data = data; + + QMutexLocker locker(&mutex); + writeRequests << request; + + if (!wokeUp) { + wokeUp = true; + QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection); + } +} + +void FileManager::verifyPiece(int pieceIndex) +{ + QMutexLocker locker(&mutex); + pendingVerificationRequests << pieceIndex; + startVerification = true; + + if (!wokeUp) { + wokeUp = true; + QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection); + } +} + +int FileManager::pieceLengthAt(int pieceIndex) const +{ + QMutexLocker locker(&mutex); + return (sha1s.size() == pieceIndex + 1) + ? (totalLength % pieceLength) : pieceLength; +} + +QBitArray FileManager::completedPieces() const +{ + QMutexLocker locker(&mutex); + return verifiedPieces; +} + +void FileManager::setCompletedPieces(const QBitArray &pieces) +{ + QMutexLocker locker(&mutex); + verifiedPieces = pieces; +} + +QString FileManager::errorString() const +{ + return errString; +} + +void FileManager::run() +{ + if (!generateFiles()) + return; + + do { + { + // Go to sleep if there's nothing to do. + QMutexLocker locker(&mutex); + if (!quit && readRequests.isEmpty() && writeRequests.isEmpty() && !startVerification) + cond.wait(&mutex); + } + + // Read pending read requests + mutex.lock(); + QList<ReadRequest> newReadRequests = readRequests; + readRequests.clear(); + mutex.unlock(); + while (!newReadRequests.isEmpty()) { + ReadRequest request = newReadRequests.takeFirst(); + QByteArray block = readBlock(request.pieceIndex, request.offset, request.length); + emit dataRead(request.id, request.pieceIndex, request.offset, block); + } + + // Write pending write requests + mutex.lock(); + QList<WriteRequest> newWriteRequests = writeRequests; + writeRequests.clear(); + while (!quit && !newWriteRequests.isEmpty()) { + WriteRequest request = newWriteRequests.takeFirst(); + writeBlock(request.pieceIndex, request.offset, request.data); + } + + // Process pending verification requests + if (startVerification) { + newPendingVerificationRequests = pendingVerificationRequests; + pendingVerificationRequests.clear(); + verifyFileContents(); + startVerification = false; + } + mutex.unlock(); + newPendingVerificationRequests.clear(); + + } while (!quit); + + // Write pending write requests + mutex.lock(); + QList<WriteRequest> newWriteRequests = writeRequests; + writeRequests.clear(); + mutex.unlock(); + while (!newWriteRequests.isEmpty()) { + WriteRequest request = newWriteRequests.takeFirst(); + writeBlock(request.pieceIndex, request.offset, request.data); + } +} + +void FileManager::startDataVerification() +{ + QMutexLocker locker(&mutex); + startVerification = true; + cond.wakeOne(); +} + +bool FileManager::generateFiles() +{ + numPieces = -1; + + // Set up the thread local data + if (metaInfo.fileForm() == MetaInfo::SingleFileForm) { + QMutexLocker locker(&mutex); + MetaInfoSingleFile singleFile = metaInfo.singleFile(); + + QString prefix; + if (!destinationPath.isEmpty()) { + prefix = destinationPath; + if (!prefix.endsWith("/")) + prefix += "/"; + QDir dir; + if (!dir.mkpath(prefix)) { + errString = tr("Failed to create directory %1").arg(prefix); + emit error(); + return false; + } + } + QFile *file = new QFile(prefix + singleFile.name); + if (!file->open(QFile::ReadWrite)) { + errString = tr("Failed to open/create file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + return false; + } + + if (file->size() != singleFile.length) { + newFile = true; + if (!file->resize(singleFile.length)) { + errString = tr("Failed to resize file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + return false; + } + } + fileSizes << file->size(); + files << file; + file->close(); + + pieceLength = singleFile.pieceLength; + totalLength = singleFile.length; + sha1s = singleFile.sha1Sums; + } else { + QMutexLocker locker(&mutex); + QDir dir; + QString prefix; + + if (!destinationPath.isEmpty()) { + prefix = destinationPath; + if (!prefix.endsWith("/")) + prefix += "/"; + } + if (!metaInfo.name().isEmpty()) { + prefix += metaInfo.name(); + if (!prefix.endsWith("/")) + prefix += "/"; + } + if (!dir.mkpath(prefix)) { + errString = tr("Failed to create directory %1").arg(prefix); + emit error(); + return false; + } + + foreach (const MetaInfoMultiFile &entry, metaInfo.multiFiles()) { + QString filePath = QFileInfo(prefix + entry.path).path(); + if (!QFile::exists(filePath)) { + if (!dir.mkpath(filePath)) { + errString = tr("Failed to create directory %1").arg(filePath); + emit error(); + return false; + } + } + + QFile *file = new QFile(prefix + entry.path); + if (!file->open(QFile::ReadWrite)) { + errString = tr("Failed to open/create file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + return false; + } + + if (file->size() != entry.length) { + newFile = true; + if (!file->resize(entry.length)) { + errString = tr("Failed to resize file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + return false; + } + } + fileSizes << file->size(); + files << file; + file->close(); + + totalLength += entry.length; + } + + sha1s = metaInfo.sha1Sums(); + pieceLength = metaInfo.pieceLength(); + } + numPieces = sha1s.size(); + return true; +} + +QByteArray FileManager::readBlock(int pieceIndex, int offset, int length) +{ + QByteArray block; + qint64 startReadIndex = (quint64(pieceIndex) * pieceLength) + offset; + qint64 currentIndex = 0; + + for (int i = 0; !quit && i < files.size() && length > 0; ++i) { + QFile *file = files[i]; + qint64 currentFileSize = fileSizes.at(i); + if ((currentIndex + currentFileSize) > startReadIndex) { + if (!file->isOpen()) { + if (!file->open(QFile::ReadWrite)) { + errString = tr("Failed to read from file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + break; + } + } + + file->seek(startReadIndex - currentIndex); + QByteArray chunk = file->read(qMin<qint64>(length, currentFileSize - file->pos())); + file->close(); + + block += chunk; + length -= chunk.size(); + startReadIndex += chunk.size(); + if (length < 0) { + errString = tr("Failed to read from file %1 (read %3 bytes): %2") + .arg(file->fileName()).arg(file->errorString()).arg(length); + emit error(); + break; + } + } + currentIndex += currentFileSize; + } + return block; +} + +bool FileManager::writeBlock(int pieceIndex, int offset, const QByteArray &data) +{ + qint64 startWriteIndex = (qint64(pieceIndex) * pieceLength) + offset; + qint64 currentIndex = 0; + int bytesToWrite = data.size(); + int written = 0; + + for (int i = 0; !quit && i < files.size(); ++i) { + QFile *file = files[i]; + qint64 currentFileSize = fileSizes.at(i); + + if ((currentIndex + currentFileSize) > startWriteIndex) { + if (!file->isOpen()) { + if (!file->open(QFile::ReadWrite)) { + errString = tr("Failed to write to file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + break; + } + } + + file->seek(startWriteIndex - currentIndex); + qint64 bytesWritten = file->write(data.constData() + written, + qMin<qint64>(bytesToWrite, currentFileSize - file->pos())); + file->close(); + + if (bytesWritten <= 0) { + errString = tr("Failed to write to file %1: %2") + .arg(file->fileName()).arg(file->errorString()); + emit error(); + return false; + } + + written += bytesWritten; + startWriteIndex += bytesWritten; + bytesToWrite -= bytesWritten; + if (bytesToWrite == 0) + break; + } + currentIndex += currentFileSize; + } + return true; +} + +void FileManager::verifyFileContents() +{ + // Verify all pieces the first time + if (newPendingVerificationRequests.isEmpty()) { + if (verifiedPieces.count(true) == 0) { + verifiedPieces.resize(sha1s.size()); + + int oldPercent = 0; + if (!newFile) { + int numPieces = sha1s.size(); + + for (int index = 0; index < numPieces; ++index) { + verifySinglePiece(index); + + int percent = ((index + 1) * 100) / numPieces; + if (oldPercent != percent) { + emit verificationProgress(percent); + oldPercent = percent; + } + } + } + } + emit verificationDone(); + return; + } + + // Verify all pending pieces + foreach (int index, newPendingVerificationRequests) + emit pieceVerified(index, verifySinglePiece(index)); +} + +bool FileManager::verifySinglePiece(int pieceIndex) +{ + QByteArray block = readBlock(pieceIndex, 0, pieceLength); + QByteArray sha1Sum = QCryptographicHash::hash(block, QCryptographicHash::Sha1); + + if (sha1Sum != sha1s.at(pieceIndex)) + return false; + verifiedPieces.setBit(pieceIndex); + return true; +} + +void FileManager::wakeUp() +{ + QMutexLocker locker(&mutex); + wokeUp = false; + cond.wakeOne(); +} diff --git a/examples/network/torrent/filemanager.h b/examples/network/torrent/filemanager.h new file mode 100644 index 0000000..05d1f49 --- /dev/null +++ b/examples/network/torrent/filemanager.h @@ -0,0 +1,144 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef FILEMANAGER_H +#define FILEMANAGER_H + +#include <QBitArray> +#include <QList> +#include <QMutex> +#include <QThread> +#include <QWaitCondition> + +#include "metainfo.h" + +QT_BEGIN_NAMESPACE +class QByteArray; +class QFile; +class QTimerEvent; +QT_END_NAMESPACE + +class FileManager : public QThread +{ + Q_OBJECT + +public: + FileManager(QObject *parent = 0); + virtual ~FileManager(); + + inline void setMetaInfo(const MetaInfo &info) { metaInfo = info; } + inline void setDestinationFolder(const QString &directory) { destinationPath = directory; } + + int read(int pieceIndex, int offset, int length); + void write(int pieceIndex, int offset, const QByteArray &data); + void verifyPiece(int pieceIndex); + inline qint64 totalSize() const { return totalLength; } + + inline int pieceCount() const { return numPieces; } + int pieceLengthAt(int pieceIndex) const; + + QBitArray completedPieces() const; + void setCompletedPieces(const QBitArray &pieces); + + QString errorString() const; + +public slots: + void startDataVerification(); + +signals: + void dataRead(int id, int pieceIndex, int offset, const QByteArray &data); + void error(); + void verificationProgress(int percent); + void verificationDone(); + void pieceVerified(int pieceIndex, bool verified); + +protected: + void run(); + +private slots: + bool verifySinglePiece(int pieceIndex); + void wakeUp(); + +private: + bool generateFiles(); + QByteArray readBlock(int pieceIndex, int offset, int length); + bool writeBlock(int pieceIndex, int offset, const QByteArray &data); + void verifyFileContents(); + + struct WriteRequest { + int pieceIndex; + int offset; + QByteArray data; + }; + struct ReadRequest { + int pieceIndex; + int offset; + int length; + int id; + }; + + QString errString; + QString destinationPath; + MetaInfo metaInfo; + QList<QFile *> files; + QList<QByteArray> sha1s; + QBitArray verifiedPieces; + + bool newFile; + int pieceLength; + qint64 totalLength; + int numPieces; + int readId; + bool startVerification; + bool quit; + bool wokeUp; + + QList<WriteRequest> writeRequests; + QList<ReadRequest> readRequests; + QList<int> pendingVerificationRequests; + QList<int> newPendingVerificationRequests; + QList<qint64> fileSizes; + + mutable QMutex mutex; + mutable QWaitCondition cond; +}; + +#endif diff --git a/examples/network/torrent/forms/addtorrentform.ui b/examples/network/torrent/forms/addtorrentform.ui new file mode 100644 index 0000000..950bb67 --- /dev/null +++ b/examples/network/torrent/forms/addtorrentform.ui @@ -0,0 +1,266 @@ +<ui version="4.0" > + <author></author> + <comment></comment> + <exportmacro></exportmacro> + <class>AddTorrentFile</class> + <widget class="QDialog" name="AddTorrentFile" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>464</width> + <height>385</height> + </rect> + </property> + <property name="windowTitle" > + <string>Add a torrent</string> + </property> + <property name="sizeGripEnabled" > + <bool>false</bool> + </property> + <property name="modal" > + <bool>true</bool> + </property> + <layout class="QVBoxLayout" > + <property name="margin" > + <number>8</number> + </property> + <property name="spacing" > + <number>6</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox" > + <property name="title" > + <string>Select a torrent source</string> + </property> + <layout class="QGridLayout" > + <property name="margin" > + <number>8</number> + </property> + <property name="spacing" > + <number>6</number> + </property> + <item row="6" column="0" > + <widget class="QLabel" name="label_4" > + <property name="text" > + <string>Destination:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2" > + <widget class="QLineEdit" name="torrentFile" /> + </item> + <item row="1" column="0" > + <widget class="QLabel" name="label_2" > + <property name="text" > + <string>Tracker URL:</string> + </property> + </widget> + </item> + <item row="0" column="3" > + <widget class="QPushButton" name="browseTorrents" > + <property name="text" > + <string>Browse</string> + </property> + <property name="default" > + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0" > + <widget class="QLabel" name="label_5" > + <property name="text" > + <string>File(s):</string> + </property> + <property name="alignment" > + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="4" column="0" > + <widget class="QLabel" name="label_3" > + <property name="text" > + <string>Size:</string> + </property> + </widget> + </item> + <item row="2" column="0" > + <widget class="QLabel" name="label_6" > + <property name="text" > + <string>Creator:</string> + </property> + </widget> + </item> + <item row="5" column="1" colspan="3" > + <widget class="QTextEdit" name="torrentContents" > + <property name="focusPolicy" > + <enum>Qt::NoFocus</enum> + </property> + <property name="tabChangesFocus" > + <bool>true</bool> + </property> + <property name="lineWrapMode" > + <enum>QTextEdit::NoWrap</enum> + </property> + <property name="readOnly" > + <bool>true</bool> + </property> + </widget> + </item> + <item row="6" column="1" colspan="2" > + <widget class="QLineEdit" name="destinationFolder" > + <property name="focusPolicy" > + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item row="1" column="1" colspan="3" > + <widget class="QLabel" name="announceUrl" > + <property name="text" > + <string><none></string> + </property> + </widget> + </item> + <item row="0" column="0" > + <widget class="QLabel" name="label" > + <property name="text" > + <string>Torrent file:</string> + </property> + </widget> + </item> + <item row="6" column="3" > + <widget class="QPushButton" name="browseDestination" > + <property name="text" > + <string>Browse</string> + </property> + </widget> + </item> + <item row="3" column="0" > + <widget class="QLabel" name="label_7" > + <property name="text" > + <string>Comment:</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="3" > + <widget class="QLabel" name="commentLabel" > + <property name="text" > + <string><none></string> + </property> + </widget> + </item> + <item row="2" column="1" colspan="3" > + <widget class="QLabel" name="creatorLabel" > + <property name="text" > + <string><none></string> + </property> + </widget> + </item> + <item row="4" column="1" colspan="3" > + <widget class="QLabel" name="sizeLabel" > + <property name="text" > + <string>0</string> + </property> + </widget> + </item> + </layout> + <widget class="QWidget" name="widget" > + <property name="geometry" > + <rect> + <x>10</x> + <y>40</y> + <width>364</width> + <height>33</height> + </rect> + </property> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" > + <property name="margin" > + <number>0</number> + </property> + <property name="spacing" > + <number>6</number> + </property> + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" > + <size> + <width>131</width> + <height>31</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="okButton" > + <property name="enabled" > + <bool>false</bool> + </property> + <property name="text" > + <string>&OK</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancelButton" > + <property name="text" > + <string>&Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <pixmapfunction></pixmapfunction> + <tabstops> + <tabstop>torrentFile</tabstop> + <tabstop>browseTorrents</tabstop> + <tabstop>torrentContents</tabstop> + <tabstop>destinationFolder</tabstop> + <tabstop>browseDestination</tabstop> + <tabstop>okButton</tabstop> + <tabstop>cancelButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>okButton</sender> + <signal>clicked()</signal> + <receiver>AddTorrentFile</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>278</x> + <y>253</y> + </hint> + <hint type="destinationlabel" > + <x>96</x> + <y>254</y> + </hint> + </hints> + </connection> + <connection> + <sender>cancelButton</sender> + <signal>clicked()</signal> + <receiver>AddTorrentFile</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>369</x> + <y>253</y> + </hint> + <hint type="destinationlabel" > + <x>179</x> + <y>282</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/network/torrent/icons.qrc b/examples/network/torrent/icons.qrc new file mode 100644 index 0000000..9541ef7 --- /dev/null +++ b/examples/network/torrent/icons.qrc @@ -0,0 +1,12 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource prefix="/"> + <file>icons/peertopeer.png</file> + <file>icons/1uparrow.png</file> + <file>icons/1downarrow.png</file> + <file>icons/bottom.png</file> + <file>icons/player_pause.png</file> + <file>icons/player_play.png</file> + <file>icons/player_stop.png</file> + <file>icons/exit.png</file> +</qresource> +</RCC> diff --git a/examples/network/torrent/icons/1downarrow.png b/examples/network/torrent/icons/1downarrow.png Binary files differnew file mode 100644 index 0000000..08403b8 --- /dev/null +++ b/examples/network/torrent/icons/1downarrow.png diff --git a/examples/network/torrent/icons/1uparrow.png b/examples/network/torrent/icons/1uparrow.png Binary files differnew file mode 100644 index 0000000..f044811 --- /dev/null +++ b/examples/network/torrent/icons/1uparrow.png diff --git a/examples/network/torrent/icons/bottom.png b/examples/network/torrent/icons/bottom.png Binary files differnew file mode 100644 index 0000000..fe66b5d --- /dev/null +++ b/examples/network/torrent/icons/bottom.png diff --git a/examples/network/torrent/icons/edit_add.png b/examples/network/torrent/icons/edit_add.png Binary files differnew file mode 100644 index 0000000..85b022e --- /dev/null +++ b/examples/network/torrent/icons/edit_add.png diff --git a/examples/network/torrent/icons/edit_remove.png b/examples/network/torrent/icons/edit_remove.png Binary files differnew file mode 100644 index 0000000..93361f5 --- /dev/null +++ b/examples/network/torrent/icons/edit_remove.png diff --git a/examples/network/torrent/icons/exit.png b/examples/network/torrent/icons/exit.png Binary files differnew file mode 100644 index 0000000..2f7ff43 --- /dev/null +++ b/examples/network/torrent/icons/exit.png diff --git a/examples/network/torrent/icons/peertopeer.png b/examples/network/torrent/icons/peertopeer.png Binary files differnew file mode 100644 index 0000000..f4856dc --- /dev/null +++ b/examples/network/torrent/icons/peertopeer.png diff --git a/examples/network/torrent/icons/player_pause.png b/examples/network/torrent/icons/player_pause.png Binary files differnew file mode 100644 index 0000000..8c9bcc4 --- /dev/null +++ b/examples/network/torrent/icons/player_pause.png diff --git a/examples/network/torrent/icons/player_play.png b/examples/network/torrent/icons/player_play.png Binary files differnew file mode 100644 index 0000000..70daa33 --- /dev/null +++ b/examples/network/torrent/icons/player_play.png diff --git a/examples/network/torrent/icons/player_stop.png b/examples/network/torrent/icons/player_stop.png Binary files differnew file mode 100644 index 0000000..ce6585a --- /dev/null +++ b/examples/network/torrent/icons/player_stop.png diff --git a/examples/network/torrent/icons/stop.png b/examples/network/torrent/icons/stop.png Binary files differnew file mode 100644 index 0000000..52e593a --- /dev/null +++ b/examples/network/torrent/icons/stop.png diff --git a/examples/network/torrent/main.cpp b/examples/network/torrent/main.cpp new file mode 100644 index 0000000..3821254 --- /dev/null +++ b/examples/network/torrent/main.cpp @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include <QApplication> +#include <QtCore> + +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); + + Q_INIT_RESOURCE(icons); + + MainWindow window; + window.show(); + + return app.exec(); +} diff --git a/examples/network/torrent/mainwindow.cpp b/examples/network/torrent/mainwindow.cpp new file mode 100644 index 0000000..6ca3247 --- /dev/null +++ b/examples/network/torrent/mainwindow.cpp @@ -0,0 +1,713 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtGui> + +#include "addtorrentdialog.h" +#include "mainwindow.h" +#include "ratecontroller.h" +#include "torrentclient.h" + +// TorrentView extends QTreeWidget to allow drag and drop. +class TorrentView : public QTreeWidget +{ + Q_OBJECT +public: + TorrentView(QWidget *parent = 0); + +signals: + void fileDropped(const QString &fileName); + +protected: + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); +}; + +// TorrentViewDelegate is used to draw the progress bars. +class TorrentViewDelegate : public QItemDelegate +{ + Q_OBJECT +public: + inline TorrentViewDelegate(MainWindow *mainWindow) : QItemDelegate(mainWindow) {} + + inline void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index ) const + { + if (index.column() != 2) { + QItemDelegate::paint(painter, option, index); + return; + } + + // Set up a QStyleOptionProgressBar to precisely mimic the + // environment of a progress bar. + QStyleOptionProgressBar progressBarOption; + progressBarOption.state = QStyle::State_Enabled; + progressBarOption.direction = QApplication::layoutDirection(); + progressBarOption.rect = option.rect; + progressBarOption.fontMetrics = QApplication::fontMetrics(); + progressBarOption.minimum = 0; + progressBarOption.maximum = 100; + progressBarOption.textAlignment = Qt::AlignCenter; + progressBarOption.textVisible = true; + + // Set the progress and text values of the style option. + int progress = qobject_cast<MainWindow *>(parent())->clientForRow(index.row())->progress(); + progressBarOption.progress = progress < 0 ? 0 : progress; + progressBarOption.text = QString().sprintf("%d%%", progressBarOption.progress); + + // Draw the progress bar onto the view. + QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter); + } +}; + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), quitDialog(0), saveChanges(false) +{ + // Initialize some static strings + QStringList headers; + headers << tr("Torrent") << tr("Peers/Seeds") << tr("Progress") + << tr("Down rate") << tr("Up rate") << tr("Status"); + + // Main torrent list + torrentView = new TorrentView(this); + torrentView->setItemDelegate(new TorrentViewDelegate(this)); + torrentView->setHeaderLabels(headers); + torrentView->setSelectionBehavior(QAbstractItemView::SelectRows); + torrentView->setAlternatingRowColors(true); + torrentView->setRootIsDecorated(false); + setCentralWidget(torrentView); + + // Set header resize modes and initial section sizes + QFontMetrics fm = fontMetrics(); + QHeaderView *header = torrentView->header(); + header->resizeSection(0, fm.width("typical-name-for-a-torrent.torrent")); + header->resizeSection(1, fm.width(headers.at(1) + " ")); + header->resizeSection(2, fm.width(headers.at(2) + " ")); + header->resizeSection(3, qMax(fm.width(headers.at(3) + " "), fm.width(" 1234.0 KB/s "))); + header->resizeSection(4, qMax(fm.width(headers.at(4) + " "), fm.width(" 1234.0 KB/s "))); + header->resizeSection(5, qMax(fm.width(headers.at(5) + " "), fm.width(tr("Downloading") + " "))); + + // Create common actions + QAction *newTorrentAction = new QAction(QIcon(":/icons/bottom.png"), tr("Add &new torrent"), this); + pauseTorrentAction = new QAction(QIcon(":/icons/player_pause.png"), tr("&Pause torrent"), this); + removeTorrentAction = new QAction(QIcon(":/icons/player_stop.png"), tr("&Remove torrent"), this); + + // File menu + QMenu *fileMenu = menuBar()->addMenu(tr("&File")); + fileMenu->addAction(newTorrentAction); + fileMenu->addAction(pauseTorrentAction); + fileMenu->addAction(removeTorrentAction); + fileMenu->addSeparator(); + fileMenu->addAction(QIcon(":/icons/exit.png"), tr("E&xit"), this, SLOT(close())); + + // Help menu + QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); + helpMenu->addAction(tr("&About"), this, SLOT(about())); + helpMenu->addAction(tr("About &Qt"), qApp, SLOT(aboutQt())); + + // Top toolbar + QToolBar *topBar = new QToolBar(tr("Tools")); + addToolBar(Qt::TopToolBarArea, topBar); + topBar->setMovable(false); + topBar->addAction(newTorrentAction); + topBar->addAction(removeTorrentAction); + topBar->addAction(pauseTorrentAction); + topBar->addSeparator(); + downActionTool = topBar->addAction(QIcon(tr(":/icons/1downarrow.png")), tr("Move down")); + upActionTool = topBar->addAction(QIcon(tr(":/icons/1uparrow.png")), tr("Move up")); + + // Bottom toolbar + QToolBar *bottomBar = new QToolBar(tr("Rate control")); + addToolBar(Qt::BottomToolBarArea, bottomBar); + bottomBar->setMovable(false); + downloadLimitSlider = new QSlider(Qt::Horizontal); + downloadLimitSlider->setRange(0, 1000); + bottomBar->addWidget(new QLabel(tr("Max download:"))); + bottomBar->addWidget(downloadLimitSlider); + bottomBar->addWidget((downloadLimitLabel = new QLabel(tr("0 KB/s")))); + downloadLimitLabel->setFixedSize(QSize(fm.width(tr("99999 KB/s")), fm.lineSpacing())); + bottomBar->addSeparator(); + uploadLimitSlider = new QSlider(Qt::Horizontal); + uploadLimitSlider->setRange(0, 1000); + bottomBar->addWidget(new QLabel(tr("Max upload:"))); + bottomBar->addWidget(uploadLimitSlider); + bottomBar->addWidget((uploadLimitLabel = new QLabel(tr("0 KB/s")))); + uploadLimitLabel->setFixedSize(QSize(fm.width(tr("99999 KB/s")), fm.lineSpacing())); + + // Set up connections + connect(torrentView, SIGNAL(itemSelectionChanged()), + this, SLOT(setActionsEnabled())); + connect(torrentView, SIGNAL(fileDropped(const QString &)), + this, SLOT(acceptFileDrop(const QString &))); + connect(uploadLimitSlider, SIGNAL(valueChanged(int)), + this, SLOT(setUploadLimit(int))); + connect(downloadLimitSlider, SIGNAL(valueChanged(int)), + this, SLOT(setDownloadLimit(int))); + connect(newTorrentAction, SIGNAL(triggered()), + this, SLOT(addTorrent())); + connect(pauseTorrentAction, SIGNAL(triggered()), + this, SLOT(pauseTorrent())); + connect(removeTorrentAction, SIGNAL(triggered()), + this, SLOT(removeTorrent())); + connect(upActionTool, SIGNAL(triggered(bool)), + this, SLOT(moveTorrentUp())); + connect(downActionTool, SIGNAL(triggered(bool)), + this, SLOT(moveTorrentDown())); + + // Load settings and start + setWindowTitle(tr("Torrent Client")); + setActionsEnabled(); + QMetaObject::invokeMethod(this, "loadSettings", Qt::QueuedConnection); +} + +QSize MainWindow::sizeHint() const +{ + const QHeaderView *header = torrentView->header(); + + // Add up the sizes of all header sections. The last section is + // stretched, so its size is relative to the size of the width; + // instead of counting it, we count the size of its largest value. + int width = fontMetrics().width(tr("Downloading") + " "); + for (int i = 0; i < header->count() - 1; ++i) + width += header->sectionSize(i); + + return QSize(width, QMainWindow::sizeHint().height()) + .expandedTo(QApplication::globalStrut()); +} + +const TorrentClient *MainWindow::clientForRow(int row) const +{ + // Return the client at the given row. + return jobs.at(row).client; +} + +int MainWindow::rowOfClient(TorrentClient *client) const +{ + // Return the row that displays this client's status, or -1 if the + // client is not known. + int row = 0; + foreach (Job job, jobs) { + if (job.client == client) + return row; + ++row; + } + return -1; +} + +void MainWindow::loadSettings() +{ + // Load base settings (last working directory, upload/download limits). + QSettings settings("Trolltech", "Torrent"); + lastDirectory = settings.value("LastDirectory").toString(); + if (lastDirectory.isEmpty()) + lastDirectory = QDir::currentPath(); + int up = settings.value("UploadLimit").toInt(); + int down = settings.value("DownloadLimit").toInt(); + uploadLimitSlider->setValue(up ? up : 170); + downloadLimitSlider->setValue(down ? down : 550); + + // Resume all previous downloads. + int size = settings.beginReadArray("Torrents"); + for (int i = 0; i < size; ++i) { + settings.setArrayIndex(i); + QByteArray resumeState = settings.value("resumeState").toByteArray(); + QString fileName = settings.value("sourceFileName").toString(); + QString dest = settings.value("destinationFolder").toString(); + + if (addTorrent(fileName, dest, resumeState)) { + TorrentClient *client = jobs.last().client; + client->setDownloadedBytes(settings.value("downloadedBytes").toLongLong()); + client->setUploadedBytes(settings.value("uploadedBytes").toLongLong()); + } + } +} + +bool MainWindow::addTorrent() +{ + // Show the file dialog, let the user select what torrent to start downloading. + QString fileName = QFileDialog::getOpenFileName(this, tr("Choose a torrent file"), + lastDirectory, + tr("Torrents (*.torrent);;" + " All files (*.*)")); + if (fileName.isEmpty()) + return false; + lastDirectory = QFileInfo(fileName).absolutePath(); + + // Show the "Add Torrent" dialog. + AddTorrentDialog *addTorrentDialog = new AddTorrentDialog(this); + addTorrentDialog->setTorrent(fileName); + addTorrentDialog->deleteLater(); + if (!addTorrentDialog->exec()) + return false; + + // Add the torrent to our list of downloads + addTorrent(fileName, addTorrentDialog->destinationFolder()); + if (!saveChanges) { + saveChanges = true; + QTimer::singleShot(1000, this, SLOT(saveSettings())); + } + return true; +} + +void MainWindow::removeTorrent() +{ + // Find the row of the current item, and find the torrent client + // for that row. + int row = torrentView->indexOfTopLevelItem(torrentView->currentItem()); + TorrentClient *client = jobs.at(row).client; + + // Stop the client. + client->disconnect(); + connect(client, SIGNAL(stopped()), this, SLOT(torrentStopped())); + client->stop(); + + // Remove the row from the view. + delete torrentView->takeTopLevelItem(row); + jobs.removeAt(row); + setActionsEnabled(); + + saveChanges = true; + saveSettings(); +} + +void MainWindow::torrentStopped() +{ + // Schedule the client for deletion. + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + client->deleteLater(); + + // If the quit dialog is shown, update its progress. + if (quitDialog) { + if (++jobsStopped == jobsToStop) + quitDialog->close(); + } +} + +void MainWindow::torrentError(TorrentClient::Error) +{ + // Delete the client. + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + int row = rowOfClient(client); + QString fileName = jobs.at(row).torrentFileName; + jobs.removeAt(row); + + // Display the warning. + QMessageBox::warning(this, tr("Error"), + tr("An error occurred while downloading %0: %1") + .arg(fileName) + .arg(client->errorString())); + + delete torrentView->takeTopLevelItem(row); + client->deleteLater(); +} + +bool MainWindow::addTorrent(const QString &fileName, const QString &destinationFolder, + const QByteArray &resumeState) +{ + // Check if the torrent is already being downloaded. + foreach (Job job, jobs) { + if (job.torrentFileName == fileName && job.destinationDirectory == destinationFolder) { + QMessageBox::warning(this, tr("Already downloading"), + tr("The torrent file %1 is " + "already being downloaded.").arg(fileName)); + return false; + } + } + + // Create a new torrent client and attempt to parse the torrent data. + TorrentClient *client = new TorrentClient(this); + if (!client->setTorrent(fileName)) { + QMessageBox::warning(this, tr("Error"), + tr("The torrent file %1 cannot not be opened/resumed.").arg(fileName)); + delete client; + return false; + } + client->setDestinationFolder(destinationFolder); + client->setDumpedState(resumeState); + + // Setup the client connections. + connect(client, SIGNAL(stateChanged(TorrentClient::State)), this, SLOT(updateState(TorrentClient::State))); + connect(client, SIGNAL(peerInfoUpdated()), this, SLOT(updatePeerInfo())); + connect(client, SIGNAL(progressUpdated(int)), this, SLOT(updateProgress(int))); + connect(client, SIGNAL(downloadRateUpdated(int)), this, SLOT(updateDownloadRate(int))); + connect(client, SIGNAL(uploadRateUpdated(int)), this, SLOT(updateUploadRate(int))); + connect(client, SIGNAL(stopped()), this, SLOT(torrentStopped())); + connect(client, SIGNAL(error(TorrentClient::Error)), this, SLOT(torrentError(TorrentClient::Error))); + + // Add the client to the list of downloading jobs. + Job job; + job.client = client; + job.torrentFileName = fileName; + job.destinationDirectory = destinationFolder; + jobs << job; + + // Create and add a row in the torrent view for this download. + QTreeWidgetItem *item = new QTreeWidgetItem(torrentView); + + QString baseFileName = QFileInfo(fileName).fileName(); + if (baseFileName.toLower().endsWith(".torrent")) + baseFileName.remove(baseFileName.size() - 8); + + item->setText(0, baseFileName); + item->setToolTip(0, tr("Torrent: %1<br>Destination: %2") + .arg(baseFileName).arg(destinationFolder)); + item->setText(1, tr("0/0")); + item->setText(2, "0"); + item->setText(3, "0.0 KB/s"); + item->setText(4, "0.0 KB/s"); + item->setText(5, tr("Idle")); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setTextAlignment(1, Qt::AlignHCenter); + + if (!saveChanges) { + saveChanges = true; + QTimer::singleShot(5000, this, SLOT(saveSettings())); + } + client->start(); + return true; +} + +void MainWindow::saveSettings() +{ + if (!saveChanges) + return; + saveChanges = false; + + // Prepare and reset the settings + QSettings settings("Trolltech", "Torrent"); + settings.clear(); + + settings.setValue("LastDirectory", lastDirectory); + settings.setValue("UploadLimit", uploadLimitSlider->value()); + settings.setValue("DownloadLimit", downloadLimitSlider->value()); + + // Store data on all known torrents + settings.beginWriteArray("Torrents"); + for (int i = 0; i < jobs.size(); ++i) { + settings.setArrayIndex(i); + settings.setValue("sourceFileName", jobs.at(i).torrentFileName); + settings.setValue("destinationFolder", jobs.at(i).destinationDirectory); + settings.setValue("uploadedBytes", jobs.at(i).client->uploadedBytes()); + settings.setValue("downloadedBytes", jobs.at(i).client->downloadedBytes()); + settings.setValue("resumeState", jobs.at(i).client->dumpedState()); + } + settings.endArray(); + settings.sync(); +} + +void MainWindow::updateState(TorrentClient::State) +{ + // Update the state string whenever the client's state changes. + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + int row = rowOfClient(client); + QTreeWidgetItem *item = torrentView->topLevelItem(row); + if (item) { + item->setToolTip(0, tr("Torrent: %1<br>Destination: %2<br>State: %3") + .arg(jobs.at(row).torrentFileName) + .arg(jobs.at(row).destinationDirectory) + .arg(client->stateString())); + + item->setText(5, client->stateString()); + } + setActionsEnabled(); +} + +void MainWindow::updatePeerInfo() +{ + // Update the number of connected, visited, seed and leecher peers. + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + int row = rowOfClient(client); + + QTreeWidgetItem *item = torrentView->topLevelItem(row); + item->setText(1, tr("%1/%2").arg(client->connectedPeerCount()) + .arg(client->seedCount())); +} + +void MainWindow::updateProgress(int percent) +{ + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + int row = rowOfClient(client); + + // Update the progressbar. + QTreeWidgetItem *item = torrentView->topLevelItem(row); + if (item) + item->setText(2, QString::number(percent)); +} + +void MainWindow::setActionsEnabled() +{ + // Find the view item and client for the current row, and update + // the states of the actions. + QTreeWidgetItem *item = 0; + if (!torrentView->selectedItems().isEmpty()) + item = torrentView->selectedItems().first(); + TorrentClient *client = item ? jobs.at(torrentView->indexOfTopLevelItem(item)).client : 0; + bool pauseEnabled = client && ((client->state() == TorrentClient::Paused) + || (client->state() > TorrentClient::Preparing)); + + removeTorrentAction->setEnabled(item != 0); + pauseTorrentAction->setEnabled(item != 0 && pauseEnabled); + + if (client && client->state() == TorrentClient::Paused) { + pauseTorrentAction->setIcon(QIcon(":/icons/player_play.png")); + pauseTorrentAction->setText(tr("Resume torrent")); + } else { + pauseTorrentAction->setIcon(QIcon(":/icons/player_pause.png")); + pauseTorrentAction->setText(tr("Pause torrent")); + } + + int row = torrentView->indexOfTopLevelItem(item); + upActionTool->setEnabled(item && row != 0); + downActionTool->setEnabled(item && row != jobs.size() - 1); +} + +void MainWindow::updateDownloadRate(int bytesPerSecond) +{ + // Update the download rate. + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + int row = rowOfClient(client); + QString num; + num.sprintf("%.1f KB/s", bytesPerSecond / 1024.0); + torrentView->topLevelItem(row)->setText(3, num); + + if (!saveChanges) { + saveChanges = true; + QTimer::singleShot(5000, this, SLOT(saveSettings())); + } +} + +void MainWindow::updateUploadRate(int bytesPerSecond) +{ + // Update the upload rate. + TorrentClient *client = qobject_cast<TorrentClient *>(sender()); + int row = rowOfClient(client); + QString num; + num.sprintf("%.1f KB/s", bytesPerSecond / 1024.0); + torrentView->topLevelItem(row)->setText(4, num); + + if (!saveChanges) { + saveChanges = true; + QTimer::singleShot(5000, this, SLOT(saveSettings())); + } +} + +void MainWindow::pauseTorrent() +{ + // Pause or unpause the current torrent. + int row = torrentView->indexOfTopLevelItem(torrentView->currentItem()); + TorrentClient *client = jobs.at(row).client; + client->setPaused(client->state() != TorrentClient::Paused); + setActionsEnabled(); +} + +void MainWindow::moveTorrentUp() +{ + QTreeWidgetItem *item = torrentView->currentItem(); + int row = torrentView->indexOfTopLevelItem(item); + if (row == 0) + return; + + Job tmp = jobs.at(row - 1); + jobs[row - 1] = jobs[row]; + jobs[row] = tmp; + + QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(row - 1); + torrentView->insertTopLevelItem(row, itemAbove); + setActionsEnabled(); +} + +void MainWindow::moveTorrentDown() +{ + QTreeWidgetItem *item = torrentView->currentItem(); + int row = torrentView->indexOfTopLevelItem(item); + if (row == jobs.size() - 1) + return; + + Job tmp = jobs.at(row + 1); + jobs[row + 1] = jobs[row]; + jobs[row] = tmp; + + QTreeWidgetItem *itemAbove = torrentView->takeTopLevelItem(row + 1); + torrentView->insertTopLevelItem(row, itemAbove); + setActionsEnabled(); +} + +static int rateFromValue(int value) +{ + int rate = 0; + if (value >= 0 && value < 250) { + rate = 1 + int(value * 0.124); + } else if (value < 500) { + rate = 32 + int((value - 250) * 0.384); + } else if (value < 750) { + rate = 128 + int((value - 500) * 1.536); + } else { + rate = 512 + int((value - 750) * 6.1445); + } + return rate; +} + +void MainWindow::setUploadLimit(int value) +{ + int rate = rateFromValue(value); + uploadLimitLabel->setText(tr("%1 KB/s").arg(QString().sprintf("%4d", rate))); + RateController::instance()->setUploadLimit(rate * 1024); +} + +void MainWindow::setDownloadLimit(int value) +{ + int rate = rateFromValue(value); + downloadLimitLabel->setText(tr("%1 KB/s").arg(QString().sprintf("%4d", rate))); + RateController::instance()->setDownloadLimit(rate * 1024); +} + +void MainWindow::about() +{ + QLabel *icon = new QLabel; + icon->setPixmap(QPixmap(":/icons/peertopeer.png")); + + QLabel *text = new QLabel; + text->setWordWrap(true); + text->setText("<p>The <b>Torrent Client</b> example demonstrates how to" + " write a complete peer-to-peer file sharing" + " application using Qt's network and thread classes.</p>" + "<p>This feature complete client implementation of" + " the BitTorrent protocol can efficiently" + " maintain several hundred network connections" + " simultaneously.</p>"); + + QPushButton *quitButton = new QPushButton("OK"); + + QHBoxLayout *topLayout = new QHBoxLayout; + topLayout->setMargin(10); + topLayout->setSpacing(10); + topLayout->addWidget(icon); + topLayout->addWidget(text); + + QHBoxLayout *bottomLayout = new QHBoxLayout; + bottomLayout->addStretch(); + bottomLayout->addWidget(quitButton); + bottomLayout->addStretch(); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addLayout(topLayout); + mainLayout->addLayout(bottomLayout); + + QDialog about(this); + about.setModal(true); + about.setWindowTitle(tr("About Torrent Client")); + about.setLayout(mainLayout); + + connect(quitButton, SIGNAL(clicked()), &about, SLOT(close())); + + about.exec(); +} + +void MainWindow::acceptFileDrop(const QString &fileName) +{ + // Create and show the "Add Torrent" dialog. + AddTorrentDialog *addTorrentDialog = new AddTorrentDialog; + lastDirectory = QFileInfo(fileName).absolutePath(); + addTorrentDialog->setTorrent(fileName); + addTorrentDialog->deleteLater(); + if (!addTorrentDialog->exec()) + return; + + // Add the torrent to our list of downloads. + addTorrent(fileName, addTorrentDialog->destinationFolder()); + saveSettings(); +} + +void MainWindow::closeEvent(QCloseEvent *) +{ + if (jobs.isEmpty()) + return; + + // Save upload / download numbers. + saveSettings(); + saveChanges = false; + + quitDialog = new QProgressDialog(tr("Disconnecting from trackers"), tr("Abort"), 0, jobsToStop, this); + + // Stop all clients, remove the rows from the view and wait for + // them to signal that they have stopped. + jobsToStop = 0; + jobsStopped = 0; + foreach (Job job, jobs) { + ++jobsToStop; + TorrentClient *client = job.client; + client->disconnect(); + connect(client, SIGNAL(stopped()), this, SLOT(torrentStopped())); + client->stop(); + delete torrentView->takeTopLevelItem(0); + } + + if (jobsToStop > jobsStopped) + quitDialog->exec(); + quitDialog->deleteLater(); + quitDialog = 0; +} + +TorrentView::TorrentView(QWidget *parent) + : QTreeWidget(parent) +{ + setAcceptDrops(true); +} + +void TorrentView::dragMoveEvent(QDragMoveEvent *event) +{ + // Accept file actions with a '.torrent' extension. + QUrl url(event->mimeData()->text()); + if (url.isValid() && url.scheme().toLower() == "file" + && url.path().toLower().endsWith(".torrent")) + event->acceptProposedAction(); +} + +void TorrentView::dropEvent(QDropEvent *event) +{ + // Accept drops if the file has a '.torrent' extension and it + // exists. + QString fileName = QUrl(event->mimeData()->text()).path(); + if (QFile::exists(fileName) && fileName.toLower().endsWith(".torrent")) + emit fileDropped(fileName); +} + +#include "mainwindow.moc" diff --git a/examples/network/torrent/mainwindow.h b/examples/network/torrent/mainwindow.h new file mode 100644 index 0000000..3249781 --- /dev/null +++ b/examples/network/torrent/mainwindow.h @@ -0,0 +1,132 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QList> +#include <QStringList> +#include <QMainWindow> + +#include "torrentclient.h" + +QT_BEGIN_NAMESPACE +class QAction; +class QCloseEvent; +class QLabel; +class QProgressDialog; +class QSlider; +QT_END_NAMESPACE +class TorrentView; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = 0); + + QSize sizeHint() const; + const TorrentClient *clientForRow(int row) const; + +protected: + void closeEvent(QCloseEvent *event); + +private slots: + void loadSettings(); + void saveSettings(); + + bool addTorrent(); + void removeTorrent(); + void pauseTorrent(); + void moveTorrentUp(); + void moveTorrentDown(); + + void torrentStopped(); + void torrentError(TorrentClient::Error error); + + void updateState(TorrentClient::State state); + void updatePeerInfo(); + void updateProgress(int percent); + void updateDownloadRate(int bytesPerSecond); + void updateUploadRate(int bytesPerSecond); + + void setUploadLimit(int bytes); + void setDownloadLimit(int bytes); + + void about(); + void setActionsEnabled(); + void acceptFileDrop(const QString &fileName); + +private: + int rowOfClient(TorrentClient *client) const; + bool addTorrent(const QString &fileName, const QString &destinationFolder, + const QByteArray &resumeState = QByteArray()); + + TorrentView *torrentView; + QAction *pauseTorrentAction; + QAction *removeTorrentAction; + QAction *upActionTool; + QAction *downActionTool; + QSlider *uploadLimitSlider; + QSlider *downloadLimitSlider; + QLabel *uploadLimitLabel; + QLabel *downloadLimitLabel; + + int uploadLimit; + int downloadLimit; + + struct Job { + TorrentClient *client; + QString torrentFileName; + QString destinationDirectory; + }; + QList<Job> jobs; + int jobsStopped; + int jobsToStop; + + QString lastDirectory; + QProgressDialog *quitDialog; + + bool saveChanges; +}; + +#endif diff --git a/examples/network/torrent/metainfo.cpp b/examples/network/torrent/metainfo.cpp new file mode 100644 index 0000000..03d824c --- /dev/null +++ b/examples/network/torrent/metainfo.cpp @@ -0,0 +1,218 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "bencodeparser.h" +#include "metainfo.h" + +#include <QDateTime> +#include <QMetaType> +#include <QString> + +MetaInfo::MetaInfo() +{ + clear(); +} + +void MetaInfo::clear() +{ + errString = "Unknown error"; + content.clear(); + infoData.clear(); + metaInfoMultiFiles.clear(); + metaInfoAnnounce.clear(); + metaInfoAnnounceList.clear(); + metaInfoCreationDate = QDateTime(); + metaInfoComment.clear(); + metaInfoCreatedBy.clear(); + metaInfoName.clear(); + metaInfoPieceLength = 0; + metaInfoSha1Sums.clear(); +} + +bool MetaInfo::parse(const QByteArray &data) +{ + clear(); + content = data; + + BencodeParser parser; + if (!parser.parse(content)) { + errString = parser.errorString(); + return false; + } + + infoData = parser.infoSection(); + + QMap<QByteArray, QVariant> dict = parser.dictionary(); + if (!dict.contains("info")) + return false; + + QMap<QByteArray, QVariant> info = qVariantValue<Dictionary>(dict.value("info")); + + if (info.contains("files")) { + metaInfoFileForm = MultiFileForm; + + QList<QVariant> files = info.value("files").toList(); + + for (int i = 0; i < files.size(); ++i) { + QMap<QByteArray, QVariant> file = qVariantValue<Dictionary>(files.at(i)); + QList<QVariant> pathElements = file.value("path").toList(); + QByteArray path; + foreach (QVariant p, pathElements) { + if (!path.isEmpty()) + path += "/"; + path += p.toByteArray(); + } + + MetaInfoMultiFile multiFile; + multiFile.length = file.value("length").toLongLong(); + multiFile.path = QString::fromUtf8(path); + multiFile.md5sum = file.value("md5sum").toByteArray(); + metaInfoMultiFiles << multiFile; + } + + metaInfoName = QString::fromUtf8(info.value("name").toByteArray()); + metaInfoPieceLength = info.value("piece length").toInt(); + QByteArray pieces = info.value("pieces").toByteArray(); + for (int i = 0; i < pieces.size(); i += 20) + metaInfoSha1Sums << pieces.mid(i, 20); + } else if (info.contains("length")) { + metaInfoFileForm = SingleFileForm; + metaInfoSingleFile.length = info.value("length").toLongLong(); + metaInfoSingleFile.md5sum = info.value("md5sum").toByteArray(); + metaInfoSingleFile.name = QString::fromUtf8(info.value("name").toByteArray()); + metaInfoSingleFile.pieceLength = info.value("piece length").toInt(); + + QByteArray pieces = info.value("pieces").toByteArray(); + for (int i = 0; i < pieces.size(); i += 20) + metaInfoSingleFile.sha1Sums << pieces.mid(i, 20); + } + + metaInfoAnnounce = QString::fromUtf8(dict.value("announce").toByteArray()); + + if (dict.contains("announce-list")) { + // ### unimplemented + } + + if (dict.contains("creation date")) + metaInfoCreationDate.setTime_t(dict.value("creation date").toInt()); + if (dict.contains("comment")) + metaInfoComment = QString::fromUtf8(dict.value("comment").toByteArray()); + if (dict.contains("created by")) + metaInfoCreatedBy = QString::fromUtf8(dict.value("created by").toByteArray()); + + return true; +} + +QByteArray MetaInfo::infoValue() const +{ + return infoData; +} + +QString MetaInfo::errorString() const +{ + return errString; +} + +MetaInfo::FileForm MetaInfo::fileForm() const +{ + return metaInfoFileForm; +} + +QString MetaInfo::announceUrl() const +{ + return metaInfoAnnounce; +} + +QStringList MetaInfo::announceList() const +{ + return metaInfoAnnounceList; +} + +QDateTime MetaInfo::creationDate() const +{ + return metaInfoCreationDate; +} + +QString MetaInfo::comment() const +{ + return metaInfoComment; +} + +QString MetaInfo::createdBy() const +{ + return metaInfoCreatedBy; +} + +MetaInfoSingleFile MetaInfo::singleFile() const +{ + return metaInfoSingleFile; +} + +QList<MetaInfoMultiFile> MetaInfo::multiFiles() const +{ + return metaInfoMultiFiles; +} + +QString MetaInfo::name() const +{ + return metaInfoName; +} + +int MetaInfo::pieceLength() const +{ + return metaInfoPieceLength; +} + +QList<QByteArray> MetaInfo::sha1Sums() const +{ + return metaInfoSha1Sums; +} + +qint64 MetaInfo::totalSize() const +{ + if (fileForm() == SingleFileForm) + return singleFile().length; + + qint64 size = 0; + foreach (MetaInfoMultiFile file, multiFiles()) + size += file.length; + return size; +} diff --git a/examples/network/torrent/metainfo.h b/examples/network/torrent/metainfo.h new file mode 100644 index 0000000..de60b99 --- /dev/null +++ b/examples/network/torrent/metainfo.h @@ -0,0 +1,122 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef METAINFO_H +#define METAINFO_H + +#include <QByteArray> +#include <QDateTime> +#include <QList> +#include <QMap> +#include <QString> +#include <QStringList> +#include <QVariant> + +struct MetaInfoSingleFile +{ + qint64 length; + QByteArray md5sum; + QString name; + int pieceLength; + QList<QByteArray> sha1Sums; +}; + +struct MetaInfoMultiFile +{ + qint64 length; + QByteArray md5sum; + QString path; +}; + +class MetaInfo +{ +public: + enum FileForm { + SingleFileForm, + MultiFileForm + }; + + MetaInfo(); + void clear(); + + bool parse(const QByteArray &data); + QString errorString() const; + + QByteArray infoValue() const; + + FileForm fileForm() const; + QString announceUrl() const; + QStringList announceList() const; + QDateTime creationDate() const; + QString comment() const; + QString createdBy() const; + + // For single file form + MetaInfoSingleFile singleFile() const; + + // For multifile form + QList<MetaInfoMultiFile> multiFiles() const; + QString name() const; + int pieceLength() const; + QList<QByteArray> sha1Sums() const; + + // Total size + qint64 totalSize() const; + +private: + QString errString; + QByteArray content; + QByteArray infoData; + + FileForm metaInfoFileForm; + MetaInfoSingleFile metaInfoSingleFile; + QList<MetaInfoMultiFile> metaInfoMultiFiles; + QString metaInfoAnnounce; + QStringList metaInfoAnnounceList; + QDateTime metaInfoCreationDate; + QString metaInfoComment; + QString metaInfoCreatedBy; + QString metaInfoName; + int metaInfoPieceLength; + QList<QByteArray> metaInfoSha1Sums; +}; + +#endif diff --git a/examples/network/torrent/peerwireclient.cpp b/examples/network/torrent/peerwireclient.cpp new file mode 100644 index 0000000..e79184b --- /dev/null +++ b/examples/network/torrent/peerwireclient.cpp @@ -0,0 +1,665 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "peerwireclient.h" + +#include <QHostAddress> +#include <QTimerEvent> + +static const int PendingRequestTimeout = 60 * 1000; +static const int ClientTimeout = 120 * 1000; +static const int ConnectTimeout = 60 * 1000; +static const int KeepAliveInterval = 30 * 1000; +static const int RateControlTimerDelay = 2000; +static const int MinimalHeaderSize = 48; +static const int FullHeaderSize = 68; +static const char ProtocolId[] = "BitTorrent protocol"; +static const char ProtocolIdSize = 19; + +// Reads a 32bit unsigned int from data in network order. +static inline quint32 fromNetworkData(const char *data) +{ + const unsigned char *udata = (const unsigned char *)data; + return (quint32(udata[0]) << 24) + | (quint32(udata[1]) << 16) + | (quint32(udata[2]) << 8) + | (quint32(udata[3])); +} + +// Writes a 32bit unsigned int from num to data in network order. +static inline void toNetworkData(quint32 num, char *data) +{ + unsigned char *udata = (unsigned char *)data; + udata[3] = (num & 0xff); + udata[2] = (num & 0xff00) >> 8; + udata[1] = (num & 0xff0000) >> 16; + udata[0] = (num & 0xff000000) >> 24; +} + +// Constructs an unconnected PeerWire client and starts the connect timer. +PeerWireClient::PeerWireClient(const QByteArray &peerId, QObject *parent) + : QTcpSocket(parent), pendingBlockSizes(0), + pwState(ChokingPeer | ChokedByPeer), receivedHandShake(false), gotPeerId(false), + sentHandShake(false), nextPacketLength(-1), pendingRequestTimer(0), invalidateTimeout(false), + keepAliveTimer(0), torrentPeer(0) +{ + memset(uploadSpeedData, 0, sizeof(uploadSpeedData)); + memset(downloadSpeedData, 0, sizeof(downloadSpeedData)); + + transferSpeedTimer = startTimer(RateControlTimerDelay); + timeoutTimer = startTimer(ConnectTimeout); + peerIdString = peerId; + + connect(this, SIGNAL(readyRead()), this, SIGNAL(readyToTransfer())); + connect(this, SIGNAL(connected()), this, SIGNAL(readyToTransfer())); + + connect(&socket, SIGNAL(connected()), + this, SIGNAL(connected())); + connect(&socket, SIGNAL(readyRead()), + this, SIGNAL(readyRead())); + connect(&socket, SIGNAL(disconnected()), + this, SIGNAL(disconnected())); + connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)), + this, SIGNAL(error(QAbstractSocket::SocketError))); + connect(&socket, SIGNAL(bytesWritten(qint64)), + this, SIGNAL(bytesWritten(qint64))); + connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), + this, SLOT(socketStateChanged(QAbstractSocket::SocketState))); + +} + +// Registers the peer ID and SHA1 sum of the torrent, and initiates +// the handshake. +void PeerWireClient::initialize(const QByteArray &infoHash, int pieceCount) +{ + this->infoHash = infoHash; + peerPieces.resize(pieceCount); + if (!sentHandShake) + sendHandShake(); +} + +void PeerWireClient::setPeer(TorrentPeer *peer) +{ + torrentPeer = peer; +} + +TorrentPeer *PeerWireClient::peer() const +{ + return torrentPeer; +} + +QBitArray PeerWireClient::availablePieces() const +{ + return peerPieces; +} + +QList<TorrentBlock> PeerWireClient::incomingBlocks() const +{ + return incoming; +} + +// Sends a "choke" message, asking the peer to stop requesting blocks. +void PeerWireClient::chokePeer() +{ + const char message[] = {0, 0, 0, 1, 0}; + write(message, sizeof(message)); + pwState |= ChokingPeer; + + // After receiving a choke message, the peer will assume all + // pending requests are lost. + pendingBlocks.clear(); + pendingBlockSizes = 0; +} + +// Sends an "unchoke" message, allowing the peer to start/resume +// requesting blocks. +void PeerWireClient::unchokePeer() +{ + const char message[] = {0, 0, 0, 1, 1}; + write(message, sizeof(message)); + pwState &= ~ChokingPeer; + + if (pendingRequestTimer) + killTimer(pendingRequestTimer); +} + +// Sends a "keep-alive" message to prevent the peer from closing +// the connection when there's no activity +void PeerWireClient::sendKeepAlive() +{ + const char message[] = {0, 0, 0, 0}; + write(message, sizeof(message)); +} + +// Sends an "interested" message, informing the peer that it has got +// pieces that we'd like to download. +void PeerWireClient::sendInterested() +{ + const char message[] = {0, 0, 0, 1, 2}; + write(message, sizeof(message)); + pwState |= InterestedInPeer; + + // After telling the peer that we're interested, we expect to get + // unchoked within a certain timeframe; otherwise we'll drop the + // connection. + if (pendingRequestTimer) + killTimer(pendingRequestTimer); + pendingRequestTimer = startTimer(PendingRequestTimeout); +} + +// Sends a "not interested" message, informing the peer that it does +// not have any pieces that we'd like to download. +void PeerWireClient::sendNotInterested() +{ + const char message[] = {0, 0, 0, 1, 3}; + write(message, sizeof(message)); + pwState &= ~InterestedInPeer; +} + +// Sends a piece notification / a "have" message, informing the peer +// that we have just downloaded a new piece. +void PeerWireClient::sendPieceNotification(int piece) +{ + if (!sentHandShake) + sendHandShake(); + + char message[] = {0, 0, 0, 5, 4, 0, 0, 0, 0}; + toNetworkData(piece, &message[5]); + write(message, sizeof(message)); +} + +// Sends the complete list of pieces that we have downloaded. +void PeerWireClient::sendPieceList(const QBitArray &bitField) +{ + // The bitfield message may only be sent immediately after the + // handshaking sequence is completed, and before any other + // messages are sent. + if (!sentHandShake) + sendHandShake(); + + // Don't send the bitfield if it's all zeros. + if (bitField.count(true) == 0) + return; + + int bitFieldSize = bitField.size(); + int size = (bitFieldSize + 7) / 8; + QByteArray bits(size, '\0'); + for (int i = 0; i < bitFieldSize; ++i) { + if (bitField.testBit(i)) { + quint32 byte = quint32(i) / 8; + quint32 bit = quint32(i) % 8; + bits[byte] = uchar(bits.at(byte)) | (1 << (7 - bit)); + } + } + + char message[] = {0, 0, 0, 1, 5}; + toNetworkData(bits.size() + 1, &message[0]); + write(message, sizeof(message)); + write(bits); +} + +// Sends a request for a block. +void PeerWireClient::requestBlock(int piece, int offset, int length) +{ + char message[] = {0, 0, 0, 1, 6}; + toNetworkData(13, &message[0]); + write(message, sizeof(message)); + + char numbers[4 * 3]; + toNetworkData(piece, &numbers[0]); + toNetworkData(offset, &numbers[4]); + toNetworkData(length, &numbers[8]); + write(numbers, sizeof(numbers)); + + incoming << TorrentBlock(piece, offset, length); + + // After requesting a block, we expect the block to be sent by the + // other peer within a certain number of seconds. Otherwise, we + // drop the connection. + if (pendingRequestTimer) + killTimer(pendingRequestTimer); + pendingRequestTimer = startTimer(PendingRequestTimeout); +} + +// Cancels a request for a block. +void PeerWireClient::cancelRequest(int piece, int offset, int length) +{ + char message[] = {0, 0, 0, 1, 8}; + toNetworkData(13, &message[0]); + write(message, sizeof(message)); + + char numbers[4 * 3]; + toNetworkData(piece, &numbers[0]); + toNetworkData(offset, &numbers[4]); + toNetworkData(length, &numbers[8]); + write(numbers, sizeof(numbers)); + + incoming.removeAll(TorrentBlock(piece, offset, length)); +} + +// Sends a block to the peer. +void PeerWireClient::sendBlock(int piece, int offset, const QByteArray &data) +{ + QByteArray block; + + char message[] = {0, 0, 0, 1, 7}; + toNetworkData(9 + data.size(), &message[0]); + block += QByteArray(message, sizeof(message)); + + char numbers[4 * 2]; + toNetworkData(piece, &numbers[0]); + toNetworkData(offset, &numbers[4]); + block += QByteArray(numbers, sizeof(numbers)); + block += data; + + BlockInfo blockInfo; + blockInfo.pieceIndex = piece; + blockInfo.offset = offset; + blockInfo.length = data.size(); + blockInfo.block = block; + + pendingBlocks << blockInfo; + pendingBlockSizes += block.size(); + + if (pendingBlockSizes > 32 * 16384) { + chokePeer(); + unchokePeer(); + return; + } + emit readyToTransfer(); +} + +// Attempts to write 'bytes' bytes to the socket from the buffer. +// This is used by RateController, which precisely controls how much +// each client can write. +qint64 PeerWireClient::writeToSocket(qint64 bytes) +{ + qint64 totalWritten = 0; + do { + if (outgoingBuffer.isEmpty() && !pendingBlocks.isEmpty()) { + BlockInfo block = pendingBlocks.takeFirst(); + pendingBlockSizes -= block.length; + outgoingBuffer += block.block; + } + qint64 written = socket.write(outgoingBuffer.constData(), + qMin<qint64>(bytes - totalWritten, outgoingBuffer.size())); + if (written <= 0) + return totalWritten ? totalWritten : written; + + totalWritten += written; + uploadSpeedData[0] += written; + outgoingBuffer.remove(0, written); + } while (totalWritten < bytes && (!outgoingBuffer.isEmpty() || !pendingBlocks.isEmpty())); + + return totalWritten; +} + +// Attempts to read at most 'bytes' bytes from the socket. +qint64 PeerWireClient::readFromSocket(qint64 bytes) +{ + char buffer[1024]; + qint64 totalRead = 0; + do { + qint64 bytesRead = socket.read(buffer, qMin<qint64>(sizeof(buffer), bytes - totalRead)); + if (bytesRead <= 0) + break; + qint64 oldSize = incomingBuffer.size(); + incomingBuffer.resize(oldSize + bytesRead); + memcpy(incomingBuffer.data() + oldSize, buffer, bytesRead); + + totalRead += bytesRead; + } while (totalRead < bytes); + + if (totalRead > 0) { + downloadSpeedData[0] += totalRead; + emit bytesReceived(totalRead); + processIncomingData(); + } + return totalRead; +} + +// Returns the average number of bytes per second this client is +// downloading. +qint64 PeerWireClient::downloadSpeed() const +{ + qint64 sum = 0; + for (unsigned int i = 0; i < sizeof(downloadSpeedData) / sizeof(qint64); ++i) + sum += downloadSpeedData[i]; + return sum / (8 * 2); +} + +// Returns the average number of bytes per second this client is +// uploading. +qint64 PeerWireClient::uploadSpeed() const +{ + qint64 sum = 0; + for (unsigned int i = 0; i < sizeof(uploadSpeedData) / sizeof(qint64); ++i) + sum += uploadSpeedData[i]; + return sum / (8 * 2); +} + +void PeerWireClient::setReadBufferSize(int size) +{ + socket.setReadBufferSize(size); +} + +bool PeerWireClient::canTransferMore() const +{ + return bytesAvailable() > 0 || socket.bytesAvailable() > 0 + || !outgoingBuffer.isEmpty() || !pendingBlocks.isEmpty(); +} + +void PeerWireClient::connectToHostImplementation(const QString &hostName, + quint16 port, OpenMode openMode) + +{ + setOpenMode(openMode); + socket.connectToHost(hostName, port, openMode); +} + +void PeerWireClient::diconnectFromHostImplementation() +{ + socket.disconnectFromHost(); +} + +void PeerWireClient::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == transferSpeedTimer) { + // Rotate the upload / download records. + for (int i = 6; i >= 0; --i) { + uploadSpeedData[i + 1] = uploadSpeedData[i]; + downloadSpeedData[i + 1] = downloadSpeedData[i]; + } + uploadSpeedData[0] = 0; + downloadSpeedData[0] = 0; + } else if (event->timerId() == timeoutTimer) { + // Disconnect if we timed out; otherwise the timeout is + // restarted. + if (invalidateTimeout) { + invalidateTimeout = false; + } else { + abort(); + emit infoHashReceived(QByteArray()); + } + } else if (event->timerId() == pendingRequestTimer) { + abort(); + } else if (event->timerId() == keepAliveTimer) { + sendKeepAlive(); + } + QTcpSocket::timerEvent(event); +} + +// Sends the handshake to the peer. +void PeerWireClient::sendHandShake() +{ + sentHandShake = true; + + // Restart the timeout + if (timeoutTimer) + killTimer(timeoutTimer); + timeoutTimer = startTimer(ClientTimeout); + + // Write the 68 byte PeerWire handshake. + write(&ProtocolIdSize, 1); + write(ProtocolId, ProtocolIdSize); + write(QByteArray(8, '\0')); + write(infoHash); + write(peerIdString); +} + +void PeerWireClient::processIncomingData() +{ + invalidateTimeout = true; + if (!receivedHandShake) { + // Check that we received enough data + if (bytesAvailable() < MinimalHeaderSize) + return; + + // Sanity check the protocol ID + QByteArray id = read(ProtocolIdSize + 1); + if (id.at(0) != ProtocolIdSize || !id.mid(1).startsWith(ProtocolId)) { + abort(); + return; + } + + // Discard 8 reserved bytes, then read the info hash and peer ID + (void) read(8); + + // Read infoHash + QByteArray peerInfoHash = read(20); + if (!infoHash.isEmpty() && peerInfoHash != infoHash) { + abort(); + return; + } + + emit infoHashReceived(peerInfoHash); + if (infoHash.isEmpty()) { + abort(); + return; + } + + // Send handshake + if (!sentHandShake) + sendHandShake(); + receivedHandShake = true; + } + + // Handle delayed peer id arrival + if (!gotPeerId) { + if (bytesAvailable() < 20) + return; + gotPeerId = true; + if (read(20) == peerIdString) { + // We connected to ourself + abort(); + return; + } + } + + // Initialize keep-alive timer + if (!keepAliveTimer) + keepAliveTimer = startTimer(KeepAliveInterval); + + do { + // Find the packet length + if (nextPacketLength == -1) { + if (bytesAvailable() < 4) + return; + + char tmp[4]; + read(tmp, sizeof(tmp)); + nextPacketLength = fromNetworkData(tmp); + + if (nextPacketLength < 0 || nextPacketLength > 200000) { + // Prevent DoS + abort(); + return; + } + } + + // KeepAlive + if (nextPacketLength == 0) { + nextPacketLength = -1; + continue; + } + + // Wait with parsing until the whole packet has been received + if (bytesAvailable() < nextPacketLength) + return; + + // Read the packet + QByteArray packet = read(nextPacketLength); + if (packet.size() != nextPacketLength) { + abort(); + return; + } + + switch (packet.at(0)) { + case ChokePacket: + // We have been choked. + pwState |= ChokedByPeer; + incoming.clear(); + if (pendingRequestTimer) + killTimer(pendingRequestTimer); + emit choked(); + break; + case UnchokePacket: + // We have been unchoked. + pwState &= ~ChokedByPeer; + emit unchoked(); + break; + case InterestedPacket: + // The peer is interested in downloading. + pwState |= PeerIsInterested; + emit interested(); + break; + case NotInterestedPacket: + // The peer is not interested in downloading. + pwState &= ~PeerIsInterested; + emit notInterested(); + break; + case HavePacket: { + // The peer has a new piece available. + quint32 index = fromNetworkData(&packet.data()[1]); + if (index < quint32(peerPieces.size())) { + // Only accept indexes within the valid range. + peerPieces.setBit(int(index)); + } + emit piecesAvailable(availablePieces()); + break; + } + case BitFieldPacket: + // The peer has the following pieces available. + for (int i = 1; i < packet.size(); ++i) { + for (int bit = 0; bit < 8; ++bit) { + if (packet.at(i) & (1 << (7 - bit))) { + int bitIndex = int(((i - 1) * 8) + bit); + if (bitIndex >= 0 && bitIndex < peerPieces.size()) { + // Occasionally, broken clients claim to have + // pieces whose index is outside the valid range. + // The most common mistake is the index == size + // case. + peerPieces.setBit(bitIndex); + } + } + } + } + emit piecesAvailable(availablePieces()); + break; + case RequestPacket: { + // The peer requests a block. + quint32 index = fromNetworkData(&packet.data()[1]); + quint32 begin = fromNetworkData(&packet.data()[5]); + quint32 length = fromNetworkData(&packet.data()[9]); + emit blockRequested(int(index), int(begin), int(length)); + break; + } + case PiecePacket: { + int index = int(fromNetworkData(&packet.data()[1])); + int begin = int(fromNetworkData(&packet.data()[5])); + + incoming.removeAll(TorrentBlock(index, begin, packet.size() - 9)); + + // The peer sends a block. + emit blockReceived(index, begin, packet.mid(9)); + + // Kill the pending block timer. + if (pendingRequestTimer) { + killTimer(pendingRequestTimer); + pendingRequestTimer = 0; + } + break; + } + case CancelPacket: { + // The peer cancels a block request. + quint32 index = fromNetworkData(&packet.data()[1]); + quint32 begin = fromNetworkData(&packet.data()[5]); + quint32 length = fromNetworkData(&packet.data()[9]); + for (int i = 0; i < pendingBlocks.size(); ++i) { + const BlockInfo &blockInfo = pendingBlocks.at(i); + if (blockInfo.pieceIndex == int(index) + && blockInfo.offset == int(begin) + && blockInfo.length == int(length)) { + pendingBlocks.removeAt(i); + break; + } + } + break; + } + default: + // Unsupported packet type; just ignore it. + break; + } + nextPacketLength = -1; + } while (bytesAvailable() > 0); +} + +void PeerWireClient::socketStateChanged(QAbstractSocket::SocketState state) +{ + setLocalAddress(socket.localAddress()); + setLocalPort(socket.localPort()); + setPeerName(socket.peerName()); + setPeerAddress(socket.peerAddress()); + setPeerPort(socket.peerPort()); + setSocketState(state); +} + +qint64 PeerWireClient::readData(char *data, qint64 size) +{ + int n = qMin<int>(size, incomingBuffer.size()); + memcpy(data, incomingBuffer.constData(), n); + incomingBuffer.remove(0, n); + return n; +} + +qint64 PeerWireClient::readLineData(char *data, qint64 maxlen) +{ + return QIODevice::readLineData(data, maxlen); +} + +qint64 PeerWireClient::writeData(const char *data, qint64 size) +{ + int oldSize = outgoingBuffer.size(); + outgoingBuffer.resize(oldSize + size); + memcpy(outgoingBuffer.data() + oldSize, data, size); + emit readyToTransfer(); + return size; +} diff --git a/examples/network/torrent/peerwireclient.h b/examples/network/torrent/peerwireclient.h new file mode 100644 index 0000000..a21faa2 --- /dev/null +++ b/examples/network/torrent/peerwireclient.h @@ -0,0 +1,210 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef PEERWIRECLIENT_H +#define PEERWIRECLIENT_H + +#include <QBitArray> +#include <QList> +#include <QTcpSocket> + +QT_BEGIN_NAMESPACE +class QHostAddress; +class QTimerEvent; +QT_END_NAMESPACE +class TorrentPeer; + +struct TorrentBlock +{ + inline TorrentBlock(int p, int o, int l) + : pieceIndex(p), offset(o), length(l) + { + } + inline bool operator==(const TorrentBlock &other) const + { + return pieceIndex == other.pieceIndex + && offset == other.offset + && length == other.length; + } + + int pieceIndex; + int offset; + int length; +}; + +class PeerWireClient : public QTcpSocket +{ + Q_OBJECT + +public: + enum PeerWireStateFlag { + ChokingPeer = 0x1, + InterestedInPeer = 0x2, + ChokedByPeer = 0x4, + PeerIsInterested = 0x8 + }; + Q_DECLARE_FLAGS(PeerWireState, PeerWireStateFlag) + + PeerWireClient(const QByteArray &peerId, QObject *parent = 0); + void initialize(const QByteArray &infoHash, int pieceCount); + + void setPeer(TorrentPeer *peer); + TorrentPeer *peer() const; + + // State + inline PeerWireState peerWireState() const { return pwState; } + QBitArray availablePieces() const; + QList<TorrentBlock> incomingBlocks() const; + + // Protocol + void chokePeer(); + void unchokePeer(); + void sendInterested(); + void sendKeepAlive(); + void sendNotInterested(); + void sendPieceNotification(int piece); + void sendPieceList(const QBitArray &bitField); + void requestBlock(int piece, int offset, int length); + void cancelRequest(int piece, int offset, int length); + void sendBlock(int piece, int offset, const QByteArray &data); + + // Rate control + qint64 writeToSocket(qint64 bytes); + qint64 readFromSocket(qint64 bytes); + qint64 downloadSpeed() const; + qint64 uploadSpeed() const; + + bool canTransferMore() const; + qint64 bytesAvailable() const { return incomingBuffer.size() + QTcpSocket::bytesAvailable(); } + qint64 socketBytesAvailable() const { return socket.bytesAvailable(); } + qint64 socketBytesToWrite() const { return socket.bytesToWrite(); } + + void setReadBufferSize(int size); + +signals: + void infoHashReceived(const QByteArray &infoHash); + void readyToTransfer(); + + void choked(); + void unchoked(); + void interested(); + void notInterested(); + + void piecesAvailable(const QBitArray &pieces); + void blockRequested(int pieceIndex, int begin, int length); + void blockReceived(int pieceIndex, int begin, const QByteArray &data); + + void bytesReceived(qint64 size); + +protected slots: + void connectToHostImplementation(const QString &hostName, + quint16 port, OpenMode openMode = ReadWrite); + void diconnectFromHostImplementation(); + +protected: + void timerEvent(QTimerEvent *event); + + qint64 readData(char *data, qint64 maxlen); + qint64 readLineData(char *data, qint64 maxlen); + qint64 writeData(const char *data, qint64 len); + +private slots: + void sendHandShake(); + void processIncomingData(); + void socketStateChanged(QAbstractSocket::SocketState state); + +private: + // Data waiting to be read/written + QByteArray incomingBuffer; + QByteArray outgoingBuffer; + + struct BlockInfo { + int pieceIndex; + int offset; + int length; + QByteArray block; + }; + QList<BlockInfo> pendingBlocks; + int pendingBlockSizes; + QList<TorrentBlock> incoming; + + enum PacketType { + ChokePacket = 0, + UnchokePacket = 1, + InterestedPacket = 2, + NotInterestedPacket = 3, + HavePacket = 4, + BitFieldPacket = 5, + RequestPacket = 6, + PiecePacket = 7, + CancelPacket = 8 + }; + + // State + PeerWireState pwState; + bool receivedHandShake; + bool gotPeerId; + bool sentHandShake; + int nextPacketLength; + + // Upload/download speed records + qint64 uploadSpeedData[8]; + qint64 downloadSpeedData[8]; + int transferSpeedTimer; + + // Timeout handling + int timeoutTimer; + int pendingRequestTimer; + bool invalidateTimeout; + int keepAliveTimer; + + // Checksum, peer ID and set of available pieces + QByteArray infoHash; + QByteArray peerIdString; + QBitArray peerPieces; + TorrentPeer *torrentPeer; + + QTcpSocket socket; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(PeerWireClient::PeerWireState) + +#endif diff --git a/examples/network/torrent/ratecontroller.cpp b/examples/network/torrent/ratecontroller.cpp new file mode 100644 index 0000000..243d897 --- /dev/null +++ b/examples/network/torrent/ratecontroller.cpp @@ -0,0 +1,156 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "peerwireclient.h" +#include "ratecontroller.h" + +#include <QtCore> + +Q_GLOBAL_STATIC(RateController, rateController) + +RateController *RateController::instance() +{ + return rateController(); +} + +void RateController::addSocket(PeerWireClient *socket) +{ + connect(socket, SIGNAL(readyToTransfer()), this, SLOT(scheduleTransfer())); + socket->setReadBufferSize(downLimit * 4); + sockets << socket; + scheduleTransfer(); +} + +void RateController::removeSocket(PeerWireClient *socket) +{ + disconnect(socket, SIGNAL(readyToTransfer()), this, SLOT(scheduleTransfer())); + socket->setReadBufferSize(0); + sockets.remove(socket); +} + +void RateController::setDownloadLimit(int bytesPerSecond) +{ + downLimit = bytesPerSecond; + foreach (PeerWireClient *socket, sockets) + socket->setReadBufferSize(downLimit * 4); +} + +void RateController::scheduleTransfer() +{ + if (transferScheduled) + return; + transferScheduled = true; + QTimer::singleShot(50, this, SLOT(transfer())); +} + +void RateController::transfer() +{ + transferScheduled = false; + if (sockets.isEmpty()) + return; + + int msecs = 1000; + if (!stopWatch.isNull()) + msecs = qMin(msecs, stopWatch.elapsed()); + + qint64 bytesToWrite = (upLimit * msecs) / 1000; + qint64 bytesToRead = (downLimit * msecs) / 1000; + if (bytesToWrite == 0 && bytesToRead == 0) { + scheduleTransfer(); + return; + } + + QSet<PeerWireClient *> pendingSockets; + foreach (PeerWireClient *client, sockets) { + if (client->canTransferMore()) + pendingSockets << client; + } + if (pendingSockets.isEmpty()) + return; + + stopWatch.start(); + + bool canTransferMore; + do { + canTransferMore = false; + qint64 writeChunk = qMax<qint64>(1, bytesToWrite / pendingSockets.size()); + qint64 readChunk = qMax<qint64>(1, bytesToRead / pendingSockets.size()); + + QSetIterator<PeerWireClient *> it(pendingSockets); + while (it.hasNext() && (bytesToWrite > 0 || bytesToRead > 0)) { + PeerWireClient *socket = it.next(); + if (socket->state() != QAbstractSocket::ConnectedState) { + pendingSockets.remove(socket); + continue; + } + + bool dataTransferred = false; + qint64 available = qMin<qint64>(socket->socketBytesAvailable(), readChunk); + if (available > 0) { + qint64 readBytes = socket->readFromSocket(qMin<qint64>(available, bytesToRead)); + if (readBytes > 0) { + bytesToRead -= readBytes; + dataTransferred = true; + } + } + + if (upLimit * 2 > socket->bytesToWrite()) { + qint64 chunkSize = qMin<qint64>(writeChunk, bytesToWrite); + qint64 toWrite = qMin(upLimit * 2 - socket->bytesToWrite(), chunkSize); + if (toWrite > 0) { + qint64 writtenBytes = socket->writeToSocket(toWrite); + if (writtenBytes > 0) { + bytesToWrite -= writtenBytes; + dataTransferred = true; + } + } + } + + if (dataTransferred && socket->canTransferMore()) + canTransferMore = true; + else + pendingSockets.remove(socket); + } + } while (canTransferMore && (bytesToWrite > 0 || bytesToRead > 0) && !pendingSockets.isEmpty()); + + if (canTransferMore || bytesToWrite == 0 || bytesToRead == 0) + scheduleTransfer(); +} diff --git a/examples/network/torrent/ratecontroller.h b/examples/network/torrent/ratecontroller.h new file mode 100644 index 0000000..fb22159 --- /dev/null +++ b/examples/network/torrent/ratecontroller.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef RATECONTROLLER_H +#define RATECONTROLLER_H + +#include <QObject> +#include <QSet> +#include <QTime> + +class PeerWireClient; + +class RateController : public QObject +{ + Q_OBJECT + +public: + inline RateController(QObject *parent = 0) + : QObject(parent), transferScheduled(false) { } + static RateController *instance(); + + void addSocket(PeerWireClient *socket); + void removeSocket(PeerWireClient *socket); + + inline int uploadLimit() const { return upLimit; } + inline int downloadLimit() const { return downLimit; } + inline void setUploadLimit(int bytesPerSecond) { upLimit = bytesPerSecond; } + void setDownloadLimit(int bytesPerSecond); + +public slots: + void transfer(); + void scheduleTransfer(); + +private: + QTime stopWatch; + QSet<PeerWireClient *> sockets; + int upLimit; + int downLimit; + bool transferScheduled; +}; + +#endif diff --git a/examples/network/torrent/torrent.pro b/examples/network/torrent/torrent.pro new file mode 100644 index 0000000..10b2672 --- /dev/null +++ b/examples/network/torrent/torrent.pro @@ -0,0 +1,37 @@ +HEADERS += addtorrentdialog.h \ + bencodeparser.h \ + connectionmanager.h \ + mainwindow.h \ + metainfo.h \ + peerwireclient.h \ + ratecontroller.h \ + filemanager.h \ + torrentclient.h \ + torrentserver.h \ + trackerclient.h + +SOURCES += main.cpp \ + addtorrentdialog.cpp \ + bencodeparser.cpp \ + connectionmanager.cpp \ + mainwindow.cpp \ + metainfo.cpp \ + peerwireclient.cpp \ + ratecontroller.cpp \ + filemanager.cpp \ + torrentclient.cpp \ + torrentserver.cpp \ + trackerclient.cpp + +# Forms and resources +FORMS += forms/addtorrentform.ui +RESOURCES += icons.qrc + +QT += network + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/network/torrent +sources.files = $$SOURCES $$HEADERS $$RESOURCES torrent.pro *.torrent +sources.files += icons forms 3rdparty +sources.path = $$[QT_INSTALL_EXAMPLES]/network/torrent +INSTALLS += target sources diff --git a/examples/network/torrent/torrentclient.cpp b/examples/network/torrent/torrentclient.cpp new file mode 100644 index 0000000..e8473bb --- /dev/null +++ b/examples/network/torrent/torrentclient.cpp @@ -0,0 +1,1529 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "connectionmanager.h" +#include "filemanager.h" +#include "metainfo.h" +#include "torrentclient.h" +#include "torrentserver.h" +#include "trackerclient.h" +#include "peerwireclient.h" +#include "ratecontroller.h" + +#include <QtCore> +#include <QNetworkInterface> + +// These constants could also be configurable by the user. +static const int ServerMinPort = 6881; +static const int ServerMaxPort = /* 6889 */ 7000; +static const int BlockSize = 16384; +static const int MaxBlocksInProgress = 5; +static const int MaxBlocksInMultiMode = 2; +static const int MaxConnectionPerPeer = 1; +static const int RateControlWindowLength = 10; +static const int RateControlTimerDelay = 1000; +static const int MinimumTimeBeforeRevisit = 30; +static const int MaxUploads = 4; +static const int UploadScheduleInterval = 10000; +static const int EndGamePieces = 5; + +class TorrentPiece { +public: + int index; + int length; + QBitArray completedBlocks; + QBitArray requestedBlocks; + bool inProgress; +}; + +class TorrentClientPrivate +{ +public: + TorrentClientPrivate(TorrentClient *qq); + + // State / error + void setError(TorrentClient::Error error); + void setState(TorrentClient::State state); + TorrentClient::Error error; + TorrentClient::State state; + QString errorString; + QString stateString; + + // Where to save data + QString destinationFolder; + MetaInfo metaInfo; + + // Announce tracker and file manager + QByteArray peerId; + QByteArray infoHash; + TrackerClient trackerClient; + FileManager fileManager; + + // Connections + QList<PeerWireClient *> connections; + QList<TorrentPeer *> peers; + bool schedulerCalled; + void callScheduler(); + bool connectingToClients; + void callPeerConnector(); + int uploadScheduleTimer; + + // Pieces + QMap<int, PeerWireClient *> readIds; + QMultiMap<PeerWireClient *, TorrentPiece *> payloads; + QMap<int, TorrentPiece *> pendingPieces; + QBitArray completedPieces; + QBitArray incompletePieces; + int pieceCount; + + // Progress + int lastProgressValue; + qint64 downloadedBytes; + qint64 uploadedBytes; + int downloadRate[RateControlWindowLength]; + int uploadRate[RateControlWindowLength]; + int transferRateTimer; + + TorrentClient *q; +}; + +TorrentClientPrivate::TorrentClientPrivate(TorrentClient *qq) + : trackerClient(qq), q(qq) +{ + error = TorrentClient::UnknownError; + state = TorrentClient::Idle; + errorString = QT_TRANSLATE_NOOP(TorrentClient, "Unknown error"); + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Idle"); + schedulerCalled = false; + connectingToClients = false; + uploadScheduleTimer = 0; + lastProgressValue = -1; + pieceCount = 0; + downloadedBytes = 0; + uploadedBytes = 0; + memset(downloadRate, 0, sizeof(downloadRate)); + memset(uploadRate, 0, sizeof(uploadRate)); + transferRateTimer = 0; +} + +void TorrentClientPrivate::setError(TorrentClient::Error errorCode) +{ + this->error = errorCode; + switch (error) { + case TorrentClient::UnknownError: + errorString = QT_TRANSLATE_NOOP(TorrentClient, "Unknown error"); + break; + case TorrentClient::TorrentParseError: + errorString = QT_TRANSLATE_NOOP(TorrentClient, "Invalid torrent data"); + break; + case TorrentClient::InvalidTrackerError: + errorString = QT_TRANSLATE_NOOP(TorrentClient, "Unable to connect to tracker"); + break; + case TorrentClient::FileError: + errorString = QT_TRANSLATE_NOOP(TorrentClient, "File error"); + break; + case TorrentClient::ServerError: + errorString = QT_TRANSLATE_NOOP(TorrentClient, "Unable to initialize server"); + break; + } + emit q->error(errorCode); +} + +void TorrentClientPrivate::setState(TorrentClient::State state) +{ + this->state = state; + switch (state) { + case TorrentClient::Idle: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Idle"); + break; + case TorrentClient::Paused: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Paused"); + break; + case TorrentClient::Stopping: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Stopping"); + break; + case TorrentClient::Preparing: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Preparing"); + break; + case TorrentClient::Searching: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Searching"); + break; + case TorrentClient::Connecting: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Connecting"); + break; + case TorrentClient::WarmingUp: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Warming up"); + break; + case TorrentClient::Downloading: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Downloading"); + break; + case TorrentClient::Endgame: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Finishing"); + break; + case TorrentClient::Seeding: + stateString = QT_TRANSLATE_NOOP(TorrentClient, "Seeding"); + break; + } + emit q->stateChanged(state); +} + +void TorrentClientPrivate::callScheduler() +{ + if (!schedulerCalled) { + schedulerCalled = true; + QMetaObject::invokeMethod(q, "scheduleDownloads", Qt::QueuedConnection); + } +} + +void TorrentClientPrivate::callPeerConnector() +{ + if (!connectingToClients) { + connectingToClients = true; + QTimer::singleShot(10000, q, SLOT(connectToPeers())); + } +} + +TorrentClient::TorrentClient(QObject *parent) + : QObject(parent), d(new TorrentClientPrivate(this)) +{ + // Connect the file manager + connect(&d->fileManager, SIGNAL(dataRead(int, int, int, const QByteArray &)), + this, SLOT(sendToPeer(int, int, int, const QByteArray &))); + connect(&d->fileManager, SIGNAL(verificationProgress(int)), + this, SLOT(updateProgress(int))); + connect(&d->fileManager, SIGNAL(verificationDone()), + this, SLOT(fullVerificationDone())); + connect(&d->fileManager, SIGNAL(pieceVerified(int, bool)), + this, SLOT(pieceVerified(int, bool))); + connect(&d->fileManager, SIGNAL(error()), + this, SLOT(handleFileError())); + + // Connect the tracker client + connect(&d->trackerClient, SIGNAL(peerListUpdated(const QList<TorrentPeer> &)), + this, SLOT(addToPeerList(const QList<TorrentPeer> &))); + connect(&d->trackerClient, SIGNAL(stopped()), + this, SIGNAL(stopped())); +} + +TorrentClient::~TorrentClient() +{ + qDeleteAll(d->peers); + qDeleteAll(d->pendingPieces); + delete d; +} + +bool TorrentClient::setTorrent(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly) || !setTorrent(file.readAll())) { + d->setError(TorrentParseError); + return false; + } + return true; +} + +bool TorrentClient::setTorrent(const QByteArray &torrentData) +{ + if (!d->metaInfo.parse(torrentData)) { + d->setError(TorrentParseError); + return false; + } + + // Calculate SHA1 hash of the "info" section in the torrent + QByteArray infoValue = d->metaInfo.infoValue(); + d->infoHash = QCryptographicHash::hash(infoValue, QCryptographicHash::Sha1); + + return true; +} + +MetaInfo TorrentClient::metaInfo() const +{ + return d->metaInfo; +} + +void TorrentClient::setDestinationFolder(const QString &directory) +{ + d->destinationFolder = directory; +} + +QString TorrentClient::destinationFolder() const +{ + return d->destinationFolder; +} + +void TorrentClient::setDumpedState(const QByteArray &dumpedState) +{ + // Recover partially completed pieces + QDataStream stream(dumpedState); + + quint16 version = 0; + stream >> version; + if (version != 2) + return; + + stream >> d->completedPieces; + + while (!stream.atEnd()) { + int index; + int length; + QBitArray completed; + stream >> index >> length >> completed; + if (stream.status() != QDataStream::Ok) { + d->completedPieces.clear(); + break; + } + + TorrentPiece *piece = new TorrentPiece; + piece->index = index; + piece->length = length; + piece->completedBlocks = completed; + piece->requestedBlocks.resize(completed.size()); + piece->inProgress = false; + d->pendingPieces[index] = piece; + } +} + +QByteArray TorrentClient::dumpedState() const +{ + QByteArray partials; + QDataStream stream(&partials, QIODevice::WriteOnly); + + stream << quint16(2); + stream << d->completedPieces; + + // Save the state of all partially downloaded pieces into a format + // suitable for storing in settings. + QMap<int, TorrentPiece *>::ConstIterator it = d->pendingPieces.constBegin(); + while (it != d->pendingPieces.constEnd()) { + TorrentPiece *piece = it.value(); + if (blocksLeftForPiece(piece) > 0 && blocksLeftForPiece(piece) < piece->completedBlocks.size()) { + stream << piece->index; + stream << piece->length; + stream << piece->completedBlocks; + } + ++it; + } + + return partials; +} + +qint64 TorrentClient::progress() const +{ + return d->lastProgressValue; +} + +void TorrentClient::setDownloadedBytes(qint64 bytes) +{ + d->downloadedBytes = bytes; +} + +qint64 TorrentClient::downloadedBytes() const +{ + return d->downloadedBytes; +} + +void TorrentClient::setUploadedBytes(qint64 bytes) +{ + d->uploadedBytes = bytes; +} + +qint64 TorrentClient::uploadedBytes() const +{ + return d->uploadedBytes; +} + +int TorrentClient::connectedPeerCount() const +{ + int tmp = 0; + foreach (PeerWireClient *client, d->connections) { + if (client->state() == QAbstractSocket::ConnectedState) + ++tmp; + } + return tmp; +} + +int TorrentClient::seedCount() const +{ + int tmp = 0; + foreach (PeerWireClient *client, d->connections) { + if (client->availablePieces().count(true) == d->pieceCount) + ++tmp; + } + return tmp; +} + +TorrentClient::State TorrentClient::state() const +{ + return d->state; +} + +QString TorrentClient::stateString() const +{ + return d->stateString; +} + +TorrentClient::Error TorrentClient::error() const +{ + return d->error; +} + +QString TorrentClient::errorString() const +{ + return d->errorString; +} + +QByteArray TorrentClient::peerId() const +{ + return d->peerId; +} + +QByteArray TorrentClient::infoHash() const +{ + return d->infoHash; +} + +void TorrentClient::start() +{ + if (d->state != Idle) + return; + + TorrentServer::instance()->addClient(this); + + // Initialize the file manager + d->setState(Preparing); + d->fileManager.setMetaInfo(d->metaInfo); + d->fileManager.setDestinationFolder(d->destinationFolder); + d->fileManager.setCompletedPieces(d->completedPieces); + d->fileManager.start(QThread::LowestPriority); + d->fileManager.startDataVerification(); +} + +void TorrentClient::stop() +{ + if (d->state == Stopping) + return; + + TorrentServer::instance()->removeClient(this); + + // Update the state + State oldState = d->state; + d->setState(Stopping); + + // Stop the timer + if (d->transferRateTimer) { + killTimer(d->transferRateTimer); + d->transferRateTimer = 0; + } + + // Abort all existing connections + foreach (PeerWireClient *client, d->connections) { + RateController::instance()->removeSocket(client); + ConnectionManager::instance()->removeConnection(client); + client->abort(); + } + d->connections.clear(); + + // Perhaps stop the tracker + if (oldState > Preparing) { + d->trackerClient.stop(); + } else { + d->setState(Idle); + emit stopped(); + } +} + +void TorrentClient::setPaused(bool paused) +{ + if (paused) { + // Abort all connections, and set the max number of + // connections to 0. Keep the list of peers, so we can quickly + // resume later. + d->setState(Paused); + foreach (PeerWireClient *client, d->connections) + client->abort(); + d->connections.clear(); + TorrentServer::instance()->removeClient(this); + } else { + // Restore the max number of connections, and start the peer + // connector. We should also quickly start receiving incoming + // connections. + d->setState(d->completedPieces.count(true) == d->fileManager.pieceCount() + ? Seeding : Searching); + connectToPeers(); + TorrentServer::instance()->addClient(this); + } +} + +void TorrentClient::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == d->uploadScheduleTimer) { + // Update the state of who's choked and who's not + scheduleUploads(); + return; + } + + if (event->timerId() != d->transferRateTimer) { + QObject::timerEvent(event); + return; + } + + // Calculate average upload/download rate + qint64 uploadBytesPerSecond = 0; + qint64 downloadBytesPerSecond = 0; + for (int i = 0; i < RateControlWindowLength; ++i) { + uploadBytesPerSecond += d->uploadRate[i]; + downloadBytesPerSecond += d->downloadRate[i]; + } + uploadBytesPerSecond /= qint64(RateControlWindowLength); + downloadBytesPerSecond /= qint64(RateControlWindowLength); + for (int i = RateControlWindowLength - 2; i >= 0; --i) { + d->uploadRate[i + 1] = d->uploadRate[i]; + d->downloadRate[i + 1] = d->downloadRate[i]; + } + d->uploadRate[0] = 0; + d->downloadRate[0] = 0; + emit uploadRateUpdated(int(uploadBytesPerSecond)); + emit downloadRateUpdated(int(downloadBytesPerSecond)); + + // Stop the timer if there is no activity. + if (downloadBytesPerSecond == 0 && uploadBytesPerSecond == 0) { + killTimer(d->transferRateTimer); + d->transferRateTimer = 0; + } +} + +void TorrentClient::sendToPeer(int readId, int pieceIndex, int begin, const QByteArray &data) +{ + // Send the requested block to the peer if the client connection + // still exists; otherwise do nothing. This slot is called by the + // file manager after it has read a block of data. + PeerWireClient *client = d->readIds.value(readId); + if (client) { + if ((client->peerWireState() & PeerWireClient::ChokingPeer) == 0) + client->sendBlock(pieceIndex, begin, data); + } + d->readIds.remove(readId); +} + +void TorrentClient::fullVerificationDone() +{ + // Update our list of completed and incomplete pieces. + d->completedPieces = d->fileManager.completedPieces(); + d->incompletePieces.resize(d->completedPieces.size()); + d->pieceCount = d->completedPieces.size(); + for (int i = 0; i < d->fileManager.pieceCount(); ++i) { + if (!d->completedPieces.testBit(i)) + d->incompletePieces.setBit(i); + } + + updateProgress(); + + // If the checksums show that what the dumped state thought was + // partial was in fact complete, then we trust the checksums. + QMap<int, TorrentPiece *>::Iterator it = d->pendingPieces.begin(); + while (it != d->pendingPieces.end()) { + if (d->completedPieces.testBit(it.key())) + it = d->pendingPieces.erase(it); + else + ++it; + } + + d->uploadScheduleTimer = startTimer(UploadScheduleInterval); + + // Start the server + TorrentServer *server = TorrentServer::instance(); + if (!server->isListening()) { + // Set up the peer wire server + for (int i = ServerMinPort; i <= ServerMaxPort; ++i) { + if (server->listen(QHostAddress::Any, i)) + break; + } + if (!server->isListening()) { + d->setError(ServerError); + return; + } + } + + d->setState(d->completedPieces.count(true) == d->pieceCount ? Seeding : Searching); + + // Start the tracker client + d->trackerClient.start(d->metaInfo); +} + +void TorrentClient::pieceVerified(int pieceIndex, bool ok) +{ + TorrentPiece *piece = d->pendingPieces.value(pieceIndex); + + // Remove this piece from all payloads + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.begin(); + while (it != d->payloads.end()) { + if (it.value()->index == pieceIndex) + it = d->payloads.erase(it); + else + ++it; + } + + if (!ok) { + // If a piece did not pass the SHA1 check, we'll simply clear + // its state, and the scheduler will re-request it + piece->inProgress = false; + piece->completedBlocks.fill(false); + piece->requestedBlocks.fill(false); + d->callScheduler(); + return; + } + + // Update the peer list so we know who's still interesting. + foreach (TorrentPeer *peer, d->peers) { + if (!peer->interesting) + continue; + bool interesting = false; + for (int i = 0; i < d->pieceCount; ++i) { + if (peer->pieces.testBit(i) && d->incompletePieces.testBit(i)) { + interesting = true; + break; + } + } + peer->interesting = interesting; + } + + // Delete the piece and update our structures. + delete piece; + d->pendingPieces.remove(pieceIndex); + d->completedPieces.setBit(pieceIndex); + d->incompletePieces.clearBit(pieceIndex); + + // Notify connected peers. + foreach (PeerWireClient *client, d->connections) { + if (client->state() == QAbstractSocket::ConnectedState + && !client->availablePieces().testBit(pieceIndex)) { + client->sendPieceNotification(pieceIndex); + } + } + + // Notify the tracker if we've entered Seeding status; otherwise + // call the scheduler. + int completed = d->completedPieces.count(true); + if (completed == d->pieceCount) { + if (d->state != Seeding) { + d->setState(Seeding); + d->trackerClient.startSeeding(); + } + } else { + if (completed == 1) + d->setState(Downloading); + else if (d->incompletePieces.count(true) < 5 && d->pendingPieces.size() > d->incompletePieces.count(true)) + d->setState(Endgame); + d->callScheduler(); + } + + updateProgress(); +} + +void TorrentClient::handleFileError() +{ + if (d->state == Paused) + return; + setPaused(true); + emit error(FileError); +} + +void TorrentClient::connectToPeers() +{ + d->connectingToClients = false; + + if (d->state == Stopping || d->state == Idle || d->state == Paused) + return; + + if (d->state == Searching) + d->setState(Connecting); + + // Find the list of peers we are not currently connected to, where + // the more interesting peers are listed more than once. + QList<TorrentPeer *> weighedPeers = weighedFreePeers(); + + // Start as many connections as we can + while (!weighedPeers.isEmpty() && ConnectionManager::instance()->canAddConnection() + && (qrand() % (ConnectionManager::instance()->maxConnections() / 2))) { + PeerWireClient *client = new PeerWireClient(ConnectionManager::instance()->clientId(), this); + RateController::instance()->addSocket(client); + ConnectionManager::instance()->addConnection(client); + + initializeConnection(client); + d->connections << client; + + // Pick a random peer from the list of weighed peers. + TorrentPeer *peer = weighedPeers.takeAt(qrand() % weighedPeers.size()); + weighedPeers.removeAll(peer); + peer->connectStart = QDateTime::currentDateTime().toTime_t(); + peer->lastVisited = peer->connectStart; + + // Connect to the peer. + client->setPeer(peer); + client->connectToHost(peer->address, peer->port); + } +} + +QList<TorrentPeer *> TorrentClient::weighedFreePeers() const +{ + QList<TorrentPeer *> weighedPeers; + + // Generate a list of peers that we want to connect to. + uint now = QDateTime::currentDateTime().toTime_t(); + QList<TorrentPeer *> freePeers; + QMap<QString, int> connectionsPerPeer; + foreach (TorrentPeer *peer, d->peers) { + bool busy = false; + foreach (PeerWireClient *client, d->connections) { + if (client->state() == PeerWireClient::ConnectedState + && client->peerAddress() == peer->address + && client->peerPort() == peer->port) { + if (++connectionsPerPeer[peer->address.toString()] >= MaxConnectionPerPeer) { + busy = true; + break; + } + } + } + if (!busy && (now - peer->lastVisited) > uint(MinimumTimeBeforeRevisit)) + freePeers << peer; + } + + // Nothing to connect to + if (freePeers.isEmpty()) + return weighedPeers; + + // Assign points based on connection speed and pieces available. + QList<QPair<int, TorrentPeer *> > points; + foreach (TorrentPeer *peer, freePeers) { + int tmp = 0; + if (peer->interesting) { + tmp += peer->numCompletedPieces; + if (d->state == Seeding) + tmp = d->pieceCount - tmp; + if (!peer->connectStart) // An unknown peer is as interesting as a seed + tmp += d->pieceCount; + + // 1/5 of the total score for each second below 5 it takes to + // connect. + if (peer->connectTime < 5) + tmp += (d->pieceCount / 10) * (5 - peer->connectTime); + } + points << QPair<int, TorrentPeer *>(tmp, peer); + } + qSort(points); + + // Minimize the list so the point difference is never more than 1. + typedef QPair<int,TorrentPeer*> PointPair; + QMultiMap<int, TorrentPeer *> pointMap; + int lowestScore = 0; + int lastIndex = 0; + foreach (PointPair point, points) { + if (point.first > lowestScore) { + lowestScore = point.first; + ++lastIndex; + } + pointMap.insert(lastIndex, point.second); + } + + // Now make up a list of peers where the ones with more points are + // listed many times. + QMultiMap<int, TorrentPeer *>::ConstIterator it = pointMap.constBegin(); + while (it != pointMap.constEnd()) { + for (int i = 0; i < it.key() + 1; ++i) + weighedPeers << it.value(); + ++it; + } + + return weighedPeers; +} + +void TorrentClient::setupIncomingConnection(PeerWireClient *client) +{ + // Connect signals + initializeConnection(client); + + // Initialize this client + RateController::instance()->addSocket(client); + d->connections << client; + + client->initialize(d->infoHash, d->pieceCount); + client->sendPieceList(d->completedPieces); + + emit peerInfoUpdated(); + + if (d->state == Searching || d->state == Connecting) { + int completed = d->completedPieces.count(true); + if (completed == 0) + d->setState(WarmingUp); + else if (d->incompletePieces.count(true) < 5 && d->pendingPieces.size() > d->incompletePieces.count(true)) + d->setState(Endgame); + } + + if (d->connections.isEmpty()) + scheduleUploads(); +} + +void TorrentClient::setupOutgoingConnection() +{ + PeerWireClient *client = qobject_cast<PeerWireClient *>(sender()); + + // Update connection statistics. + foreach (TorrentPeer *peer, d->peers) { + if (peer->port == client->peerPort() && peer->address == client->peerAddress()) { + peer->connectTime = peer->lastVisited - peer->connectStart; + break; + } + } + + // Send handshake and piece list + client->initialize(d->infoHash, d->pieceCount); + client->sendPieceList(d->completedPieces); + + emit peerInfoUpdated(); + + if (d->state == Searching || d->state == Connecting) { + int completed = d->completedPieces.count(true); + if (completed == 0) + d->setState(WarmingUp); + else if (d->incompletePieces.count(true) < 5 && d->pendingPieces.size() > d->incompletePieces.count(true)) + d->setState(Endgame); + } +} + +void TorrentClient::initializeConnection(PeerWireClient *client) +{ + connect(client, SIGNAL(connected()), + this, SLOT(setupOutgoingConnection())); + connect(client, SIGNAL(disconnected()), + this, SLOT(removeClient())); + connect(client, SIGNAL(error(QAbstractSocket::SocketError)), + this, SLOT(removeClient())); + connect(client, SIGNAL(piecesAvailable(const QBitArray &)), + this, SLOT(peerPiecesAvailable(const QBitArray &))); + connect(client, SIGNAL(blockRequested(int, int, int)), + this, SLOT(peerRequestsBlock(int, int, int))); + connect(client, SIGNAL(blockReceived(int, int, const QByteArray &)), + this, SLOT(blockReceived(int, int, const QByteArray &))); + connect(client, SIGNAL(choked()), + this, SLOT(peerChoked())); + connect(client, SIGNAL(unchoked()), + this, SLOT(peerUnchoked())); + connect(client, SIGNAL(bytesWritten(qint64)), + this, SLOT(peerWireBytesWritten(qint64))); + connect(client, SIGNAL(bytesReceived(qint64)), + this, SLOT(peerWireBytesReceived(qint64))); +} + +void TorrentClient::removeClient() +{ + PeerWireClient *client = static_cast<PeerWireClient *>(sender()); + + // Remove the host from our list of known peers if the connection + // failed. + if (client->peer() && client->error() == QAbstractSocket::ConnectionRefusedError) + d->peers.removeAll(client->peer()); + + // Remove the client from RateController and all structures. + RateController::instance()->removeSocket(client); + d->connections.removeAll(client); + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.find(client); + while (it != d->payloads.end() && it.key() == client) { + TorrentPiece *piece = it.value(); + piece->inProgress = false; + piece->requestedBlocks.fill(false); + it = d->payloads.erase(it); + } + + // Remove pending read requests. + QMapIterator<int, PeerWireClient *> it2(d->readIds); + while (it2.findNext(client)) + d->readIds.remove(it2.key()); + + // Delete the client later. + disconnect(client, SIGNAL(disconnected()), this, SLOT(removeClient())); + client->deleteLater(); + ConnectionManager::instance()->removeConnection(client); + + emit peerInfoUpdated(); + d->callPeerConnector(); +} + +void TorrentClient::peerPiecesAvailable(const QBitArray &pieces) +{ + PeerWireClient *client = qobject_cast<PeerWireClient *>(sender()); + + // Find the peer in our list of announced peers. If it's there, + // then we can use the piece list into to gather statistics that + // help us decide what peers to connect to. + TorrentPeer *peer = 0; + QList<TorrentPeer *>::Iterator it = d->peers.begin(); + while (it != d->peers.end()) { + if ((*it)->address == client->peerAddress() && (*it)->port == client->peerPort()) { + peer = *it; + break; + } + ++it; + } + + // If the peer is a seed, and we are in seeding mode, then the + // peer is uninteresting. + if (pieces.count(true) == d->pieceCount) { + if (peer) + peer->seed = true; + emit peerInfoUpdated(); + if (d->state == Seeding) { + client->abort(); + return; + } else { + if (peer) + peer->interesting = true; + if ((client->peerWireState() & PeerWireClient::InterestedInPeer) == 0) + client->sendInterested(); + d->callScheduler(); + return; + } + } + + // Update our list of available pieces. + if (peer) { + peer->pieces = pieces; + peer->numCompletedPieces = pieces.count(true); + } + + // Check for interesting pieces, and tell the peer whether we are + // interested or not. + bool interested = false; + int piecesSize = pieces.size(); + for (int pieceIndex = 0; pieceIndex < piecesSize; ++pieceIndex) { + if (!pieces.testBit(pieceIndex)) + continue; + if (!d->completedPieces.testBit(pieceIndex)) { + interested = true; + if ((client->peerWireState() & PeerWireClient::InterestedInPeer) == 0) { + if (peer) + peer->interesting = true; + client->sendInterested(); + } + + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.find(client); + int inProgress = 0; + while (it != d->payloads.end() && it.key() == client) { + if (it.value()->inProgress) + inProgress += it.value()->requestedBlocks.count(true); + ++it; + } + if (!inProgress) + d->callScheduler(); + break; + } + } + if (!interested && (client->peerWireState() & PeerWireClient::InterestedInPeer)) { + if (peer) + peer->interesting = false; + client->sendNotInterested(); + } +} + +void TorrentClient::peerRequestsBlock(int pieceIndex, int begin, int length) +{ + PeerWireClient *client = qobject_cast<PeerWireClient *>(sender()); + + // Silently ignore requests from choked peers + if (client->peerWireState() & PeerWireClient::ChokingPeer) + return; + + // Silently ignore requests for pieces we don't have. + if (!d->completedPieces.testBit(pieceIndex)) + return; + + // Request the block from the file manager + d->readIds.insert(d->fileManager.read(pieceIndex, begin, length), + qobject_cast<PeerWireClient *>(sender())); +} + +void TorrentClient::blockReceived(int pieceIndex, int begin, const QByteArray &data) +{ + PeerWireClient *client = qobject_cast<PeerWireClient *>(sender()); + if (data.size() == 0) { + client->abort(); + return; + } + + // Ignore it if we already have this block. + int blockBit = begin / BlockSize; + TorrentPiece *piece = d->pendingPieces.value(pieceIndex); + if (!piece || piece->completedBlocks.testBit(blockBit)) { + // Discard blocks that we already have, and fill up the pipeline. + requestMore(client); + return; + } + + // If we are in warmup or endgame mode, cancel all duplicate + // requests for this block. + if (d->state == WarmingUp || d->state == Endgame) { + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.begin(); + while (it != d->payloads.end()) { + PeerWireClient *otherClient = it.key(); + if (otherClient != client && it.value()->index == pieceIndex) { + if (otherClient->incomingBlocks().contains(TorrentBlock(pieceIndex, begin, data.size()))) + it.key()->cancelRequest(pieceIndex, begin, data.size()); + } + ++it; + } + } + + if (d->state != Downloading && d->state != Endgame && d->completedPieces.count(true) > 0) + d->setState(Downloading); + + // Store this block + d->fileManager.write(pieceIndex, begin, data); + piece->completedBlocks.setBit(blockBit); + piece->requestedBlocks.clearBit(blockBit); + + if (blocksLeftForPiece(piece) == 0) { + // Ask the file manager to verify the newly downloaded piece + d->fileManager.verifyPiece(piece->index); + + // Remove this piece from all payloads + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.begin(); + while (it != d->payloads.end()) { + if (!it.value() || it.value()->index == piece->index) + it = d->payloads.erase(it); + else + ++it; + } + } + + // Fill up the pipeline. + requestMore(client); +} + +void TorrentClient::peerWireBytesWritten(qint64 size) +{ + if (!d->transferRateTimer) + d->transferRateTimer = startTimer(RateControlTimerDelay); + + d->uploadRate[0] += size; + d->uploadedBytes += size; + emit dataSent(size); +} + +void TorrentClient::peerWireBytesReceived(qint64 size) +{ + if (!d->transferRateTimer) + d->transferRateTimer = startTimer(RateControlTimerDelay); + + d->downloadRate[0] += size; + d->downloadedBytes += size; + emit dataSent(size); +} + +int TorrentClient::blocksLeftForPiece(const TorrentPiece *piece) const +{ + int blocksLeft = 0; + int completedBlocksSize = piece->completedBlocks.size(); + for (int i = 0; i < completedBlocksSize; ++i) { + if (!piece->completedBlocks.testBit(i)) + ++blocksLeft; + } + return blocksLeft; +} + +void TorrentClient::scheduleUploads() +{ + // Generate a list of clients sorted by their transfer + // speeds. When leeching, we sort by download speed, and when + // seeding, we sort by upload speed. Seeds are left out; there's + // no use in unchoking them. + QList<PeerWireClient *> allClients = d->connections; + QMultiMap<int, PeerWireClient *> transferSpeeds; + foreach (PeerWireClient *client, allClients) { + if (client->state() == QAbstractSocket::ConnectedState + && client->availablePieces().count(true) != d->pieceCount) { + if (d->state == Seeding) { + transferSpeeds.insert(client->uploadSpeed(), client); + } else { + transferSpeeds.insert(client->downloadSpeed(), client); + } + } + } + + // Unchoke the top 'MaxUploads' downloaders (peers that we are + // uploading to) and choke all others. + int maxUploaders = MaxUploads; + QMapIterator<int, PeerWireClient *> it(transferSpeeds); + it.toBack(); + while (it.hasPrevious()) { + PeerWireClient *client = it.previous().value(); + bool interested = (client->peerWireState() & PeerWireClient::PeerIsInterested); + + if (maxUploaders) { + allClients.removeAll(client); + if (client->peerWireState() & PeerWireClient::ChokingPeer) + client->unchokePeer(); + --maxUploaders; + continue; + } + + if ((client->peerWireState() & PeerWireClient::ChokingPeer) == 0) { + if ((qrand() % 10) == 0) + client->abort(); + else + client->chokePeer(); + allClients.removeAll(client); + } + if (!interested) + allClients.removeAll(client); + } + + // Only interested peers are left in allClients. Unchoke one + // random peer to allow it to compete for a position among the + // downloaders. (This is known as an "optimistic unchoke".) + if (!allClients.isEmpty()) { + PeerWireClient *client = allClients[qrand() % allClients.size()]; + if (client->peerWireState() & PeerWireClient::ChokingPeer) + client->unchokePeer(); + } +} + +void TorrentClient::scheduleDownloads() +{ + d->schedulerCalled = false; + + if (d->state == Stopping || d->state == Paused || d->state == Idle) + return; + + // Check what each client is doing, and assign payloads to those + // who are either idle or done. + foreach (PeerWireClient *client, d->connections) + schedulePieceForClient(client); +} + +void TorrentClient::schedulePieceForClient(PeerWireClient *client) +{ + // Only schedule connected clients. + if (client->state() != QTcpSocket::ConnectedState) + return; + + // The peer has choked us; try again later. + if (client->peerWireState() & PeerWireClient::ChokedByPeer) + return; + + // Make a list of all the client's pending pieces, and count how + // many blocks have been requested. + QList<int> currentPieces; + bool somePiecesAreNotInProgress = false; + TorrentPiece *lastPendingPiece = 0; + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.find(client); + while (it != d->payloads.end() && it.key() == client) { + lastPendingPiece = it.value(); + if (lastPendingPiece->inProgress) { + currentPieces << lastPendingPiece->index; + } else { + somePiecesAreNotInProgress = true; + } + ++it; + } + + // Skip clients that already have too many blocks in progress. + if (client->incomingBlocks().size() >= ((d->state == Endgame || d->state == WarmingUp) + ? MaxBlocksInMultiMode : MaxBlocksInProgress)) + return; + + // If all pieces are in progress, but we haven't filled up our + // block requesting quota, then we need to schedule another piece. + if (!somePiecesAreNotInProgress || client->incomingBlocks().size() > 0) + lastPendingPiece = 0; + TorrentPiece *piece = lastPendingPiece; + + // In warmup state, all clients request blocks from the same pieces. + if (d->state == WarmingUp && d->pendingPieces.size() >= 4) { + piece = d->payloads.value(client); + if (!piece) { + QList<TorrentPiece *> values = d->pendingPieces.values(); + piece = values.value(qrand() % values.size()); + piece->inProgress = true; + d->payloads.insert(client, piece); + } + if (piece->completedBlocks.count(false) == client->incomingBlocks().size()) + return; + } + + // If no pieces are currently in progress, schedule a new one. + if (!piece) { + // Build up a list of what pieces that we have not completed + // are available to this client. + QBitArray incompletePiecesAvailableToClient = d->incompletePieces; + + // Remove all pieces that are marked as being in progress + // already (i.e., pieces that this or other clients are + // already waiting for). A special rule applies to warmup and + // endgame mode; there, we allow several clients to request + // the same piece. In endgame mode, this only applies to + // clients that are currently uploading (more than 1.0KB/s). + if ((d->state == Endgame && client->uploadSpeed() < 1024) || d->state != WarmingUp) { + QMap<int, TorrentPiece *>::ConstIterator it = d->pendingPieces.constBegin(); + while (it != d->pendingPieces.constEnd()) { + if (it.value()->inProgress) + incompletePiecesAvailableToClient.clearBit(it.key()); + ++it; + } + } + + // Remove all pieces that the client cannot download. + incompletePiecesAvailableToClient &= client->availablePieces(); + + // Remove all pieces that this client has already requested. + foreach (int i, currentPieces) + incompletePiecesAvailableToClient.clearBit(i); + + // Only continue if more pieces can be scheduled. If no pieces + // are available and no blocks are in progress, just leave + // the connection idle; it might become interesting later. + if (incompletePiecesAvailableToClient.count(true) == 0) + return; + + // Check if any of the partially completed pieces can be + // recovered, and if so, pick a random one of them. + QList<TorrentPiece *> partialPieces; + QMap<int, TorrentPiece *>::ConstIterator it = d->pendingPieces.constBegin(); + while (it != d->pendingPieces.constEnd()) { + TorrentPiece *tmp = it.value(); + if (incompletePiecesAvailableToClient.testBit(it.key())) { + if (!tmp->inProgress || d->state == WarmingUp || d->state == Endgame) { + partialPieces << tmp; + break; + } + } + ++it; + } + if (!partialPieces.isEmpty()) + piece = partialPieces.value(qrand() % partialPieces.size()); + + if (!piece) { + // Pick a random piece 3 out of 4 times; otherwise, pick either + // one of the most common or the least common pieces available, + // depending on the state we're in. + int pieceIndex = 0; + if (d->state == WarmingUp || (qrand() & 4) == 0) { + int *occurrances = new int[d->pieceCount]; + memset(occurrances, 0, d->pieceCount * sizeof(int)); + + // Count how many of each piece are available. + foreach (PeerWireClient *peer, d->connections) { + QBitArray peerPieces = peer->availablePieces(); + int peerPiecesSize = peerPieces.size(); + for (int i = 0; i < peerPiecesSize; ++i) { + if (peerPieces.testBit(i)) + ++occurrances[i]; + } + } + + // Find the rarest or most common pieces. + int numOccurrances = d->state == WarmingUp ? 0 : 99999; + QList<int> piecesReadyForDownload; + for (int i = 0; i < d->pieceCount; ++i) { + if (d->state == WarmingUp) { + // Add common pieces + if (occurrances[i] >= numOccurrances + && incompletePiecesAvailableToClient.testBit(i)) { + if (occurrances[i] > numOccurrances) + piecesReadyForDownload.clear(); + piecesReadyForDownload.append(i); + numOccurrances = occurrances[i]; + } + } else { + // Add rare pieces + if (occurrances[i] <= numOccurrances + && incompletePiecesAvailableToClient.testBit(i)) { + if (occurrances[i] < numOccurrances) + piecesReadyForDownload.clear(); + piecesReadyForDownload.append(i); + numOccurrances = occurrances[i]; + } + } + } + + // Select one piece randomly + pieceIndex = piecesReadyForDownload.at(qrand() % piecesReadyForDownload.size()); + delete [] occurrances; + } else { + // Make up a list of available piece indices, and pick + // a random one. + QList<int> values; + int incompletePiecesAvailableToClientSize = incompletePiecesAvailableToClient.size(); + for (int i = 0; i < incompletePiecesAvailableToClientSize; ++i) { + if (incompletePiecesAvailableToClient.testBit(i)) + values << i; + } + pieceIndex = values.at(qrand() % values.size()); + } + + // Create a new TorrentPiece and fill in all initial + // properties. + piece = new TorrentPiece; + piece->index = pieceIndex; + piece->length = d->fileManager.pieceLengthAt(pieceIndex); + int numBlocks = piece->length / BlockSize; + if (piece->length % BlockSize) + ++numBlocks; + piece->completedBlocks.resize(numBlocks); + piece->requestedBlocks.resize(numBlocks); + d->pendingPieces.insert(pieceIndex, piece); + } + + piece->inProgress = true; + d->payloads.insert(client, piece); + } + + // Request more blocks from all pending pieces. + requestMore(client); +} + +void TorrentClient::requestMore(PeerWireClient *client) +{ + // Make a list of all pieces this client is currently waiting for, + // and count the number of blocks in progress. + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.find(client); + int numBlocksInProgress = client->incomingBlocks().size(); + QList<TorrentPiece *> piecesInProgress; + while (it != d->payloads.end() && it.key() == client) { + TorrentPiece *piece = it.value(); + if (piece->inProgress || (d->state == WarmingUp || d->state == Endgame)) + piecesInProgress << piece; + ++it; + } + + // If no pieces are in progress, call the scheduler. + if (piecesInProgress.isEmpty() && d->incompletePieces.count(true)) { + d->callScheduler(); + return; + } + + // If too many pieces are in progress, there's nothing to do. + int maxInProgress = ((d->state == Endgame || d->state == WarmingUp) + ? MaxBlocksInMultiMode : MaxBlocksInProgress); + if (numBlocksInProgress == maxInProgress) + return; + + // Starting with the first piece that we're waiting for, request + // blocks until the quota is filled up. + foreach (TorrentPiece *piece, piecesInProgress) { + numBlocksInProgress += requestBlocks(client, piece, maxInProgress - numBlocksInProgress); + if (numBlocksInProgress == maxInProgress) + break; + } + + // If we still didn't fill up the quota, we need to schedule more + // pieces. + if (numBlocksInProgress < maxInProgress && d->state != WarmingUp) + d->callScheduler(); +} + +int TorrentClient::requestBlocks(PeerWireClient *client, TorrentPiece *piece, int maxBlocks) +{ + // Generate the list of incomplete blocks for this piece. + QVector<int> bits; + int completedBlocksSize = piece->completedBlocks.size(); + for (int i = 0; i < completedBlocksSize; ++i) { + if (!piece->completedBlocks.testBit(i) && !piece->requestedBlocks.testBit(i)) + bits << i; + } + + // Nothing more to request. + if (bits.size() == 0) { + if (d->state != WarmingUp && d->state != Endgame) + return 0; + bits.clear(); + for (int i = 0; i < completedBlocksSize; ++i) { + if (!piece->completedBlocks.testBit(i)) + bits << i; + } + } + + if (d->state == WarmingUp || d->state == Endgame) { + // By randomizing the list of blocks to request, we + // significantly speed up the warmup and endgame modes, where + // the same blocks are requested from multiple peers. The + // speedup comes from an increased chance of receiving + // different blocks from the different peers. + for (int i = 0; i < bits.size(); ++i) { + int a = qrand() % bits.size(); + int b = qrand() % bits.size(); + int tmp = bits[a]; + bits[a] = bits[b]; + bits[b] = tmp; + } + } + + // Request no more blocks than we've been asked to. + int blocksToRequest = qMin(maxBlocks, bits.size()); + + // Calculate the offset and size of each block, and send requests. + for (int i = 0; i < blocksToRequest; ++i) { + int blockSize = BlockSize; + if ((piece->length % BlockSize) && bits.at(i) == completedBlocksSize - 1) + blockSize = piece->length % BlockSize; + client->requestBlock(piece->index, bits.at(i) * BlockSize, blockSize); + piece->requestedBlocks.setBit(bits.at(i)); + } + + return blocksToRequest; +} + +void TorrentClient::peerChoked() +{ + PeerWireClient *client = qobject_cast<PeerWireClient *>(sender()); + if (!client) + return; + + // When the peer chokes us, we immediately forget about all blocks + // we've requested from it. We also remove the piece from out + // payload, making it available to other clients. + QMultiMap<PeerWireClient *, TorrentPiece *>::Iterator it = d->payloads.find(client); + while (it != d->payloads.end() && it.key() == client) { + it.value()->inProgress = false; + it.value()->requestedBlocks.fill(false); + it = d->payloads.erase(it); + } +} + +void TorrentClient::peerUnchoked() +{ + PeerWireClient *client = qobject_cast<PeerWireClient *>(sender()); + if (!client) + return; + + // We got unchoked, which means we can request more blocks. + if (d->state != Seeding) + d->callScheduler(); +} + +void TorrentClient::addToPeerList(const QList<TorrentPeer> &peerList) +{ + // Add peers we don't already know of to our list of peers. + QList<QHostAddress> addresses = QNetworkInterface::allAddresses(); + foreach (TorrentPeer peer, peerList) { + if (addresses.contains(peer.address) + && peer.port == TorrentServer::instance()->serverPort()) { + // Skip our own server. + continue; + } + + bool known = false; + foreach (TorrentPeer *knownPeer, d->peers) { + if (knownPeer->port == peer.port + && knownPeer->address == peer.address) { + known = true; + break; + } + } + if (!known) { + TorrentPeer *newPeer = new TorrentPeer; + *newPeer = peer; + newPeer->interesting = true; + newPeer->seed = false; + newPeer->lastVisited = 0; + newPeer->connectStart = 0; + newPeer->connectTime = 999999; + newPeer->pieces.resize(d->pieceCount); + newPeer->numCompletedPieces = 0; + d->peers << newPeer; + } + } + + // If we've got more peers than we can connect to, we remove some + // of the peers that have no (or low) activity. + int maxPeers = ConnectionManager::instance()->maxConnections() * 3; + if (d->peers.size() > maxPeers) { + // Find what peers are currently connected & active + QSet<TorrentPeer *> activePeers; + foreach (TorrentPeer *peer, d->peers) { + foreach (PeerWireClient *client, d->connections) { + if (client->peer() == peer && (client->downloadSpeed() + client->uploadSpeed()) > 1024) + activePeers << peer; + } + } + + // Remove inactive peers from the peer list until we're below + // the max connections count. + QList<int> toRemove; + for (int i = 0; i < d->peers.size() && (d->peers.size() - toRemove.size()) > maxPeers; ++i) { + if (!activePeers.contains(d->peers.at(i))) + toRemove << i; + } + QListIterator<int> toRemoveIterator(toRemove); + toRemoveIterator.toBack(); + while (toRemoveIterator.hasPrevious()) + d->peers.removeAt(toRemoveIterator.previous()); + + // If we still have too many peers, remove the oldest ones. + while (d->peers.size() > maxPeers) + d->peers.takeFirst(); + } + + if (d->state != Paused && d->state != Stopping && d->state != Idle) { + if (d->state == Searching || d->state == WarmingUp) + connectToPeers(); + else + d->callPeerConnector(); + } +} + +void TorrentClient::trackerStopped() +{ + d->setState(Idle); + emit stopped(); +} + +void TorrentClient::updateProgress(int progress) +{ + if (progress == -1 && d->pieceCount > 0) { + int newProgress = (d->completedPieces.count(true) * 100) / d->pieceCount; + if (d->lastProgressValue != newProgress) { + d->lastProgressValue = newProgress; + emit progressUpdated(newProgress); + } + } else if (d->lastProgressValue != progress) { + d->lastProgressValue = progress; + emit progressUpdated(progress); + } +} diff --git a/examples/network/torrent/torrentclient.h b/examples/network/torrent/torrentclient.h new file mode 100644 index 0000000..6caaf38 --- /dev/null +++ b/examples/network/torrent/torrentclient.h @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TORRENTCLIENT_H +#define TORRENTCLIENT_H + +#include <QBitArray> +#include <QHostAddress> +#include <QList> + +class MetaInfo; +class PeerWireClient; +class TorrentClientPrivate; +class TorrentPeer; +class TorrentPiece; +QT_BEGIN_NAMESPACE +class QTimerEvent; +QT_END_NAMESPACE + +class TorrentPeer { +public: + QHostAddress address; + quint16 port; + QString id; + bool interesting; + bool seed; + uint lastVisited; + uint connectStart; + uint connectTime; + QBitArray pieces; + int numCompletedPieces; + + inline bool operator==(const TorrentPeer &other) + { + return port == other.port + && address == other.address + && id == other.id; + } +}; + +class TorrentClient : public QObject +{ + Q_OBJECT + +public: + enum State { + Idle, + Paused, + Stopping, + Preparing, + Searching, + Connecting, + WarmingUp, + Downloading, + Endgame, + Seeding + }; + enum Error { + UnknownError, + TorrentParseError, + InvalidTrackerError, + FileError, + ServerError + }; + + TorrentClient(QObject *parent = 0); + ~TorrentClient(); + + bool setTorrent(const QString &fileName); + bool setTorrent(const QByteArray &torrentData); + MetaInfo metaInfo() const; + + void setMaxConnections(int connections); + int maxConnections() const; + + void setDestinationFolder(const QString &directory); + QString destinationFolder() const; + + void setDumpedState(const QByteArray &dumpedState); + QByteArray dumpedState() const; + + // Progress and stats for download feedback. + qint64 progress() const; + void setDownloadedBytes(qint64 bytes); + qint64 downloadedBytes() const; + void setUploadedBytes(qint64 bytes); + qint64 uploadedBytes() const; + int connectedPeerCount() const; + int seedCount() const; + + // Accessors for the tracker + QByteArray peerId() const; + QByteArray infoHash() const; + quint16 serverPort() const; + + // State and error. + State state() const; + QString stateString() const; + Error error() const; + QString errorString() const; + +signals: + void stateChanged(TorrentClient::State state); + void error(TorrentClient::Error error); + + void downloadCompleted(); + void peerInfoUpdated(); + + void dataSent(int uploadedBytes); + void dataReceived(int downloadedBytes); + void progressUpdated(int percentProgress); + void downloadRateUpdated(int bytesPerSecond); + void uploadRateUpdated(int bytesPerSecond); + + void stopped(); + +public slots: + void start(); + void stop(); + void setPaused(bool paused); + void setupIncomingConnection(PeerWireClient *client); + +protected slots: + void timerEvent(QTimerEvent *event); + +private slots: + // File management + void sendToPeer(int readId, int pieceIndex, int begin, const QByteArray &data); + void fullVerificationDone(); + void pieceVerified(int pieceIndex, bool ok); + void handleFileError(); + + // Connection handling + void connectToPeers(); + QList<TorrentPeer *> weighedFreePeers() const; + void setupOutgoingConnection(); + void initializeConnection(PeerWireClient *client); + void removeClient(); + void peerPiecesAvailable(const QBitArray &pieces); + void peerRequestsBlock(int pieceIndex, int begin, int length); + void blockReceived(int pieceIndex, int begin, const QByteArray &data); + void peerWireBytesWritten(qint64 bytes); + void peerWireBytesReceived(qint64 bytes); + int blocksLeftForPiece(const TorrentPiece *piece) const; + + // Scheduling + void scheduleUploads(); + void scheduleDownloads(); + void schedulePieceForClient(PeerWireClient *client); + void requestMore(PeerWireClient *client); + int requestBlocks(PeerWireClient *client, TorrentPiece *piece, int maxBlocks); + void peerChoked(); + void peerUnchoked(); + + // Tracker handling + void addToPeerList(const QList<TorrentPeer> &peerList); + void trackerStopped(); + + // Progress + void updateProgress(int progress = -1); + +private: + TorrentClientPrivate *d; + friend class TorrentClientPrivate; +}; + +#endif diff --git a/examples/network/torrent/torrentserver.cpp b/examples/network/torrent/torrentserver.cpp new file mode 100644 index 0000000..3087d6a --- /dev/null +++ b/examples/network/torrent/torrentserver.cpp @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "connectionmanager.h" +#include "peerwireclient.h" +#include "ratecontroller.h" +#include "torrentclient.h" +#include "torrentserver.h" + +Q_GLOBAL_STATIC(TorrentServer, torrentServer) + +TorrentServer *TorrentServer::instance() +{ + return torrentServer(); +} + +void TorrentServer::addClient(TorrentClient *client) +{ + clients << client; +} + +void TorrentServer::removeClient(TorrentClient *client) +{ + clients.removeAll(client); +} + +void TorrentServer::incomingConnection(int socketDescriptor) +{ + PeerWireClient *client = + new PeerWireClient(ConnectionManager::instance()->clientId(), this); + + if (client->setSocketDescriptor(socketDescriptor)) { + if (ConnectionManager::instance()->canAddConnection() && !clients.isEmpty()) { + connect(client, SIGNAL(infoHashReceived(const QByteArray &)), + this, SLOT(processInfoHash(const QByteArray &))); + connect(client, SIGNAL(error(QAbstractSocket::SocketError)), + this, SLOT(removeClient())); + RateController::instance()->addSocket(client); + ConnectionManager::instance()->addConnection(client); + return; + } + } + client->abort(); + delete client; +} + +void TorrentServer::removeClient() +{ + PeerWireClient *peer = qobject_cast<PeerWireClient *>(sender()); + RateController::instance()->removeSocket(peer); + ConnectionManager::instance()->removeConnection(peer); + peer->deleteLater(); +} + +void TorrentServer::processInfoHash(const QByteArray &infoHash) +{ + PeerWireClient *peer = qobject_cast<PeerWireClient *>(sender()); + foreach (TorrentClient *client, clients) { + if (client->state() >= TorrentClient::Searching && client->infoHash() == infoHash) { + peer->disconnect(peer, 0, this, 0); + client->setupIncomingConnection(peer); + return; + } + } + removeClient(); +} diff --git a/examples/network/torrent/torrentserver.h b/examples/network/torrent/torrentserver.h new file mode 100644 index 0000000..6ae89e1 --- /dev/null +++ b/examples/network/torrent/torrentserver.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TORRENTSERVER_H +#define TORRENTSERVER_H + +#include <QList> +#include <QTcpServer> + +class TorrentClient; + +class TorrentServer : public QTcpServer +{ + Q_OBJECT + +public: + inline TorrentServer() {} + static TorrentServer *instance(); + + void addClient(TorrentClient *client); + void removeClient(TorrentClient *client); + +protected: + void incomingConnection(int socketDescriptor); + +private slots: + void removeClient(); + void processInfoHash(const QByteArray &infoHash); + +private: + QList<TorrentClient *> clients; +}; + +#endif diff --git a/examples/network/torrent/trackerclient.cpp b/examples/network/torrent/trackerclient.cpp new file mode 100644 index 0000000..bda8140 --- /dev/null +++ b/examples/network/torrent/trackerclient.cpp @@ -0,0 +1,237 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "bencodeparser.h" +#include "connectionmanager.h" +#include "torrentclient.h" +#include "torrentserver.h" +#include "trackerclient.h" + +#include <QtCore> + +TrackerClient::TrackerClient(TorrentClient *downloader, QObject *parent) + : QObject(parent), torrentDownloader(downloader) +{ + length = 0; + requestInterval = 5 * 60; + requestIntervalTimer = -1; + firstTrackerRequest = true; + lastTrackerRequest = false; + firstSeeding = true; + + connect(&http, SIGNAL(done(bool)), this, SLOT(httpRequestDone(bool))); +} + +void TrackerClient::start(const MetaInfo &info) +{ + metaInfo = info; + QTimer::singleShot(0, this, SLOT(fetchPeerList())); + + if (metaInfo.fileForm() == MetaInfo::SingleFileForm) { + length = metaInfo.singleFile().length; + } else { + QList<MetaInfoMultiFile> files = metaInfo.multiFiles(); + for (int i = 0; i < files.size(); ++i) + length += files.at(i).length; + } +} + +void TrackerClient::startSeeding() +{ + firstSeeding = true; + fetchPeerList(); +} + +void TrackerClient::stop() +{ + lastTrackerRequest = true; + http.abort(); + fetchPeerList(); +} + +void TrackerClient::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == requestIntervalTimer) { + if (http.state() == QHttp::Unconnected) + fetchPeerList(); + } else { + QObject::timerEvent(event); + } +} + +void TrackerClient::fetchPeerList() +{ + // Prepare connection details + QString fullUrl = metaInfo.announceUrl(); + QUrl url(fullUrl); + QString passkey = "?"; + if (fullUrl.contains("?passkey")) { + passkey = metaInfo.announceUrl().mid(fullUrl.indexOf("?passkey"), -1); + passkey += "&"; + } + + // Percent encode the hash + QByteArray infoHash = torrentDownloader->infoHash(); + QString encodedSum; + for (int i = 0; i < infoHash.size(); ++i) { + encodedSum += '%'; + encodedSum += QString::number(infoHash[i], 16).right(2).rightJustified(2, '0'); + } + + bool seeding = (torrentDownloader->state() == TorrentClient::Seeding); + QByteArray query; + query += url.path().toLatin1(); + query += passkey; + query += "info_hash=" + encodedSum; + query += "&peer_id=" + ConnectionManager::instance()->clientId(); + query += "&port=" + QByteArray::number(TorrentServer::instance()->serverPort()); + query += "&compact=1"; + query += "&uploaded=" + QByteArray::number(torrentDownloader->uploadedBytes()); + + if (!firstSeeding) { + query += "&downloaded=0"; + query += "&left=0"; + } else { + query += "&downloaded=" + QByteArray::number( + torrentDownloader->downloadedBytes()); + int left = qMax<int>(0, metaInfo.totalSize() - torrentDownloader->downloadedBytes()); + query += "&left=" + QByteArray::number(seeding ? 0 : left); + } + + if (seeding && firstSeeding) { + query += "&event=completed"; + firstSeeding = false; + } else if (firstTrackerRequest) { + firstTrackerRequest = false; + query += "&event=started"; + } else if(lastTrackerRequest) { + query += "&event=stopped"; + } + + if (!trackerId.isEmpty()) + query += "&trackerid=" + trackerId; + + http.setHost(url.host(), url.port() == -1 ? 80 : url.port()); + if (!url.userName().isEmpty()) + http.setUser(url.userName(), url.password()); + http.get(query); +} + +void TrackerClient::httpRequestDone(bool error) +{ + if (lastTrackerRequest) { + emit stopped(); + return; + } + + if (error) { + emit connectionError(http.error()); + return; + } + + QByteArray response = http.readAll(); + http.abort(); + + BencodeParser parser; + if (!parser.parse(response)) { + qWarning("Error parsing bencode response from tracker: %s", + qPrintable(parser.errorString())); + http.abort(); + return; + } + + QMap<QByteArray, QVariant> dict = parser.dictionary(); + + if (dict.contains("failure reason")) { + // no other items are present + emit failure(QString::fromUtf8(dict.value("failure reason").toByteArray())); + return; + } + + if (dict.contains("warning message")) { + // continue processing + emit warning(QString::fromUtf8(dict.value("warning message").toByteArray())); + } + + if (dict.contains("tracker id")) { + // store it + trackerId = dict.value("tracker id").toByteArray(); + } + + if (dict.contains("interval")) { + // Mandatory item + if (requestIntervalTimer != -1) + killTimer(requestIntervalTimer); + requestIntervalTimer = startTimer(dict.value("interval").toInt() * 1000); + } + + if (dict.contains("peers")) { + // store it + peers.clear(); + QVariant peerEntry = dict.value("peers"); + if (peerEntry.type() == QVariant::List) { + QList<QVariant> peerTmp = peerEntry.toList(); + for (int i = 0; i < peerTmp.size(); ++i) { + TorrentPeer tmp; + QMap<QByteArray, QVariant> peer = qVariantValue<QMap<QByteArray, QVariant> >(peerTmp.at(i)); + tmp.id = QString::fromUtf8(peer.value("peer id").toByteArray()); + tmp.address.setAddress(QString::fromUtf8(peer.value("ip").toByteArray())); + tmp.port = peer.value("port").toInt(); + peers << tmp; + } + } else { + QByteArray peerTmp = peerEntry.toByteArray(); + for (int i = 0; i < peerTmp.size(); i += 6) { + TorrentPeer tmp; + uchar *data = (uchar *)peerTmp.constData() + i; + tmp.port = (int(data[4]) << 8) + data[5]; + uint ipAddress = 0; + ipAddress += uint(data[0]) << 24; + ipAddress += uint(data[1]) << 16; + ipAddress += uint(data[2]) << 8; + ipAddress += uint(data[3]); + tmp.address.setAddress(ipAddress); + peers << tmp; + } + } + emit peerListUpdated(peers); + } +} diff --git a/examples/network/torrent/trackerclient.h b/examples/network/torrent/trackerclient.h new file mode 100644 index 0000000..f1ce37db --- /dev/null +++ b/examples/network/torrent/trackerclient.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TRACKERCLIENT_H +#define TRACKERCLIENT_H + +#include <QByteArray> +#include <QList> +#include <QObject> +#include <QHostAddress> +#include <QHttp> + +#include "metainfo.h" +#include "torrentclient.h" + +class TorrentClient; + +class TrackerClient : public QObject +{ + Q_OBJECT + +public: + TrackerClient(TorrentClient *downloader, QObject *parent = 0); + + void start(const MetaInfo &info); + void stop(); + void startSeeding(); + +signals: + void connectionError(QHttp::Error error); + + void failure(const QString &reason); + void warning(const QString &message); + void peerListUpdated(const QList<TorrentPeer> &peerList); + + void uploadCountUpdated(qint64 newUploadCount); + void downloadCountUpdated(qint64 newDownloadCount); + + void stopped(); + +protected: + void timerEvent(QTimerEvent *event); + +private slots: + void fetchPeerList(); + void httpRequestDone(bool error); + +private: + TorrentClient *torrentDownloader; + + int requestInterval; + int requestIntervalTimer; + QHttp http; + MetaInfo metaInfo; + QByteArray trackerId; + QList<TorrentPeer> peers; + qint64 uploadedBytes; + qint64 downloadedBytes; + qint64 length; + + bool firstTrackerRequest; + bool lastTrackerRequest; + bool firstSeeding; +}; + +#endif |