⬆️ ⬇️

We write a simple UDP BitTorrent tracker on Netty + MongoDB

Introduction



This article highlights the work of UDP Tracker Protocol. All the examples in this article will be in Java using the Netty NIO framework. MongoDB is taken as a database.



Usually torrent trackers work through the HTTP protocol, transmitting data via GET requests. The work of the tracker via the UDP protocol allows you to significantly reduce traffic (more than 2 times), as well as get rid of the restriction on the number of simultaneous connections that TCP protocol imposes.



The link to the UDP tracker in the client may look like this: udp: //tracker.openbittorrent.com: 80 / announce , where there could be anything (or nothing at all) in the place of announce. But the indication of the port is mandatory , unlike the HTTP tracker.



')

General principles of the protocol



Now how in general the UDP tracker works.

1. First, the client sends a connection request to the tracker (package 0x00 Connect). In this request, the connection ID field is 0x41727101980 - this is the protocol identifier. In addition, the client sends the transaction ID chosen by him randomly.

2. Next, the server creates a unique connection ID for the client, which it sends in a reply packet. In this case, the server is obliged to transmit the transaction ID that it received from the client.

3. The client now has a unique ID (which, however, is not really needed if it is an open tracker without user registration and traffic accounting.) And can send us packages with announcements.

4. In response to the announcement, the server gives a list of peers of the torrent, the interval for the client to contact the server, and statistics for sedes / peers.

5. Another client can send us scrape-requests, where several hashes of torrents are transmitted, to which he wants to receive statistics. The number of requested torrents per 1 request cannot exceed 74 due to the limitations of the UDP protocol.



Server development



At this stage, I advise you to download the source tracker, because in the article I will describe only the key points. Download the sources and used libraries here: github.com/lafayette/udp-torrent-tracker



Netty initialization.


Executor threadPool = Executors.newCachedThreadPool(); DatagramChannelFactory factory = new NioDatagramChannelFactory(threadPool); //    UDP. bootstrap = new ConnectionlessBootstrap(factory); bootstrap.getPipeline().addLast("handler", new Handler()); // Handler      . bootstrap.setOption("reuseAddress", true); //        .  ,   reuseAddress   .  ,  -    . //  ShutdowHook    ,       Netty. ,     . Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { public void run() { channel.close(); bootstrap.releaseExternalResources(); } })); String host = Config.getInstance().getString("listen.host", "127.0.0.1"); Integer port = Config.getInstance().getInt("listen.port", 8080); InetSocketAddress address = new InetSocketAddress(host, port); //  ,  ,      .       Netty     . logger.info("Listening on " + host + ":" + port); //       bind,       . bootstrap.bind(address); 




Receive messages from customers.


 public class Handler extends SimpleChannelUpstreamHandler { private static final Logger logger = Logger.getLogger(Handler.class); public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { ChannelBuffer channelBuffer = (ChannelBuffer)e.getMessage(); //  ChannelBuffer      udp-. //  ,   ,    connection ID (long), action ID (int)  transaction ID (int)      16 . if (channelBuffer.readableBytes() < 16) { logger.debug("Incorrect packet received from " + e.getRemoteAddress()); } long connectionId = channelBuffer.readLong(); //    connectionId,             . int actionId = channelBuffer.readInt(); // ID .  : 0x00 Connect; 0x01 Announce; 0x02 Scrape; 0x03: Error.  ()   . int transactionId = channelBuffer.readInt(); // ID .       ID ,    . Action action = Action.byId(actionId); ClientRequest request; switch (action) { case CONNECT: request = new ConnectionRequest(); break; case ANNOUNCE: request = new AnnounceRequest(); break; case SCRAPE: request = new ScrapeRequest(); break; default: logger.debug("Incorrect action supplied"); ErrorResponse.send(e, transactionId, "Incorrect action"); return; } //         ,     ,   . request.setContext(ctx); request.setMessageEvent(e); request.setChannelBuffer(channelBuffer); request.setConnectionId(connectionId); request.setAction(action); request.setTransactionId(transactionId); //         . request.read(); } } 




MongoDB


To work with MongoDB, I used a wonderful mapping library - Morphia .



Here is how I described the class for storing a feast:

 @Entity("peers") public class Peer { public @Id ObjectId id; public @Indexed byte[] infoHash; public byte[] peerId; public long downloaded; public long left; public long uploaded; public @Transient int event; public int ip; public short port; public @Transient int key; public @Transient int numWant; public @Transient short extensions; public long lastUpdate; @PrePersist private void prePersist() { this.lastUpdate = System.currentTimeMillis(); } } 


Transient annotation means that we do not save this field to a table. We will need these fields only to process the request. The infoHash field is annotated with Indexed , because we will look for suitable peers by the torrent hash.



We also need to create a connection to the database. This is done quite simply:

 morphia = new Morphia(); morphia.map(Peer.class); //  ,     . mongo = new Mongo(host, port); datastore = morphia.createDatastore(mongo, "udptracker"); 




And an example search for peers by info_hash

 Query<Peer> peersQuery = Datastore.instance().find(Peer.class); peersQuery.field("infoHash").equal(peer.infoHash); peersQuery.field("peerId").notEqual(peer.peerId); //    . peersQuery.limit(peer.numWant).offset(randomOffset); //   numWant    . 




For a better understanding, it is better to look into the Morphia documentation.



Otherwise, everything is quite simple: from the received ChannelBuffer, we read the data from the client, and then send the answer to e.getChannel (). You can see the implementation of all the packages in the source code.



In addition, for a better understanding of the protocol, I advise you to study xbtt.sourceforge.net/udp_tracker_protocol.html



Sources of the above server: github.com/lafayette/udp-torrent-tracker



PS Just want to say that this is my first experience with both Netty and MongoDB. In fact, on this project I studied both these wonderful things. Therefore, advice on how to make better / prettier / in Jedi is very welcome.

Source: https://habr.com/ru/post/130563/



All Articles