// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #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 #include #include // 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; struct TorrentPiece { QBitArray completedBlocks; QBitArray requestedBlocks; int index = 0; int length = 0; bool inProgress = false; }; 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 connections; QList peers; bool schedulerCalled; void callScheduler(); bool connectingToClients; void callPeerConnector(); int uploadScheduleTimer; // Pieces QMap readIds; QMultiMap payloads; QMap 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, &TorrentClient::connectToPeers); } } TorrentClient::TorrentClient(QObject *parent) : QObject(parent), d(new TorrentClientPrivate(this)) { // Connect the file manager connect(&d->fileManager, &FileManager::dataRead, this, &TorrentClient::sendToPeer); connect(&d->fileManager, &FileManager::verificationProgress, this, &TorrentClient::updateProgress); connect(&d->fileManager, &FileManager::verificationDone, this, &TorrentClient::fullVerificationDone); connect(&d->fileManager, &FileManager::pieceVerified, this, &TorrentClient::pieceVerified); connect(&d->fileManager, &FileManager::error, this, &TorrentClient::handleFileError); // Connect the tracker client connect(&d->trackerClient, &TrackerClient::peerListUpdated, this, &TorrentClient::addToPeerList); connect(&d->trackerClient, &TrackerClient::stopped, this, &TorrentClient::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::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; for (PeerWireClient *client : d->connections) { if (client->state() == QAbstractSocket::ConnectedState) ++tmp; } return tmp; } int TorrentClient::seedCount() const { int tmp = 0; for (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 for (PeerWireClient *client : std::as_const(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); for (PeerWireClient *client : std::as_const(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::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::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. for (TorrentPeer *peer : std::as_const(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. for (PeerWireClient *client : std::as_const(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 weighedPeers = weighedFreePeers(); // Start as many connections as we can while (!weighedPeers.isEmpty() && ConnectionManager::instance()->canAddConnection() && (QRandomGenerator::global()->bounded(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(QRandomGenerator::global()->bounded(weighedPeers.size())); weighedPeers.removeAll(peer); peer->connectStart = QDateTime::currentSecsSinceEpoch(); peer->lastVisited = peer->connectStart; // Connect to the peer. client->setPeer(peer); client->connectToHost(peer->address, peer->port); } } QList TorrentClient::weighedFreePeers() const { QList weighedPeers; // Generate a list of peers that we want to connect to. qint64 now = QDateTime::currentSecsSinceEpoch(); QList freePeers; QMap connectionsPerPeer; for (TorrentPeer *peer : std::as_const(d->peers)) { bool busy = false; for (PeerWireClient *client : std::as_const(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 > points; for (TorrentPeer *peer : std::as_const(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(tmp, peer); } std::sort(points.begin(), points.end()); // Minimize the list so the point difference is never more than 1. typedef QPair PointPair; QMultiMap pointMap; int lowestScore = 0; int lastIndex = 0; for (const PointPair &point : std::as_const(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::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(sender()); // Update connection statistics. for (TorrentPeer *peer : std::as_const(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, &PeerWireClient::connected, this, &TorrentClient::setupOutgoingConnection); connect(client, &PeerWireClient::disconnected, this, &TorrentClient::removeClient); connect(client, &PeerWireClient::errorOccurred, this, &TorrentClient::removeClient); connect(client, &PeerWireClient::piecesAvailable, this, &TorrentClient::peerPiecesAvailable); connect(client, &PeerWireClient::blockRequested, this, &TorrentClient::peerRequestsBlock); connect(client, &PeerWireClient::blockReceived, this, &TorrentClient::blockReceived); connect(client, &PeerWireClient::choked, this, &TorrentClient::peerChoked); connect(client, &PeerWireClient::unchoked, this, &TorrentClient::peerUnchoked); connect(client, &PeerWireClient::bytesWritten, this, &TorrentClient::peerWireBytesWritten); connect(client, &PeerWireClient::bytesReceived, this, &TorrentClient::peerWireBytesReceived); } void TorrentClient::removeClient() { PeerWireClient *client = static_cast(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); for (auto it = d->payloads.find(client); it != d->payloads.end() && it.key() == client; /*erasing*/) { TorrentPiece *piece = it.value(); piece->inProgress = false; piece->requestedBlocks.fill(false); it = d->payloads.erase(it); } // Remove pending read requests. for (auto it = d->readIds.begin(), end = d->readIds.end(); it != end; /*erasing*/) { if (it.value() == client) it = d->readIds.erase(it); else ++it; } // Delete the client later. disconnect(client, &PeerWireClient::disconnected, this, &TorrentClient::removeClient); client->deleteLater(); ConnectionManager::instance()->removeConnection(client); emit peerInfoUpdated(); d->callPeerConnector(); } void TorrentClient::peerPiecesAvailable(const QBitArray &pieces) { PeerWireClient *client = qobject_cast(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 = nullptr; QList::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::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(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(sender())); } void TorrentClient::blockReceived(int pieceIndex, int begin, const QByteArray &data) { PeerWireClient *client = qobject_cast(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::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::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 allClients = d->connections; QList> transferSpeeds; for (PeerWireClient *client : std::as_const(allClients)) { if (client->state() == QAbstractSocket::ConnectedState && client->availablePieces().count(true) != d->pieceCount) { if (d->state == Seeding) { transferSpeeds.push_back({client->uploadSpeed(), client}); } else { transferSpeeds.push_back({client->downloadSpeed(), client}); } } } std::sort(transferSpeeds.begin(), transferSpeeds.end()); // Unchoke the top 'MaxUploads' downloaders (peers that we are // uploading to) and choke all others. int maxUploaders = MaxUploads; for (auto rit = transferSpeeds.crbegin(), rend = transferSpeeds.crend(); rit != rend; ++rit) { PeerWireClient *client = rit->second; 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 ((QRandomGenerator::global()->bounded(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[QRandomGenerator::global()->bounded(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. for (PeerWireClient *client : std::as_const(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 currentPieces; bool somePiecesAreNotInProgress = false; TorrentPiece *lastPendingPiece = nullptr; QMultiMap::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 = nullptr; 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 values = d->pendingPieces.values(); piece = values.value(QRandomGenerator::global()->bounded(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::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. for (int i : std::as_const(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 partialPieces; QMap::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(QRandomGenerator::global()->bounded(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 || (QRandomGenerator::global()->generate() & 4) == 0) { int *occurrences = new int[d->pieceCount]; memset(occurrences, 0, d->pieceCount * sizeof(int)); // Count how many of each piece are available. for (PeerWireClient *peer : std::as_const(d->connections)) { QBitArray peerPieces = peer->availablePieces(); int peerPiecesSize = peerPieces.size(); for (int i = 0; i < peerPiecesSize; ++i) { if (peerPieces.testBit(i)) ++occurrences[i]; } } // Find the rarest or most common pieces. int numOccurrences = d->state == WarmingUp ? 0 : 99999; QList piecesReadyForDownload; for (int i = 0; i < d->pieceCount; ++i) { if (d->state == WarmingUp) { // Add common pieces if (occurrences[i] >= numOccurrences && incompletePiecesAvailableToClient.testBit(i)) { if (occurrences[i] > numOccurrences) piecesReadyForDownload.clear(); piecesReadyForDownload.append(i); numOccurrences = occurrences[i]; } } else { // Add rare pieces if (occurrences[i] <= numOccurrences && incompletePiecesAvailableToClient.testBit(i)) { if (occurrences[i] < numOccurrences) piecesReadyForDownload.clear(); piecesReadyForDownload.append(i); numOccurrences = occurrences[i]; } } } // Select one piece randomly pieceIndex = piecesReadyForDownload.at(QRandomGenerator::global()->bounded(piecesReadyForDownload.size())); delete [] occurrences; } else { // Make up a list of available piece indices, and pick // a random one. QList values; int incompletePiecesAvailableToClientSize = incompletePiecesAvailableToClient.size(); for (int i = 0; i < incompletePiecesAvailableToClientSize; ++i) { if (incompletePiecesAvailableToClient.testBit(i)) values << i; } pieceIndex = values.at(QRandomGenerator::global()->bounded(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::Iterator it = d->payloads.find(client); int numBlocksInProgress = client->incomingBlocks().size(); QList 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. for (TorrentPiece *piece : std::as_const(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. QList 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 = QRandomGenerator::global()->bounded(bits.size()); int b = QRandomGenerator::global()->bounded(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(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::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(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 &peerList) { // Add peers we don't already know of to our list of peers. const QList addresses = QNetworkInterface::allAddresses(); for (const TorrentPeer &peer : peerList) { if (addresses.contains(peer.address) && peer.port == TorrentServer::instance()->serverPort()) { // Skip our own server. continue; } bool known = false; for (const TorrentPeer *knownPeer : std::as_const(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) { auto tooMany = d->peers.size() - maxPeers; // Find what peers are currently connected & active const auto firstNInactivePeers = [&tooMany, this] (TorrentPeer *peer) { if (!tooMany) return false; for (const PeerWireClient *client : std::as_const(d->connections)) { if (client->peer() == peer && (client->downloadSpeed() + client->uploadSpeed()) > 1024) return false; } --tooMany; return true; }; // Remove inactive peers from the peer list until we're below // the max connections count. d->peers.removeIf(firstNInactivePeers); // If we still have too many peers, remove the oldest ones. d->peers.erase(d->peers.begin(), d->peers.begin() + tooMany); } 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); } }