This is part of Early Gnutella

LimeWire Design

Christopher Rohrs

This document provides an overview of LimeWire’s design. It assumes you have a working knowledge of the Gnutella protocol. In our presentation, we have decided to take a bottom-up approach. First we describe the core Gnutella messaging classes. Then we add bits of the backend, like fetching connections, routing messages, and handling uploads and downloads. Next, we describe the interface between the LimeWire backend and GUI. Finally we describe LimeWire’s threading model.

Messages and Connections

Message is an abstract class representing all types of Gnutella messages. Message stores data common to all Gnutella messages, like GUIDs. It is subclassed by classes representing each Gnutella message: QueryRequest, QueryReply, PingRequest, PingReply, and PushRequest. This is illustrated below. Each of these classes has two constructors, for incoming and outgoing messages respectively. The first constructor of QueryRequest, for example, is called when the user initiates a query. It takes a query string and minimum speed. The second is called when reading a query request from the network. It takes a payload of raw bytes, which is then parsed to find the query string and minimum speed.

The Connection class implements a simply Gnutella messaging connection. It has two constructors, for incoming and outgoing connections. The first takes an address and port, and the second takes a Socket created by a ServerSocket. Connection provides methods for reading and writing messages. These delegate to Message.read and Message.write, respectively.

Message Hierarchy

Figure 1: Message Hierarchy

When adding or modifying the Message hierarchy, keep in mind the following rules. First, all Message classes are immutable, with the exception of TTL’s and hops. (It would be inefficient to have to copy every message that is forwarded.) Secondly, the Message hierarchy is designed to be simple and reusable. In particular, there are no dependencies on any other classes.

Fetching and Routing

Connection is subclassed by ManagedConnection, which adds three major pieces of functionality:

  1. Automatic buffering and flushing of outgoing messages; there is no need to call flush() after send(m). This is implemented with a dedicated writer thread and provides a primitive form of flow control. If the remote host cannot keep up, messages are dropped in the following order: PingRequest, PingReply, QueryRequest, QueryReply, PushRequest.
  2. A loopForMessage method for automatically reading messages. This is called by the message dispatch thread for that connection.
  3. Various statistics (e.g., number of bytes read) and other non-essential data (e.g., is this a Clip2 Reflector). The personal and route spam filters are stored here. Each is implemented as a subclass of SpamFilter.

ConnectionManager maintains the list of all ManagedConnection instances. It also provides factory methods to instantiate ManagedConnection—the latter has no public constructors. ConnectionManager is responsible for fetching outgoing connections as needed. It gets cached addresses from HostCatcher. HostCatcher also triggers (via ConnectionManager) connections to pong servers like router.limewire.com; this is an overly-complicated relic of the days when host discovery services had more important roles. ConnectionWatchdog looks for connections that have not sent or received any data in the last few seconds. ConnectionWatchdog kills those connections that don’t respond to a ping with TTL=1.

The details of message routing are encapsulated in MessageRouter. Every message read by the loopForMessage method of ManagedConnection is passed to the MessageRouter instance via one of the handleX(..) methods. (In the future, this will be replaced by a single handleMessage(m) method.) MessageRouter broadcasts queries and pings to all of ConnectionManager’s connections and sets up reply routing tables with the help of three RouteTable instances. Query replies are routed to the appropriate connection—or to the GUI, in the case of your own query—with the help of these tables.

While MessageRouter knows how to forward and route queries and pings, it does not actually know how to respond to them. Instead it defines two abstract methods: respondToQueryRequest(..) and respondToQueryReply. These methods are implemented by a subclass, StandardMessageRouter, which in turn delegates to FileManager. FileManager uses some rather fancy data structures to implement queries efficiently.

These classes are illustrated below. Connection, the superclass of ManagedConnection, is not shown for simplicity.

ConnectionManager and MessageRouter

Figure 2: ConnectionManager and MessageRouter

Uploads and Downloads

One difficulty of the Gnutella protocol is that the same incoming port is used to distinguish between messaging connections, uploads, and downloads. Acceptor is the class responsible for creating a server socket, accepting incoming TCP connections, and dispatching it accordingly. Sockets for messaging connections are passed to ConnectionManager. Sockets for uploads and downloads are passed to UploadManager and DownloadManager, which manage uploads and downloads, respectively.

If LimeWire already has enough messaging connections, ConnectionManager instantiates a special RejectConnection class instead of a ManagedConnection. RejectConnection extends Connection, but it instantiates a thread to wait for a ping, respond with pongs from the HostCatcher, and disconnect. RejectConnection instances are never added to the ConnectionManager’s list of connections. While this is an elegant mechanism, it may become obsolete in the future if we decide to reject a connection after reading its Gnutella 0.6-style headers.

