Started implementing Torrent Peer (download)

This commit is contained in:
Ben Davis
2017-07-16 22:05:43 +01:00
parent 414266804b
commit 913518a420
11 changed files with 846 additions and 10 deletions
+44
View File
@@ -17,6 +17,11 @@
B51638971F0EEC2B009E563E /* GCDAsyncSocketStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */; };
B51D6C091F0C180600E1E3AB /* TorrentUDPTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */; };
B51D6C0A1F0C180D00E1E3AB /* TorrentUDPTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */; };
B527F6271F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B527F6261F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift */; };
B527F6291F1BCAD2001F06AF /* TorrentPeerDelegateStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B527F6281F1BCAD2001F06AF /* TorrentPeerDelegateStub.swift */; };
B527F62B1F1BD5FA001F06AF /* TorrentPieceDownloadBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B527F62A1F1BD5FA001F06AF /* TorrentPieceDownloadBuffer.swift */; };
B527F62D1F1BD64D001F06AF /* TorrentPieceDownloadBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B527F62C1F1BD64D001F06AF /* TorrentPieceDownloadBufferTests.swift */; };
B527F6311F1BF30E001F06AF /* TorrentClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B527F6301F1BF30E001F06AF /* TorrentClient.swift */; };
B52EE3401F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52EE33F1F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift */; };
B52EE3431F129CB000AC22D6 /* TorrentPeerHandshakeMessageBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52EE3421F129CB000AC22D6 /* TorrentPeerHandshakeMessageBufferTests.swift */; };
B53553361F0FEB8800A6DCBE /* TorrentPeerCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53553351F0FEB8800A6DCBE /* TorrentPeerCommunicator.swift */; };
@@ -52,6 +57,8 @@
B585AB841C3833450093FA41 /* BitTorrentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B585AB831C3833450093FA41 /* BitTorrentTests.swift */; };
B59E1B281F0E6EA3007753CE /* BitTorrentTestMacros.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */; };
B5BD7FD61F03032400621BC2 /* TorrentHTTPTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */; };
B5C06F131F12CDD8005730B3 /* TorrentPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C06F121F12CDD8005730B3 /* TorrentPeer.swift */; };
B5C06F151F12CDE1005730B3 /* TorrentPeerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C06F141F12CDE1005730B3 /* TorrentPeerTests.swift */; };
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E977951CAFB46B0038EBE7 /* String+URLEncode.swift */; };
B5E9B0D81F02E6F800EF58E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */; };
B5E9B0DA1F02E6F800EF58E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0D91F02E6F800EF58E3 /* Assets.xcassets */; };
@@ -141,6 +148,11 @@
B51638961F0EEC2B009E563E /* GCDAsyncSocketStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCDAsyncSocketStub.swift; sourceTree = "<group>"; };
B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentUDPTrackerTests.swift; sourceTree = "<group>"; };
B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentUDPTracker.swift; sourceTree = "<group>"; };
B527F6261F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeerCommunicatorStub.swift; sourceTree = "<group>"; };
B527F6281F1BCAD2001F06AF /* TorrentPeerDelegateStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeerDelegateStub.swift; sourceTree = "<group>"; };
B527F62A1F1BD5FA001F06AF /* TorrentPieceDownloadBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPieceDownloadBuffer.swift; sourceTree = "<group>"; };
B527F62C1F1BD64D001F06AF /* TorrentPieceDownloadBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPieceDownloadBufferTests.swift; sourceTree = "<group>"; };
B527F6301F1BF30E001F06AF /* TorrentClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentClient.swift; sourceTree = "<group>"; };
B52EE33F1F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeerHandshakeMessageBuffer.swift; sourceTree = "<group>"; };
B52EE3421F129CB000AC22D6 /* TorrentPeerHandshakeMessageBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeerHandshakeMessageBufferTests.swift; sourceTree = "<group>"; };
B53553351F0FEB8800A6DCBE /* TorrentPeerCommunicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeerCommunicator.swift; sourceTree = "<group>"; };
@@ -179,6 +191,8 @@
B585AB851C3833450093FA41 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BitTorrentTestMacros.swift; path = Utilities/BitTorrentTestMacros.swift; sourceTree = "<group>"; };
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentHTTPTrackerTests.swift; sourceTree = "<group>"; };
B5C06F121F12CDD8005730B3 /* TorrentPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeer.swift; sourceTree = "<group>"; };
B5C06F141F12CDE1005730B3 /* TorrentPeerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeerTests.swift; sourceTree = "<group>"; };
B5E977951CAFB46B0038EBE7 /* String+URLEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "String+URLEncode.swift"; path = "HTTP Networking/String+URLEncode.swift"; sourceTree = "<group>"; };
B5E9B0D51F02E6F800EF58E3 /* BitTorrentExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BitTorrentExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -271,6 +285,23 @@
path = "TCP Networking";
sourceTree = "<group>";
};
B527F62E1F1BDF39001F06AF /* Piece Download Buffer */ = {
isa = PBXGroup;
children = (
B527F62A1F1BD5FA001F06AF /* TorrentPieceDownloadBuffer.swift */,
B527F62C1F1BD64D001F06AF /* TorrentPieceDownloadBufferTests.swift */,
);
path = "Piece Download Buffer";
sourceTree = "<group>";
};
B527F62F1F1BF2EB001F06AF /* Torrent Client */ = {
isa = PBXGroup;
children = (
B527F6301F1BF30E001F06AF /* TorrentClient.swift */,
);
path = "Torrent Client";
sourceTree = "<group>";
};
B54D0C131CA53979004343BD /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -373,6 +404,7 @@
children = (
B585AB771C3833450093FA41 /* BitTorrent.h */,
B585AB831C3833450093FA41 /* BitTorrentTests.swift */,
B527F62F1F1BF2EB001F06AF /* Torrent Client */,
B5F81E461F0436CC00B25C70 /* Peer */,
B55317DA1F02FC3000909ADF /* Tracker */,
B54D0C251CA56A25004343BD /* Models */,
@@ -399,6 +431,7 @@
isa = PBXGroup;
children = (
B53553351F0FEB8800A6DCBE /* TorrentPeerCommunicator.swift */,
B527F6261F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift */,
B53553381F0FEB9300A6DCBE /* TorrentPeerCommunicatorTests.swift */,
B53553411F12432A00A6DCBE /* TorrentPeerCommunicatorReadTests.swift */,
B53553441F1250E300A6DCBE /* TorrentPeerCommunicatorDelegateStub.swift */,
@@ -453,6 +486,10 @@
B5F81E461F0436CC00B25C70 /* Peer */ = {
isa = PBXGroup;
children = (
B5C06F121F12CDD8005730B3 /* TorrentPeer.swift */,
B527F6281F1BCAD2001F06AF /* TorrentPeerDelegateStub.swift */,
B5C06F141F12CDE1005730B3 /* TorrentPeerTests.swift */,
B527F62E1F1BDF39001F06AF /* Piece Download Buffer */,
B5C06F101F12C2ED005730B3 /* Communicator */,
);
path = Peer;
@@ -808,11 +845,14 @@
buildActionMask = 2147483647;
files = (
B5F81E491F0436D600B25C70 /* TorrentPeerInfo.swift in Sources */,
B527F6311F1BF30E001F06AF /* TorrentClient.swift in Sources */,
B55317DC1F02FC4D00909ADF /* TorrentHTTPTracker.swift in Sources */,
B52EE3401F129CA200AC22D6 /* TorrentPeerHandshakeMessageBuffer.swift in Sources */,
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */,
B558F4831F0A647D00438BB4 /* InternetProtocol.swift in Sources */,
B527F62B1F1BD5FA001F06AF /* TorrentPieceDownloadBuffer.swift in Sources */,
B54D0C7A1CA69FAD004343BD /* TorrentMetaInfo.swift in Sources */,
B5C06F131F12CDD8005730B3 /* TorrentPeer.swift in Sources */,
B5F81E4B1F04399800B25C70 /* TorrentTrackerResponse.swift in Sources */,
B54D0C7B1CA69FD8004343BD /* Data+sha1.swift in Sources */,
B53553361F0FEB8800A6DCBE /* TorrentPeerCommunicator.swift in Sources */,
@@ -831,6 +871,7 @@
files = (
B535533A1F0FEB9700A6DCBE /* TorrentPeerCommunicatorTests.swift in Sources */,
B51638971F0EEC2B009E563E /* GCDAsyncSocketStub.swift in Sources */,
B5C06F151F12CDE1005730B3 /* TorrentPeerTests.swift in Sources */,
B51D6C091F0C180600E1E3AB /* TorrentUDPTrackerTests.swift in Sources */,
B537CF061F03148B0084089B /* HTTPConnectionStub.swift in Sources */,
B53553451F1250E300A6DCBE /* TorrentPeerCommunicatorDelegateStub.swift in Sources */,
@@ -841,11 +882,14 @@
B535533C1F0FEE6300A6DCBE /* TCPConnectionStub.swift in Sources */,
B52EE3431F129CB000AC22D6 /* TorrentPeerHandshakeMessageBufferTests.swift in Sources */,
B535534C1F125DB500A6DCBE /* TorrentPeerMessageBufferTests.swift in Sources */,
B527F6271F1BBC6B001F06AF /* TorrentPeerCommunicatorStub.swift in Sources */,
B56A8A071C83539300426AC8 /* TestHelpers.swift in Sources */,
B585AB841C3833450093FA41 /* BitTorrentTests.swift in Sources */,
B53553401F112BDB00A6DCBE /* BitFieldTests.swift in Sources */,
B54D0C271CA56ADB004343BD /* TorrentMetaInfoTests.swift in Sources */,
B527F62D1F1BD64D001F06AF /* TorrentPieceDownloadBufferTests.swift in Sources */,
B51638951F0EEAA4009E563E /* TCPConnectionTests.swift in Sources */,
B527F6291F1BCAD2001F06AF /* TorrentPeerDelegateStub.swift in Sources */,
B55317E01F02FE1500909ADF /* URLEncodeTests.swift in Sources */,
B558F4851F0A73D000438BB4 /* InternetProtocolTests.swift in Sources */,
B50B24F71F0A553F00C23E7C /* UDPConnectionTests.swift in Sources */,
+1 -1
View File
@@ -102,7 +102,7 @@ class TorrentInfoDictionary {
let pieceLength : Int
let isPrivate : Bool
let files: [TorrentFileInfo]
let pieces : [Data]?
let pieces : [Data]
let length: Int
init?(_ dictionary: [String : AnyObject]) {
+7
View File
@@ -52,3 +52,10 @@ struct TorrentPeerInfo {
return result
}
}
extension TorrentPeerInfo: Equatable {
static func ==(_ lhs: TorrentPeerInfo, _ rhs: TorrentPeerInfo) -> Bool {
let peerIdsMatch = (lhs.peerId == nil || rhs.peerId == nil || lhs.peerId == rhs.peerId)
return (lhs.ip == rhs.ip && lhs.port == rhs.port && peerIdsMatch)
}
}
@@ -31,6 +31,8 @@ protocol TorrentPeerCommunicatorDelegate: class {
/// Responsible for sending and recieving messages in the Peer Wire Protocol
class TorrentPeerCommunicator {
var enableLogging = false
enum Message: UInt8 {
case choke = 0
case unchoke = 1
@@ -42,6 +44,31 @@ class TorrentPeerCommunicator {
case piece = 7
case cancel = 8
case port = 9
var stringValue: String {
switch self {
case .choke:
return "choke"
case .unchoke:
return "unchoke"
case .interested:
return "interested"
case .notInterested:
return "notInterested"
case .have:
return "have"
case .bitfield:
return "bitfield"
case .request:
return "request"
case .piece:
return "piece"
case .cancel:
return "cancel"
case .port:
return "port"
}
}
}
let defaultTimeout: TimeInterval = 10
@@ -51,13 +78,16 @@ class TorrentPeerCommunicator {
private let peerInfo: TorrentPeerInfo
private let connection: TCPConnectionProtocol
var handshakeReceived = false
fileprivate let infoHash: Data
fileprivate var handshakeReceived = false
fileprivate let handshakeMessageBuffer: TorrentPeerHandshakeMessageBuffer
fileprivate let messageBuffer: TorrentPeerMessageBuffer
init(peerInfo: TorrentPeerInfo, infoHash: Data, tcpConnection: TCPConnectionProtocol = TCPConnection()) {
self.peerInfo = peerInfo
self.connection = tcpConnection
self.infoHash = infoHash
self.handshakeMessageBuffer = TorrentPeerHandshakeMessageBuffer(infoHash: infoHash, peerId: peerInfo.peerId)
self.messageBuffer = TorrentPeerMessageBuffer()
@@ -71,13 +101,12 @@ class TorrentPeerCommunicator {
func connect() throws {
try connection.connect(to: peerInfo.ip, onPort: peerInfo.port)
}
}
// MARK: - Writing messages
extension TorrentPeerCommunicator {
func sendHandshake(for infoHash: Data, clientId: Data, _ completion: (()->Void)? = nil) {
// MARK: - Writing messages
func sendHandshake(for clientId: Data, _ completion: (()->Void)? = nil) {
if enableLogging { print("Send handshake") }
let protocolString = "BitTorrent protocol"
let protocolStringLength = UInt8(protocolString.count)
@@ -93,55 +122,85 @@ extension TorrentPeerCommunicator {
}
func sendKeepAlive(_ completion: (()->Void)? = nil) {
if enableLogging { print("sendKeepAlive") }
let keepAlivePayload = Data(bytes: [0, 0, 0, 0]) // 0 length message
connection.write(keepAlivePayload, withTimeout: defaultTimeout, completion: completion)
}
func sendChoke(_ completion: (()->Void)? = nil) {
if enableLogging { print("Send choke") }
let payload = makePayload(forMessage: .choke)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendUnchoke(_ completion: (()->Void)? = nil) {
if enableLogging { print("Send Unchoke") }
let payload = makePayload(forMessage: .unchoke)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendInterested(_ completion: (()->Void)? = nil) {
if enableLogging { print("Send Interested") }
let payload = makePayload(forMessage: .interested)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendNotInterested(_ completion: (()->Void)? = nil) {
if enableLogging { print("Send not interested") }
let payload = makePayload(forMessage: .notInterested)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendHavePiece(at index: Int, _ completion: (()->Void)? = nil) {
if enableLogging { print("Send have piece") }
let data = UInt32(index).toData()
let payload = makePayload(forMessage: .have, data: data)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendBitField(_ bitField: BitField, _ completion: (()->Void)? = nil) {
if enableLogging { print("Send Bit Field") }
let data = bitField.toData()
let payload = makePayload(forMessage: .bitfield, data: data)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendRequest(fromPieceAtIndex index: Int, begin: Int, length: Int, _ completion: (()->Void)? = nil) {
if enableLogging { print("Send Request for piece at index: \(index) begin: \(begin)") }
let data = UInt32(index).toData() + UInt32(begin).toData() + UInt32(length).toData()
let payload = makePayload(forMessage: .request, data: data)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendPiece(fromPieceAtIndex index: Int, begin: Int, block: Data, _ completion: (()->Void)? = nil) {
if enableLogging { print("Send Piece index: \(index) begin: \(begin)") }
let data = UInt32(index).toData() + UInt32(begin).toData() + block
let payload = makePayload(forMessage: .piece, data: data)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
}
func sendCancel(forPieceAtIndex index: Int, begin: Int, length: Int, _ completion: (()->Void)? = nil) {
if enableLogging { print("Send Cancel for piece at index: \(index) begin: \(begin)") }
let data = UInt32(index).toData() + UInt32(begin).toData() + UInt32(length).toData()
let payload = makePayload(forMessage: .cancel, data: data)
connection.write(payload, withTimeout: defaultTimeout, completion: completion)
@@ -211,7 +270,6 @@ extension TorrentPeerCommunicator: TorrentPeerHandshakeDelegate {
}
}
// TODO: test can send handshake + message
extension TorrentPeerCommunicator: TorrentPeerMessageBufferDelegate {
func peerMessageBuffer(_ sender: TorrentPeerMessageBuffer, gotMessage data: Data) {
@@ -226,6 +284,8 @@ extension TorrentPeerCommunicator: TorrentPeerMessageBufferDelegate {
return
}
if enableLogging { print("Got Message from peer \(message.stringValue)") }
switch message {
case .choke:
@@ -0,0 +1,46 @@
//
// TorrentPeerCommunicatorStub.swift
// BitTorrentTests
//
// Created by Ben Davis on 16/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
@testable import BitTorrent
class TorrentPeerCommunicatorStub: TorrentPeerCommunicator {
var connectCalled = false
override func connect() throws {
connectCalled = true
}
var sendHandshakeCalled = false
var sendHandshakeParameters: (clientId: Data, completion: (()->Void)?)?
override func sendHandshake(for clientId: Data, _ completion: (() -> Void)?) {
sendHandshakeCalled = true
sendHandshakeParameters = (clientId, completion)
}
var sendBitFieldCalled = false
var sendBitFieldParameters: (bitField: BitField, completion: (()->Void)?)?
override func sendBitField(_ bitField: BitField, _ completion: (() -> Void)?) {
sendBitFieldCalled = true
sendBitFieldParameters = (bitField, completion)
}
var sendInterestedCalled = false
var sendInterestedParameter: ((()->Void)?)?
override func sendInterested(_ completion: (() -> Void)?) {
sendInterestedCalled = true
sendInterestedParameter = completion
}
var sendRequestCalled = false
var sendRequestParameters: [(index: Int, begin: Int, length: Int, completion:(()->Void)?)] = []
override func sendRequest(fromPieceAtIndex index: Int, begin: Int, length: Int, _ completion: (() -> Void)?) {
sendRequestCalled = true
sendRequestParameters.append((index, begin, length, completion))
}
}
@@ -15,6 +15,8 @@ class TorrentPeerComminicatorTests: XCTestCase {
var delegate: TorrentPeerCommunicatorDelegateStub!
var sut: TorrentPeerCommunicator!
// MARK: - Models and example data
let ip = "127.0.0.1"
let port: UInt16 = 123
let peerId = "-BD0000-bxa]N#IRKqv`".data(using: .ascii)!
@@ -94,6 +96,8 @@ class TorrentPeerComminicatorTests: XCTestCase {
UInt32(length).toData() // length
}
// MARK: - Setup
override func setUp() {
super.setUp()
@@ -107,6 +111,8 @@ class TorrentPeerComminicatorTests: XCTestCase {
sut.delegate = delegate
}
// Mark - Tests
func test_canConnect() {
try! sut.connect()
@@ -128,7 +134,7 @@ class TorrentPeerComminicatorTests: XCTestCase {
}
func test_sendHandshake() {
sut.sendHandshake(for: infoHash, clientId: peerId)
sut.sendHandshake(for: peerId)
assertDataSent(handshakePayload)
}
@@ -0,0 +1,78 @@
//
// PieceDownloadBuffer.swift
// BitTorrent
//
// Created by Ben Davis on 16/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
struct TorrentBlockRequest: Equatable {
var piece: Int
var begin: Int
var length: Int
static func ==(_ lhs: TorrentBlockRequest, _ rhs: TorrentBlockRequest) -> Bool {
return (lhs.piece == rhs.piece && lhs.begin == rhs.begin && lhs.length == rhs.length)
}
}
class TorrentPieceDownloadBuffer {
static let blockSize = 16_384
let index: Int
let size: Int
var isComplete: Bool {
return unusedBlockRequests.count == 0 && pendingRequests.count == 0
}
var piece: Data? {
return isComplete ? data : nil
}
private var data: Data
private var unusedBlockRequests: [TorrentBlockRequest]
private var pendingRequests: [TorrentBlockRequest] = []
init(index: Int, size: Int) {
self.index = index
self.size = size
self.data = Data(repeating: 0, count: size)
let blockSize = TorrentPieceDownloadBuffer.blockSize
var blockRequests: [TorrentBlockRequest] = []
for i in 0..<size where i % blockSize == 0 {
// Last block should be the remaining bytes
let length: Int = ((i + blockSize) <= size) ? blockSize : (size - i)
blockRequests.append(TorrentBlockRequest(piece: index, begin: i, length: length))
}
self.unusedBlockRequests = blockRequests
}
func nextDownloadBlock() -> TorrentBlockRequest? {
guard unusedBlockRequests.count > 0 else { return nil }
let result = unusedBlockRequests.removeLast()
pendingRequests.append(result)
return result
}
func gotBlock(_ blockData: Data, begin: Int) {
let request = TorrentBlockRequest(piece: index, begin: begin, length: blockData.count)
guard let pendingIndex = pendingRequests.index(of: request) else { return }
let range = begin ..< (begin+blockData.count)
data.replaceSubrange(range, with: blockData)
pendingRequests.remove(at: pendingIndex)
}
}
@@ -0,0 +1,95 @@
//
// TorrentPieceDownloadBufferTests.swift
// BitTorrentTests
//
// Created by Ben Davis on 16/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import XCTest
@testable import BitTorrent
class TorrentPieceDownloadBufferTests: XCTestCase {
let index = 123
let blockSize = TorrentPieceDownloadBuffer.blockSize
func test_creation() {
let size = 234
let sut = TorrentPieceDownloadBuffer(index: index, size: size)
XCTAssertEqual(sut.index, index)
XCTAssertEqual(sut.size, size)
}
func test_splitsPieceIntoBlockRequests() {
let size: Int = Int(Double(blockSize)*2.5)
let sut = TorrentPieceDownloadBuffer(index: index, size: size)
let block1 = sut.nextDownloadBlock()
let block2 = sut.nextDownloadBlock()
let block3 = sut.nextDownloadBlock()
let block4 = sut.nextDownloadBlock()
XCTAssertNil(block4)
let sortedBlocks = [block1!, block2!, block3!].sorted(by: { $0.begin < $1.begin })
XCTAssertEqual(sortedBlocks, [
TorrentBlockRequest(piece: index, begin: 0, length: blockSize),
TorrentBlockRequest(piece: index, begin: blockSize, length: blockSize),
TorrentBlockRequest(piece: index, begin: blockSize*2, length: Int(Double(blockSize)*0.5)),
])
}
func test_isComplete() {
let size: Int = Int(Double(blockSize)*2.5)
let sut = TorrentPieceDownloadBuffer(index: index, size: size)
XCTAssertFalse(sut.isComplete)
_ = sut.nextDownloadBlock()
_ = sut.nextDownloadBlock()
_ = sut.nextDownloadBlock()
XCTAssertFalse(sut.isComplete)
let data1 = Data(repeating: 0, count: blockSize)
let data2 = Data(repeating: 0, count: blockSize)
let data3 = Data(repeating: 0, count: Int(Double(blockSize)*0.5))
sut.gotBlock(data1, begin: 0)
XCTAssertFalse(sut.isComplete)
sut.gotBlock(data2, begin: blockSize)
XCTAssertFalse(sut.isComplete)
sut.gotBlock(data3, begin: blockSize*2)
XCTAssertTrue(sut.isComplete)
}
func test_resultingData() {
let size: Int = Int(Double(blockSize)*2.5)
let sut = TorrentPieceDownloadBuffer(index: index, size: size)
XCTAssertNil(sut.piece)
_ = sut.nextDownloadBlock()
_ = sut.nextDownloadBlock()
_ = sut.nextDownloadBlock()
XCTAssertNil(sut.piece)
let data1 = Data(repeating: 1, count: blockSize)
let data2 = Data(repeating: 2, count: blockSize)
let data3 = Data(repeating: 3, count: Int(Double(blockSize)*0.5))
let complete = data1 + data2 + data3
sut.gotBlock(data1, begin: 0)
XCTAssertNil(sut.piece)
sut.gotBlock(data2, begin: blockSize)
XCTAssertNil(sut.piece)
sut.gotBlock(data3, begin: blockSize*2)
XCTAssertEqual(sut.piece, complete)
}
}
+184
View File
@@ -0,0 +1,184 @@
//
// TorrentPeer.swift
// BitTorrent
//
// Created by Ben Davis on 09/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
protocol TorrentPeerDelegate: class {
func peerCompletedHandshake(_ sender: TorrentPeer)
func peerLost(_ sender: TorrentPeer)
func peer(_ sender: TorrentPeer, gotPieceAtIndex index: Int, piece: Data)
func peer(_ sender: TorrentPeer, failedToGetPieceAtIndex index: Int)
}
class TorrentPeer {
var enableLogging = false {
didSet {
communicator.enableLogging = enableLogging
}
}
static let maximumNumberOfPendingBlockRequests = 20
weak var delegate: TorrentPeerDelegate?
let peerInfo: TorrentPeerInfo
private let communicator: TorrentPeerCommunicator
// Peer state
var peerChoked: Bool { return _peerChoked }
var peerInterested: Bool { return _peerInterested }
var amChokedToPeer: Bool { return _amChokedToPeer }
var amInterestedInPeer: Bool { return _amInterestedInPeer }
var currentProgress: BitField { return _currentProgress }
// Private state
private var _peerChoked: Bool = true
private var _peerInterested: Bool = false
private var _amChokedToPeer: Bool = true
private var _amInterestedInPeer: Bool = false
private var _currentProgress: BitField
private var downloadPieceRequests: [Int: TorrentPieceDownloadBuffer] = [:]
private var numberOfPendingRequests = 0
init(peerInfo: TorrentPeerInfo, bitFieldSize: Int, communicator: TorrentPeerCommunicator) {
self.peerInfo = peerInfo
self.communicator = communicator
self._currentProgress = BitField(size: bitFieldSize)
communicator.delegate = self
}
convenience init(peerInfo: TorrentPeerInfo, infoHash: Data, bitFieldSize: Int) {
let communicator = TorrentPeerCommunicator(peerInfo: peerInfo, infoHash: infoHash)
self.init(peerInfo: peerInfo, bitFieldSize: bitFieldSize, communicator: communicator)
}
private var handshakeData: (clientId: Data, bitField: BitField)?
func connect(withHandshakeData handshakeData:(clientId: Data, bitField: BitField)) throws {
self.handshakeData = handshakeData
try communicator.connect()
}
func downloadPiece(atIndex index: Int, size: Int) {
let downloadBuffer = TorrentPieceDownloadBuffer(index: index, size: size)
downloadPieceRequests[index] = downloadBuffer
if !amInterestedInPeer {
_amInterestedInPeer = true
communicator.sendInterested()
}
if !peerChoked {
requestNextBlock()
}
}
private func requestNextBlock() {
if numberOfPendingRequests < TorrentPeer.maximumNumberOfPendingBlockRequests {
guard let pieceRequest = downloadPieceRequests.values.first else { return }
guard let blockRequest = pieceRequest.nextDownloadBlock() else { return }
numberOfPendingRequests += 1
communicator.sendRequest(fromPieceAtIndex: blockRequest.piece,
begin: blockRequest.begin,
length: blockRequest.length)
requestNextBlock()
}
}
private func killAllDownloads() {
for downloadPieceRequest in downloadPieceRequests {
delegate?.peer(self, failedToGetPieceAtIndex: downloadPieceRequest.value.index)
}
downloadPieceRequests.removeAll()
}
}
extension TorrentPeer: TorrentPeerCommunicatorDelegate {
func peerConnected(_ sender: TorrentPeerCommunicator) {
if enableLogging { print("Peer socket connected (\(peerInfo.ip):\(peerInfo.port)") }
guard let (clientId, bitField) = handshakeData else { return }
communicator.sendHandshake(for: clientId) { [weak self] in
self?.communicator.sendBitField(bitField)
}
}
func peerLost(_ sender: TorrentPeerCommunicator) {
killAllDownloads()
delegate?.peerLost(self)
}
func peerSentHandshake(_ sender: TorrentPeerCommunicator, sentHandshakeWithPeerId peerId: Data, onDHT: Bool) {
delegate?.peerCompletedHandshake(self)
}
func peerSentKeepAlive(_ sender: TorrentPeerCommunicator) {
}
func peerBecameChoked(_ sender: TorrentPeerCommunicator) {
_peerChoked = true
killAllDownloads()
}
func peerBecameUnchoked(_ sender: TorrentPeerCommunicator) {
_peerChoked = false
requestNextBlock()
}
func peerBecameInterested(_ sender: TorrentPeerCommunicator) {
_peerInterested = true
}
func peerBecameUninterested(_ sender: TorrentPeerCommunicator) {
_peerInterested = false
}
func peer(_ sender: TorrentPeerCommunicator, hasPiece piece: Int) {
_currentProgress.set(at: piece)
}
func peer(_ sender: TorrentPeerCommunicator, hasBitField bitField: BitField) {
_currentProgress = bitField
}
func peer(_ sender: TorrentPeerCommunicator, requestedPiece index: Int, begin: Int, length: Int) {
}
func peer(_ sender: TorrentPeerCommunicator, sentPiece index: Int, begin: Int, block: Data) {
guard let downloadPieceBuffer = downloadPieceRequests[index] else { return }
numberOfPendingRequests -= 1
downloadPieceBuffer.gotBlock(block, begin: begin)
if downloadPieceBuffer.isComplete, let piece = downloadPieceBuffer.piece {
delegate?.peer(self, gotPieceAtIndex: index, piece: piece)
}
requestNextBlock()
}
func peer(_ sender: TorrentPeerCommunicator, cancelledRequestedPiece index: Int, begin: Int, length: Int) {
}
func peer(_ sender: TorrentPeerCommunicator, onDHTPort port: Int) {
}
func peerSentMalformedMessage(_ sender: TorrentPeerCommunicator) {
}
}
@@ -0,0 +1,40 @@
//
// TorrentPeerDelegateStub.swift
// BitTorrentTests
//
// Created by Ben Davis on 16/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
@testable import BitTorrent
class TorrentPeerDelegateStub: TorrentPeerDelegate {
var peerCompletedHandshakeCalled = false
var peerCompletedHandshakeParameter: TorrentPeer?
func peerCompletedHandshake(_ sender: TorrentPeer) {
peerCompletedHandshakeCalled = true
peerCompletedHandshakeParameter = sender
}
var peerLostCalled = false
var peerLostParameter: TorrentPeer?
func peerLost(_ sender: TorrentPeer) {
peerLostCalled = true
peerLostParameter = sender
}
var failedToGetPieceAtIndexCalled = false
var failedToGetPieceAtIndexParameters: (sender: TorrentPeer, index: Int)?
func peer(_ sender: TorrentPeer, failedToGetPieceAtIndex index: Int) {
failedToGetPieceAtIndexCalled = true
failedToGetPieceAtIndexParameters = (sender, index)
}
var gotPieceAtIndexCalled = false
var gotPieceAtIndexParameters: (sender: TorrentPeer, index: Int, piece: Data)?
func peer(_ sender: TorrentPeer, gotPieceAtIndex index: Int, piece: Data) {
gotPieceAtIndexCalled = true
gotPieceAtIndexParameters = (sender, index, piece)
}
}
+276
View File
@@ -0,0 +1,276 @@
//
// TorrentPeerTests.swift
// BitTorrentTests
//
// Created by Ben Davis on 09/07/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import XCTest
@testable import BitTorrent
class TorrentPeerTests: XCTestCase {
let ip = "127.0.0.1"
let port: UInt16 = 123
let peerId = Data(repeating: 1, count: 20)
let clientId = Data(repeating: 2, count: 20)
let infoHash = Data(repeating: 3, count: 20)
let pieceSize = Int(Double(TorrentPieceDownloadBuffer.blockSize)*2.5)
let pieceIndex = 123
let bitField: BitField = {
var bitField = BitField(size: 10)
bitField.set(at: 2)
bitField.set(at: 5)
bitField.set(at: 9)
return bitField
}()
var delegate: TorrentPeerDelegateStub!
var communicator: TorrentPeerCommunicatorStub!
var peerInfo: TorrentPeerInfo!
var sut: TorrentPeer!
override func setUp() {
super.setUp()
peerInfo = TorrentPeerInfo(ip: ip, port: port, peerId: peerId)
communicator = TorrentPeerCommunicatorStub(peerInfo: peerInfo,
infoHash: infoHash,
tcpConnection: TCPConnectionStub())
delegate = TorrentPeerDelegateStub()
sut = TorrentPeer(peerInfo: peerInfo, bitFieldSize: 10, communicator: communicator)
sut.delegate = delegate
}
func test_creation() {
XCTAssertEqual(sut.peerInfo, peerInfo)
XCTAssertTrue(sut.peerChoked)
XCTAssertFalse(sut.peerInterested)
XCTAssertTrue(sut.amChokedToPeer)
XCTAssertFalse(sut.amInterestedInPeer)
}
// MARK: - Connection + handshake
func test_canConnectToPeer() {
try! sut.connect(withHandshakeData: (clientId, bitField))
XCTAssert(communicator.connectCalled)
}
func test_handshakeSentOnConnect() {
try! sut.connect(withHandshakeData: (clientId, bitField))
communicator.delegate?.peerConnected(communicator)
XCTAssert(communicator.sendHandshakeCalled)
XCTAssertEqual(communicator.sendHandshakeParameters?.clientId, clientId)
}
func test_bitFieldSentAfterHandshake() {
// Given
try! sut.connect(withHandshakeData: (clientId, bitField))
communicator.delegate?.peerConnected(communicator)
guard let handshakeCompletion = communicator.sendHandshakeParameters?.completion else {
XCTFail("Cannot notify sut of handshake completion")
return
}
// When
handshakeCompletion()
// Then
XCTAssert(communicator.sendBitFieldCalled)
XCTAssertEqual(communicator.sendBitFieldParameters?.bitField, bitField)
}
func test_delegateNotifiedAfterHandshakeMade() {
try! sut.connect(withHandshakeData: (clientId, bitField))
communicator.delegate?.peerConnected(communicator)
communicator.delegate?.peerSentHandshake(communicator, sentHandshakeWithPeerId: peerId, onDHT: false)
XCTAssert(delegate.peerCompletedHandshakeCalled)
XCTAssert(delegate.peerCompletedHandshakeParameter === sut)
}
// MARK: - Tracking peer status
func test_bitFieldRecorded() {
var bitField = BitField(size: 10)
bitField.set(at: 0)
communicator.delegate?.peer(communicator, hasBitField: bitField)
XCTAssertEqual(sut.currentProgress, bitField)
}
func test_bitFieldUpdatedOnHave() {
var bitField = BitField(size: 10)
bitField.set(at: 0)
bitField.set(at: 3)
communicator.delegate?.peer(communicator, hasPiece: 0)
communicator.delegate?.peer(communicator, hasPiece: 3)
XCTAssertEqual(sut.currentProgress, bitField)
}
func test_stateUpdatedOnPeerUnchoked() {
communicator.delegate?.peerBecameUnchoked(communicator)
XCTAssertFalse(sut.peerChoked)
}
func test_stateUpdatedOnPeerChoked() {
communicator.delegate?.peerBecameUnchoked(communicator)
communicator.delegate?.peerBecameChoked(communicator)
XCTAssertTrue(sut.peerChoked)
}
func test_stateUpdatedOnPeerInterested() {
communicator.delegate?.peerBecameInterested(communicator)
XCTAssertTrue(sut.peerInterested)
}
func test_stateUpdatedOnPeerUninterested() {
communicator.delegate?.peerBecameInterested(communicator)
communicator.delegate?.peerBecameUninterested(communicator)
XCTAssertFalse(sut.peerInterested)
}
func test_interestedSentOnDownloadPieceRequest() {
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
XCTAssert(communicator.sendInterestedCalled)
XCTAssertTrue(sut.amInterestedInPeer)
}
func test_interestedNotSentIfAlreadyIntereseted() {
// Given
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
communicator.sendInterestedCalled = false
// When
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
// Then
XCTAssertFalse(communicator.sendInterestedCalled)
}
// MARK: - Piece download requests
func test_requestNotMadeIfPeerIsChoked() {
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
XCTAssertFalse(communicator.sendRequestCalled)
}
func test_requestsMadeImmediatelyIfPeerIsUnchoked() {
communicator.delegate?.peerBecameUnchoked(communicator)
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
XCTAssertTrue(communicator.sendRequestCalled)
}
func test_sendPieceRequestOnUnchoke() {
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
communicator.delegate?.peerBecameUnchoked(communicator)
XCTAssertTrue(communicator.sendRequestCalled)
}
func test_correctBlockRequestsSent() {
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
communicator.delegate?.peerBecameUnchoked(communicator)
let blockSize = TorrentPieceDownloadBuffer.blockSize
let requests = communicator.sendRequestParameters.sorted(by: { $0.begin < $1.begin }).map {
TorrentBlockRequest(piece: $0.index, begin: $0.begin, length: $0.length)
}
let expected = [
TorrentBlockRequest(piece: pieceIndex, begin: 0, length: blockSize),
TorrentBlockRequest(piece: pieceIndex, begin: blockSize, length: blockSize),
TorrentBlockRequest(piece: pieceIndex, begin: blockSize*2, length: Int(Double(blockSize)*0.5)),
]
XCTAssertEqual(requests, expected)
}
func test_doesNotDownloadMoreThanMaximumNumberOfRequests() {
let largePieceSize = TorrentPieceDownloadBuffer.blockSize * (TorrentPeer.maximumNumberOfPendingBlockRequests + 1)
sut.downloadPiece(atIndex: pieceIndex, size: largePieceSize)
communicator.delegate?.peerBecameUnchoked(communicator)
XCTAssertEqual(communicator.sendRequestParameters.count, TorrentPeer.maximumNumberOfPendingBlockRequests)
}
func test_nextRequestMadeOnRecievingBlock() {
let largePieceSize = TorrentPieceDownloadBuffer.blockSize * (TorrentPeer.maximumNumberOfPendingBlockRequests + 1)
sut.downloadPiece(atIndex: pieceIndex, size: largePieceSize)
communicator.delegate?.peerBecameUnchoked(communicator)
guard let request = communicator.sendRequestParameters.first else { return }
communicator.sendRequestParameters = []
communicator.delegate?.peer(communicator,
sentPiece: request.index,
begin: request.begin,
block: Data(repeating: 0, count: request.length))
XCTAssertEqual(communicator.sendRequestParameters.count, 1)
}
func test_delegateNotifiedFailedToGetPiece_whenPeerChokes() {
communicator.delegate?.peerBecameUnchoked(communicator)
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
communicator.delegate?.peerBecameChoked(communicator)
XCTAssert(delegate.failedToGetPieceAtIndexCalled)
XCTAssert(delegate.failedToGetPieceAtIndexParameters?.sender === sut)
XCTAssertEqual(delegate.failedToGetPieceAtIndexParameters?.index, pieceIndex)
}
func test_delegateNotifiedOnSuccessfulPieceDownload() {
communicator.delegate?.peerBecameUnchoked(communicator)
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
let requests = communicator.sendRequestParameters.sorted(by: { $0.begin < $1.begin })
var expectedResult: Data = Data()
var i: UInt8 = 1
for request in requests {
let block = Data(repeating: i, count: request.length)
communicator.delegate?.peer(communicator,
sentPiece: request.index,
begin: request.begin,
block: block)
i += 1
expectedResult += block
}
XCTAssert(delegate.gotPieceAtIndexCalled)
XCTAssert(delegate.gotPieceAtIndexParameters?.sender === sut)
XCTAssertEqual(delegate.gotPieceAtIndexParameters?.index, pieceIndex)
XCTAssertEqual(delegate.gotPieceAtIndexParameters?.piece, expectedResult)
}
// MARK: - Peer connection lost
func test_delegateNotifiedOnPeerLost() {
communicator.delegate?.peerLost(communicator)
XCTAssert(delegate.peerLostCalled)
XCTAssert(delegate.peerLostParameter === sut)
}
func test_delegateNotifiedFailedToGetPiece_whenPeerLost() {
communicator.delegate?.peerBecameUnchoked(communicator)
sut.downloadPiece(atIndex: pieceIndex, size: pieceSize)
communicator.delegate?.peerLost(communicator)
XCTAssert(delegate.failedToGetPieceAtIndexCalled)
XCTAssert(delegate.failedToGetPieceAtIndexParameters?.sender === sut)
XCTAssertEqual(delegate.failedToGetPieceAtIndexParameters?.index, pieceIndex)
}
// MARK: -
}