Background
This article introduces Microsoft's Peer-to-Peer Graphing technology. Graphing provides a stable, reliable, and robust infrastructure for Windows peer-to-peer applications to communicate. Graphs are the foundation for connecting peers, services and resources within a peer network. Peer Name Resolution Protocol (PNRP - a serverless DNS) is used to register and discover peers within the graph.
A graph consists of peers (sometimes referred to as nodes). A peer can be a user-interactive application, service or resource. Graphing allows data to be passed between peers efficiently and reliably. Typically, data is broadcast across the graph and shared with all peers. However, a peer can establish a direct connection with any other peer in the graph to exchange private data.
Introduction
Microsoft's entire Peer-to-Peer technology is exposed through the latest Platform SDK as C/C++ API calls. However, the code in this article shows these APIs being used from .NET managed code using C#. The sample application includes a PeerGraph
class that implements methods to Create, Open and Delete a graph. Also, delegates are provided to intercept changes in the graph. While the PeerGraph
class hides the details of using Microsoft's peer-to-peer graphing APIs (in order to simplify the programming model), the flow of unmanaged calls is outlined below.
Startup
Before any other peer-to-peer graphing APIs can be called, PeerGraphStartup
must be called to initialize the graph. Likewise, PeerGraphShutdown
must be called before the application exits, to clean up its existence in the graph. To ensure this occurs, a static singleton class is provided. The PeerGraph
class includes a reference to guarantee the singleton is created, before any other graphing APIs are used. As a result, you don't have to remember to do this. The following code shows the singleton class:
sealed class PeerGraphService
{
private PeerGraphService()
{
PEER_VERSION_DATA data;
uint hr = PeerGraphNative.PeerGraphStartup(1, out data);
if (hr != 0) throw new PeerGraphException(hr);
}
~PeerGraphService()
{
PeerGraphNative.PeerGraphShutdown();
}
public static readonly PeerGraphService Instance =
new PeerGraphService();
private static readonly Peer.NameResolution.WSAService
service = Peer.NameResolution.WSAService.Instance;
}
Notice how the last line also takes care of calling the PNRP Service singleton to guarantee that the WSAxxx sub-system is initialized.
The PeerGraph Class
The purpose of the PeerGraph
class is to expose all the peer graphing functionality. However, the code download at the beginning of this article only implements the ability to create, open and delete a graph and provides event handlers for some of the events that are fired by the peer-to-peer graph.
Creating a Graph
The first step to working with a graph is to create one. A graph only needs to be created once by the first peer to publish it. Think of it like creating a Windows Domain but without the central server.
The static Create
method takes three parameters: graph name, identity and database name. Graph name is a name describing the purpose of the graph. The database name parameter indicates the name of an internal database to use for caching data published to the graph. This database is located under "Documents and Settings\<User Name>\Application Data\PeerNetworking". The identity parameter is the name associated with the application or user connecting to the graph. A sub-folder matching the identity contains sub-folders for each graphing database that this identity creates. Identity is a graphing concept and is not related to your Windows user account. The following code shows the Create
method:
public static void Create(string GraphName,
string Identity, string DatabaseName)
{
PeerGraphProperties properties = new PeerGraphProperties(GraphName);
properties.SetIdentity(Identity);
PEER_GRAPH_PROPERTIES props = properties.Convert();
IntPtr hGraph;
uint hr = PeerGraphNative.PeerGraphCreate(ref props,
DatabaseName, IntPtr.Zero, out hGraph);
if (hr != 0) throw new PeerGraphException(hr);
hr = PeerGraphNative.PeerGraphClose(hGraph);
if (hr != 0) throw new PeerGraphException(hr);
}
A PEER_GRAPH_PROPERTIES
class is used to define settings when creating the graph (including name and identity). These properties along with the DatabaseName
are passed to the underlying PeerGraphCreate
method. This method can fail, if a copy of the database already exists locally.
Deleting a Database
The static Delete
method of the PeerGraph
class allows you to delete the local database for a given identity.
public static void Delete(string DatabaseName, string Identity)
{
uint hr = PeerGraphNative.PeerGraphDelete(properties.GraphName,
Identity, DatabaseName);
if (hr != 0) throw new PeerGraphException(hr);
}
The PeerGraphDelete
method is called to delete the local database containing a cached copy of the data published to the graph. This does not delete the graph on other peers and is strictly a local operation.
Opening a Graph
The PeerGraph
constructor requires the name of the graph. This name will be converted into an unsecured peer name for advertising the graph's existence in the Global_ cloud via PNRP.
The Open
method requires a database name and identity. The following code shows the Open
method:
public void Open(string DatabaseName, string Identity)
{
if (hGraph != IntPtr.Zero) Close();
properties.SetIdentity(Identity);
uint hr = PeerGraphNative.PeerGraphOpen(properties.GraphName,
Identity, DatabaseName, IntPtr.Zero, 0,
IntPtr.Zero, out hGraph);
if (hr != 0 && hr != PeerGraphNative.PEER_S_GRAPH_DATA_CREATED)
throw new PeerGraphException(hr);
RegisterForEvents();
Connect(true);
}
The underlying PeerGraphOpen
method is called using the graph name and identity along with the DatabaseName
parameter. The first time the graph is opened, a new database is created, otherwise, an existing database is used. Next, the Open
method registers for events (see below) and connects to the graph. The code for connecting is next.
private void Connect(bool Listen)
{
IPEndPoint endpoint = DiscoverAddress();
if (endpoint != null)
{
PEER_ADDRESS address = new PEER_ADDRESS();
address.dwSize = Marshal.SizeOf(typeof(PEER_ADDRESS));
address.sin6.sin6_addr = endpoint.Address.GetAddressBytes();
address.sin6.sin6_family = (short)endpoint.AddressFamily;
address.sin6.sin6_port = (ushort)endpoint.Port;
uint hr = PeerGraphNative.PeerGraphConnect(hGraph,
IntPtr.Zero, ref address, out connectionId);
if (hr != 0) throw new PeerGraphException(hr);
}
else if (Listen)
{
this.Listen();
}
}
Before connecting, the local peer checks with PNRP to see if this graph is already registered using the internal DiscoverAddress
method. If another peer is located, the address is passed to the PeerGraphConnect
method which connects to and synchronizes the current state of the graph with the other peer. This operation is asynchronous. The connectionId
returned as an output is used later when a ConnectionChanged
event is raised to indicate synchronization has completed. Once synchronization has completed, the ConnectionChanged
event handler internally registers with the PNRP and begins listening for other peers to connect to and use the graph.
If another peer cannot be located, then the local peer registers with the PNRP and begins listening for other peers to connect to and use the graph.
Listening
The following code shows the internal Listen
method:
private void Listen(PeerGraphScope Scope, int ScopeId, short Port)
{
uint hr = PeerGraphNative.PeerGraphListen(hGraph, Scope, ScopeId, Port);
if (hr != 0) throw new PeerGraphException(hr);
RegisterAddress();
}
Listen calls the PeerGraphListen
method which opens a dynamic port within a scope (Global_ by default). Note that if the Windows firewall is enabled, a popup will appear with a message asking if you want to keep blocking the application. Assuming you unblock the application, it will publish the graph and start listening for incoming events from other peers.
In order for other peers to locate the graph, the internal RegisterAddress
method registers the graph as peer name (in this case 0.TestGraph
) with the PNRP.
Events
The Graphing API allows an application to register and receive events when something changes within a graph. The following code shows how the internal RegisterForEvents
method uses the PeerGraphRegisterEvent
method to register for events.
private void RegisterForEvents()
{
if (hPeerEvent != IntPtr.Zero) return;
PEER_GRAPH_EVENT_REGISTRATION[] info =
{
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_STATUS_CHANGED),
new PEER_GRAPH_EVENT_REGISTRATION(P
EER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_PROPERTY_CHANGED),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_RECORD_CHANGED),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_DIRECT_CONNECTION),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_NEIGHBOR_CONNECTION),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_INCOMING_DATA),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_CONNECTION_REQUIRED),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_NODE_CHANGED),
new PEER_GRAPH_EVENT_REGISTRATION(
PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_SYNCHRONIZED)
};
int size = Marshal.SizeOf(info[0]);
IntPtr infoptr = Marshal.AllocCoTaskMem(info.Length*size);
int offset = 0;
foreach (PEER_GRAPH_EVENT_REGISTRATION item in info)
{
Marshal.StructureToPtr(item, (IntPtr)(infoptr.ToInt32()+offset), false);
offset += size;
}
AutoResetEvent sendEvent = new AutoResetEvent(false);
Handle = ThreadPool.RegisterWaitForSingleObject(sendEvent,
new WaitOrTimerCallback(PeerEventWorker), null, -1, false);
uint result = PeerGraphNative.PeerGraphRegisterEvent(hGraph,
sendEvent.Handle, info.Length, infoptr, out hPeerEvent);
if (result != 0) Marshal.ThrowExceptionForHR((int)result);
}
The PEER_GRAPH_EVENT_REGISTRATION
data structure includes a field indicating the type of event to monitor. An array of these data structures can be passed to the Register
method. Much of the code above takes care of marshalling this data structure into a block of unmanaged memory.
The WaitOrTimerCallback
represents a callback method that is executed when something changes in the graph. RegisterWaitForSingleObject
registers this delegate with the default thread pool using an infinite timeout. Finally, the event handle and data structure is passed to PeerGraphRegisterEvent
.
The following code shows how the worker thread handles incoming events:
private void PeerEventWorker(object xdata, bool timedOut)
{
while (true)
{
IntPtr evptr;
uint result =
PeerGraphNative.PeerGraphGetEventData(hPeerEvent, out evptr);
if (result == PeerGraphNative.PEER_S_NO_EVENT_DATA ||
evptr == IntPtr.Zero) break;
if (result != 0) Marshal.ThrowExceptionForHR((int)result);
PEER_GRAPH_EVENT_DATA data = (PEER_GRAPH_EVENT_DATA)
Marshal.PtrToStructure(evptr, typeof(PEER_GRAPH_EVENT_DATA));
IntPtr dataptr = (IntPtr)(evptr.ToInt32() +
Marshal.SizeOf(typeof(PEER_GRAPH_EVENT_DATA)));
switch (data.eventType)
{
case PEER_GRAPH_EVENT_TYPE.PEER_GRAPH_EVENT_STATUS_CHANGED:
HandleEventStatusChanged(dataptr);
break;
}
PeerGraphNative.PeerGraphFreeData(evptr);
}
}
The PeerGraphGetEventData
method is called to get each event. The loop ensures that all events are proceeded until PEER_S_NO_EVENT_DATA
is returned indicating no further events. Each event data structure is marshaled and a switch
statement used to handle each event type.
Event Types
The following list summarizes the event types and their meaning.
StatusChanged
- indicates the peer's status within the graph (synchronized, listening, has connections).
PropertyChanged
- one or more properties of the graph has changed.
RecordChanged
- data published to the graph has changed.
ConnectionChanged
- a connection to a remote peer or direction connection has changed (connected, disconnected, failed).
IncomingData
- private data has been received from a direct connection with another peer.
NodeChanged
- a peer's presence has changed (connected, disconnected, IP address updated).
EventSynchronized
- indicates a record type is synchronized.
The sample application only implements the StatusChanged
event. Future articles will demonstrate how the other events are typically used.
Using the Sample Application
The sample application allows you to create a graph (unsecured peer name 0.TestGraph
) with an initial identity. This identity must be used the first time the graph is opened. Afterwards, other identities can be used, provided the graph is running on another computer somewhere. The Delete
button allows you to delete the identity's database. Be sure to Close
a graph before deleting, otherwise an error indicates the database file is in use.
Once created, you can run multiple copies of the application either on the same computer or on different computers on your network. Enter an identity, then click the Open button to open the graph. Note that when running the application on the same computer, you must use different identities before Opening, otherwise, Open
fails with an "Unspecified Error". After Opening, the status bar shows the current status of the local peer. Finally, the list box logs the result of each operation. While not very exciting, it demonstrates the first step to building a peer-to-peer graphing application.
The next article will take a deeper look into monitoring and interacting with other peers in the graph and handling direct connections for passing private data.
Point of Interest
By having all running peers register the graph name with the PNRP, it allows new peers to easily locate other running peers connected to the same graph. Put another way, the graph represents the resource or service and is represented by an unsecured peer name. Each peer that registers with the PNRP is essentially publishing its participation in the graph. This makes it easy for new peers to locate running peers, connect to the service or resource and, in turn, publish its participation in the graph.
One issue that's worth mentioning is that anyone can register an unsecured peer name. Once a hacker knows the unsecured peer name associated with the graph, it's possible to create a peer that spoofs or interferes with data published to the graph or peers connected to the graph. This limits graphs for use either on the local computer for inter-process communication and shared state or a corporate network where secure access to the network can be more tightly controlled.
Microsoft created peer-to-peer Groups for broader, Internet communication. Peer Groups use a secure, peer naming scheme based on certificates. Peers must be invited before they can join and communicate with other peers in a group. But that's a topic for another article.
Links to Resources
I have found the following resources to be very useful in understanding peer graphs:
Conclusion
I hope you have found this article useful. I'm considering writing more articles on the following subjects to further your understanding of Microsoft's Peer-to-Peer technology:
- Peer Name Resolution - Windows Vista Enhancements
- Peer Graph - Nodes and Connections
- Peer Graph - Records
- Peer Graph - Attributes
- Peer Graph - Searching
- Peer Groups and Identity
- Peer Collaboration - People Near Me
- Peer Collaboration - EndPoints
- Peer Collaboration - Capabilities
- Peer Collaboration - Presence
- Peer Collaboration - Invitations
If you have suggestions for other topics, please leave a comment. Finally, a big Thanks to all those who have been voting.
History
Initial revision.