DownloadManager maintains the list of downloads. Each download is implemented with a single ManagedDownloader instance, which implements the Downloader interface. (This interface will come into play when we discuss the GUI in the next section.) ManagedDownloader provides the logic for "smart downloading" from a list of locations. Each attempt from a single location is implemented with a single HTTPDownloader instance. If you want to implement swarmed downloads, make ManagedDownloader hold a reference to multiple HTTPDownloaders. DownloadManager uses FileManager to determine if you have already downloaded the file, and HTTPDownloader uses FileManager to share the file after downloading.

UploadManager maintains the list of uploads (Actually UploadManager only maintains who is uploading files, not what they?re uploading.? But if we wanted to maintain a list of uploads in the backend, this would be the place.). Because there is no such thing as a "smart upload", each upload is implemented with a single HTTPUploader instance, which implements the Uploader interface. HTTPUploader is implemented in a rather different manner than HTTPDownloader because of a refactoring a few months ago. HTTPUploader uses the "State" pattern to delegate to a single UploadState instance. There are multiple implementations of UploadState, e.g., NormalUploadState and FileNotFoundUploadState (to send 404 errors). Uploading code uses FileManager to get the requested file.

All of this is shown in the class diagram below.

Acceptor, uploads, and downloads

Figure 3: Acceptor, uploads, and downloads

Here is more information on downloads, with emphasis on swarming logic.

GUI-Backend Interface

At last we are ready to connect the backend with the GUI. The interface between the two is well-defined and is illustrate below. This makes it easy to drop in new user interfaces, such as a command-line interface. It also means that GUI and backend developers are decoupled, though significant new features inevitably require backend and GUI changes.

The GUI triggers backend operations through a RouterService instance. This class uses the "Facade" pattern to provide a series of operations to the GUI. For example, it can be used to create and destroy connections, initiate queries, and download files. The backend communicates with the GUI through the ActivityCallback interface. This uses the "Observer" pattern. For example, the backend reports search results via the handleQueryReply(..) and upload activity via addUpload(..). ActivityCallback is implemented by VisualConnectionCallback, which delegates to the appropriate GUI components.

SettingsManager provides a list of properties shared between the backend and GUI, such as the list of shared directories. These properties are read from disk at startup and written to disk later. Changing some properties (e.g., the number of outgoing connections) requires you to take extra action (e.g., calling RouterService.setKeepAlive(int)) for the properties to take effect. You are warned that SettingsManager is notoriously difficult to modify.

RouterService, ActivityCallback, and SettingsManager have a large number of methods. You will find that just about any significant feature requires a modification to one of them. These classes could arguably be split in a larger number of smaller interfaces. However, they are not the only way that the GUI and the backend communicate. Methods in RouterService can return instances of Uploader, Downloader, and Connection. The GUI can then manipulate these object directly, avoiding the need for extra methods in RouterService.

When modifying the GUI-backend interface, keep in mind the following rules. First, the backend must not depend on GUI code, though the GUI may depend on portions of the backend code. Secondly, methods in RouterService and ActivityCallback should generally be non-blocking, i.e., should return after a bounded amount of time. Doing otherwise could slow the message dispatch threads or GUI event threads. Exceptions to this rules (e.g., createConnectionBlocking) should be well-documented.

GUI-Backend interface

Figure 4: GUI-Backend interface

For a brief discussion of how the GUI displays search results, read the result grouping overview.

Threading Summary

LimeWire is a thread-intensive program. Here is a summary of all threads in use. The thread names shown in bold do not necessarily correspond to Java thread or class names.

  • The Acceptor thread accepts incoming connections. For each incoming connection, a new Connection thread is spawned.
  • Each ManagedConnection has two threads: one to read data from the connection and one to write queued packets. There is also proposal to use a single thread for all connections via non-blocking IO (NIO).
  • ConnectionManager creates several ConnectionFetcher threads to fetch outgoing connections. These threads eventually become Connection reader threads if successful.
  • The HostCatcher's RouterConnectionThread establishes connections to pong servers as needed. It spends most of its time sleeping. This thread may become obsolete in the future.
  • The ConnectionWatchdog thread looks for dud connections and tries to replace them with better ones.
  • Each Downloader and Uploader thread transfers a single file. There is no support for persistent HTTP.
  • The Main/GUI thread manages the details of the UI. It initially creates the Acceptor thread. It may also create a number of quick-and-dirty Worker threads to keep the GUI responsive when calling potentially blocking operations.

The most important thing to note is that LimeWire uses a two thread per connection model. This means that a slow connection never prevents others from making progress. An alternative design is to use non-blocking IO (e.g., select(..)) so that one thread can handle multiple sockets. However, this would complicate the code, is not supported in all versions of Java, and would probably not improve performance significantly.