Building a BitTorrent client from scratch in C#
cheatdeath.github.io
cheatdeath.github.io
One issue to be mindful of- the HttpWebRequest.BeginGetResponse method does not honor timeouts, and you are on your own to timeout the attempt. Consider using HttpClient, if available in Mono / .NET Core. Otherwise, see MSDN for how to do this:
"In the case of asynchronous requests, it is the responsibility of the client application to implement its own time-out mechanism. The following code example shows how to do it." See: https://msdn.microsoft.com/en-us/library/system.net.httpwebr...
I'm not sure if you have access to the ThreadPool class. In a bug that Microsoft's library had, I used the TPL Task construct to resolve this. See the pull request here: https://github.com/Microsoft/ProjectOxford-ClientSDK/pull/83...
If anyone who hasn't tried doing this before, the "official" BitTorrent spec docs, namely BEP-3 (http://bittorrent.org/beps/bep_0003.html), seem little more than a vague blog post turned in to a "spec". However, somewhat conversely, this has lead to is a wealth of articles describing how to do it.
The three guides I used were:
- A 2 part blog post which has a bit of a Python bent http://www.kristenwidman.com/blog/33/how-to-write-a-bittorre...
- The unofficial specs https://wiki.theory.org/BitTorrentSpecification, and
- An incomplete Python client https://github.com/JosephSalisbury/python-bittorrent
I didn't know of the RFC mentioned in the post, that would have also been really useful.
A lot of BitTorrent stuff for Python is remarkably hard to find in all the noise of Deluge, the original client, and libtorrent wrappers, but none that existed were sophisticated (or at least well documented) enough for my experiments, they have different focuses.
I never went as far as implementing my own BEncoder library, a billion seem to exist in multiple languages and install any BitTorrent Python library and it seems to come with their own copy. (I suspect due to the way BEncoder was bundled in the original client, see: https://pypi.python.org/pypi/bencode)
I also found a Rust implementation which seems not to compile, but is useful as I'm trying to teach myself Rust https://github.com/kenpratt/rusty_torrent I think the work to get it to compile might be minimal.
I agree. I don't do C# but mostly can follow it. It also is well-organized presentation of much of a protocol all kinds of people keep re-implementing. They need the help more often than not. A great write-up.
There is also another project in Rust, it looks more active: https://github.com/GGist/bip-rs It is a collection of libraries.
> If anyone who hasn't tried doing this before, the "official" BitTorrent spec docs, namely BEP-3 (http://bittorrent.org/beps/bep_0003.html), seem little more than a vague blog post turned in to a "spec".
Doesn't look vague at all. What do you think is missing from it?
Thanks, I had seen that one, but forgot about it. I think it's a great project, but it's really just a collection of libraries that don't really tell you how it all fits together, which when I was picking stuff up wasn't very helpful. Hopefully now I have a better understanding of the client design I can make something from that.
> Doesn't look vague at all. What do you think is missing from it?
For a comparison I would recommend reading a few (what I would consider) good protocol docs. Docs that you could read and implement, and probably get working very quickly, for example:
- XMPP's XEPs (one picked for similarity in usage to BitTorrent) https://xmpp.org/extensions/xep-0020.html - Lots of examples in there for what messages should look like, which is always helpful.
- The BitTorrent RFC doc (linked in the original post) http://jonas.nitro.dk/bittorrent/bittorrent-rfc.html - Sums the situation up nicely with the layout of messages and value lengths.
I think the main thing that makes the biggest difference is adhering to a language spec such as RFC 2119 which recommends using "MUST", "SHALL", "REQUIRED"; "MUST NOT", "SHALL NOT"; "SHOULD", etc. which makes it really clear what you're meant to do or not to.
Specifically for the vagueness of BEP-3, how about this example that made me rage on IRC. In the description for the info_hash field in the Tracker section.
This value will almost certainly have to be escaped.
ALMOST CERTAINLY?? Will it, or won't it? Then, escaped? Escaped how?What this turned out to mean was that the 20-bit binary sha1 hash MUST be URL encoded, and not hex encoded.
I would love to see someone try to build a BitTorrent client for the first time based solely on this doc.
---
BEP-3 also seems more interested in implementation detail, than describing the protocol. Take the last paragraph (before Copyright) as an example.
Something else which occurred to me today is that BitTorrent is not a spec, it's not been developed, it has evolved. Along with being built in a very modular way, i.e.: DHTs can replace trackers and simply dropped in, magnet URIs can replace Torrent files. This probably contributes it's success and longevity, but what this also means is that there is a lot of stuff, like metainfo, trackers, bencoding, that SHOULD belong in their own spec docs, which form a collective whole.
1. In your EncodeDictionary, you sort byte arrays by converting them to string. Correct but subeffective. See e.g. this: http://stackoverflow.com/q/19695629/126995 but add checks for nulls, authors of that code forgot about that.
2. You don’t need a dedicated thread to wake up every 1-10 seconds and do something small. Thread are expensive system resources, they own stack, cache misses are guaranteed then they wake up, etc. If your compiler supports async-await, use that instead + endless loop + Task.Delay inside the loop. If not, System.Timers.Timer class will do.
That aside, fantastic work on this, I think previously the only Bittorrent library for C# was an abandoned Mono project.
The last time I was trying to build something that used bittorrent, I fell back on launching aria2 and redirecting and parsing its stdout, which as crazy as it was, worked much better.
https://aria2.github.io/manual/en/html/aria2c.html#rpc-inter...
Can't be worse than ASN.1 can it?
Already bad if you wanted ASN.1 but can't get Galois's version. If you can get it, then choosing an ASN.1 alternative can be bad since it probably won't have a formal spec, verified parser, Haskell implementation, and so on. Probably a drop in correctness in some corner case vs whatever they made.
Note: I'd like to see them do a high-assurance JSON and/or XDR parser instead of just ASN.1. I know they did a Haskell-to-JSON library already. A strong one that extracted parsers or generators from a user-supplied specification with plugins for various programming languages would be nice.
Pro tip: Use DateTimeOffset instead of DateTime. It's less frustratingly ambiguous than DateTime, and already has a Unix timestamp helper function if you're on the latest framework: https://msdn.microsoft.com/en-us/library/system.datetimeoffs...
Actually organising the write up forced me to tidy up the code much more than I otherwise would have. I definitely find C# to be one of the more readable languages although I have had to debug and untangle some C# messes before.
Yeah I remember changing it to a SortedDictionary but I changed it back. I can't remember exactly why, possibly because it's supposed to be sorted by raw UTF8 bytes rather than a nice neat C# string and I didn't want to start using byte arrays for dictionary keys. I guess it only needs to be sorted when in the BEncoding format and it felt better to keep the internal structure as simple as possible. The tradeoff is it doesn't support incorrectly encoded torrent files – I'm really not sure how much of an issue that is.
I have a few minor gripes with id, but they mainly relate to missing syntactic sugar rather than actual shortcomings.
Kudos to OP, great job.
EDIT: fix up grammar mistake.
BTW, regarding the original article, there is also a MonoTorrent library for .NET. Despite the name it can be compiled by Visual Studio. The original library was abandoned a while ago and seems to be buggy, but I was able to make a very simple .NET client with WinForms UI using this fork: https://github.com/ErtyHackward/monotorrent
The original bittorrent client (before µTorrent) was actually written in Python BTW.
I remember it, because 15 years ago that was the only client available. Later people started creating other clients by forking his python code, and eventually rewriting it in different languages.
There won't be a Forms or WPF port.
Xamarin.Mac provides C# bindings to Mac desktop APIs, and has nothing to do with Xamarin.Forms.
But it sounds like you are actually thinking of Xamarin.Forms, which allows sharing UI code on mobile platforms as well as UWP. You are correct that there is no Mac support for that.
Take this for example:
public byte[] Infohash { get; private set; } = new byte[20];
public string HexStringInfohash { get { return String.Join("", this.Infohash.Select(x => x.ToString("x2"))); } }
public string UrlSafeStringInfohash { get { return Encoding.UTF8.GetString(WebUtility.UrlEncodeToBytes(this.Infohash, 0, 20)); } }
You have an automatic property and two 'properties' that actually perform work every time you call the getter (might be smarter to make functions of those, so you know it's not just retrieval of data, but work is done).If you were to rewrite this a bit, you could make sure the 'work' is done only when needed, and the properties become actual simple data retrieval properties like:
public class Hashes
{
byte[] _infohash;
string _hexStringInfohash, _urlSafeStringInfohash;
public byte[] Infohash
{
get { return _infohash; }
private set
{
_infohash = value;
_hexStringInfohash = String.Join("", this.Infohash.Select(x => x.ToString("x2")));
_urlSafeStringInfohash = Encoding.UTF8.GetString(WebUtility.UrlEncodeToBytes(this.Infohash, 0, 20));
}
}
public string HexStringInfohash { get { return _hexStringInfohash; } }
public string UrlSafeStringInfohash { get { return _urlSafeStringInfohash; } }
public Hashes()
{
Infohash = new byte[20];
}
}
Going further through the article, I spot many more items to improve; but let's not forget your did great work and the code is quite readable.One thing that might help; is building some indexes to know how files are fragmented; you have the following code multiple times:
if ((start < Files[i].Offset && end < Files[i].Offset) ||
(start > Files[i].Offset + Files[i].Size && end > Files[i].Offset + Files[i].Size))
continue;
If you'd build an index to know which piece hits which files, you don't have to enumerate this every time.Another general remark is to always 'retrieve' an indexed item from the array and use that instead of keep calling the 'indexed' record.
So; do:
var file = Files[i];
if ((start < file.Offset && end < file.Offset) ||
(start > file.Offset + file.Size && end > file.Offset + file.Size))
continue;
The code becomes more readable and allows you to change the structure later on more easily since you don't have 100 references tot he same array now and only use an itermediate.edit: I'm the author, let me know if you have any questions.
If you package this up as a NuGet package and support Core then can you please ping me and I'll add it to https://anclafs.com.
https://github.com/jpsingleton/ANCLAFS/issues/6#issuecomment...
The executable project has some minor references to Mono.Posix just so it can catch kill signals while running in the Terminal and die gracefully.
Both dependencies can be removed quite easily from the project.
Thank you for the github link. Mods can we get OP link adjusted to the github link instead of what was submitted?
I agree with most of their sentiments, especially the parsing part could be made cleaner/more C#'ish :)
Otherwise nice work and very interesting writeup!