Background
Microsoft's Peer-to-Peer Grouping technology provides a stable, reliable, and robust infrastructure for Windows peer-to-peer applications to communicate. The Grouping API is based on the Graphing API, but adds a robust security system. Peers use the Peer Name Resolution Protocol (PNRP - a serverless DNS) to register and discover other peers within a group. Groups provide a secure mechanism for connecting peers, services, and resources within a peer network. Grouping allows data to be passed between peers efficiently, reliably, and securely.
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#.
Introduction
This article introduces the PeerGroup
managed class which is the main wrapper for all the peer-to-peer grouping APIs. This article builds on the previous article which introduces peer-to-peer groups and describes details about identities.
Startup
Before any other peer-to-peer grouping APIs can be called, PeerGroupStartup
must be called to initialize the group. Likewise, PeerGroupShutdown
must be called before the application exits, to clean up its existence in the group. To ensure this occurs, a static singleton class is provided. The PeerGroup
class includes a reference to guarantee the singleton is created before any other grouping APIs are used. As a result, you don't have to remember to do this. The following code shows the singleton class:
sealed class PeerGroupService
{
private PeerGroupService()
{
Peer.Graph.PEER_VERSION_DATA data;
uint hr = PeerGroupNative.PeerGroupStartup(1, out data);
if (hr != 0) throw new PeerGroupException(hr);
}
~PeerGroupService()
{
PeerGroupNative.PeerGroupShutdown();
}
public static readonly PeerGroupService Instance = new PeerGroupService();
}
The PeerGroup Class
The purpose of the PeerGroup
class is to expose all the peer grouping functionality. However, the code download at the beginning of this article only implements the ability to create, open, and delete a group. It also provides event handlers for some of the event notifications supported by peer-to-peer groups. Future articles will expand the capabilities of this class to expose the entire peer-to-peer grouping API set.
Creating a Group
The first step to working with a group is to create one. A group only needs to be created once by the first peer to publish it. The group is owned by the identity who creates it and is the initial administrator of the group.
The static Create
method takes three parameters to create a group; an identity, a group name, and a friendly name. The identity is created by a previous call to the PeerIdentity
Create
method. The group name is a short name for the group, for example, My Games or Kids Pictures. The friendly name provides additional information about a group such as a description or the e-mail contact of the group creator.
The following code shows how the Create
method wraps the underlying PeerGroupCreate
API:
public static void Create(string GroupName,
PeerIdentity Creator, string FriendlyName)
{
PeerGroupProperties properties = new PeerGroupProperties(null,
GroupName, Creator.Identity);
properties.FriendlyName = FriendlyName;
PEER_GROUP_PROPERTIES props = properties.Convert();
IntPtr hGroup;
uint hr = PeerGroupNative.PeerGroupCreate(ref props, out hGroup);
if (hr != 0) throw new PeerGroupException(hr);
hr = PeerGroupNative.PeerGroupClose(hGroup);
if (hr != 0) throw new PeerGroupException(hr);
}
Behind the scenes, a graphing database is created in the "Documents and Settings\<User Name>\Application Data\PeerNetworking" folder. A sub-folder matching the identity (for example, 47e322b5473094981ad20777bed3fe71d009ec8a.User1) contains sub-folders (for example, 7cb01116ac4b9fa469ef2bdf808a662a40fdb78e.Test Group) with a database for each group that this identity creates. The database name is always called "grouping".
Accessing Groups
Groups can only be accessed by enumerating the groups associated with an identity. Accessing the Groups
property of a PeerIdentity
returns a PeerGroupCollection
. Enumerate this collection to discover all of the PeerGroup
instances currently associated with an identity. Once a PeerGroup
instance is selected, its methods and properties can be accessed.
foreach (PeerGroup group in Identity.Groups)
{
if (group.GroupName == "Test Group")
group.Open();
else
group.Delete();
}
Group Properties
Internally, a PeerGroupProperties
class maintains the current information about the group. This class wraps the marshaled PEER_GROUP_PROPERTIES
data structure and exposes properties for each field in the structure, as shown in the following code:
public class PeerGroupProperties
{
private PeerGroup group;
private PEER_GROUP_PROPERTIES properties;
internal PeerGroupProperties(PeerGroup Group,
string GroupName, string Identity)
{
group = Group;
properties = new PEER_GROUP_PROPERTIES();
properties.dwSize =
Marshal.SizeOf(typeof(PEER_GROUP_PROPERTIES));
properties.pwzClassifier = GroupName;
properties.pwzCreatorPeerName = Identity;
properties.ulMemberDataLifetime = 2419200;
properties.ulPresenceLifetime = 300;
}
public PeerGroupPropertyAction Action
{
get { return properties.dwFlags; }
set { properties.dwFlags = value; }
}
public string Cloud
{
get { return properties.pwzCloud; }
}
public string GroupName
{
get { return properties.pwzClassifier; }
}
internal string InternalGroupName
{
get { return properties.pwzGroupPeerName; }
set { properties.pwzGroupPeerName = value; }
}
public string Creator
{
get { return properties.pwzCreatorPeerName; }
}
public string FriendlyName
{
get { return properties.pwzFriendlyName; }
set { properties.pwzFriendlyName = value; Update(); }
}
public string Comment
{
get { return properties.pwzComment; }
set { properties.pwzComment = value; Update(); }
}
public int MemberDataLifetime
{
get { return properties.ulMemberDataLifetime; }
}
public int PresenceLifetime
{
get { return properties.ulPresenceLifetime; }
}
internal void Update()
{
if (group != null && group.hGroup != IntPtr.Zero)
{
uint hr = PeerGroupNative.PeerGroupSetProperties(group.hGroup, ref properties);
if (hr != 0) throw new PeerGroupException(hr);
}
}
}
For the most part, the default values are all you will ever need. Any writable properties result in a call to the unmanaged PeerGroupSetProperties
API to update the underlying property in the group.
These properties can also be retrieved after opening a group, by accessing the Properties
property. This property uses the unmanaged PeerGroupGetProperties
API to get the current group properties as shown in the following code:
public PeerGroupProperties Properties
{
get
{
if (hGroup != IntPtr.Zero)
{
IntPtr ppGroupProperties;
uint hr = PeerGroupNative.PeerGroupGetProperties(hGroup,
out ppGroupProperties);
if (hr != 0) throw new PeerGroupException(hr);
properties = new PeerGroupProperties(this,
(PEER_GROUP_PROPERTIES)Marshal.PtrToStructure(
ppGroupProperties,typeof(PEER_GROUP_PROPERTIES)));
}
return properties;
}
}
Deleting a Group
The Delete
method of the PeerGroup
class allows you to delete the peer-to-peer group created by the owning identity. The underlying PeerGroupDelete
API is called as shown in the code below:
public void Delete()
{
if (hGroup != IntPtr.Zero) Close();
uint hr = PeerGroupNative.PeerGroupDelete(
owner.Identity, properties.InternalGroupName);
if (hr != 0)
throw new PeerGroupException(hr);
}
Opening a Group
The Open
method of the PeerGroup
class opens and connects to the group. First, the underlying PeerGroupOpen
API is called to open the group. This method passes the identity of the owner, the internal group name, and NULL
(to indicate the Global_
cloud). Next, a call to register for event notifications is made. Finally, the underlying PeerGroupConnect
API method is called which searches the PNRP for other peers in the group and registers the secured peer name with the PNRP.
public void Open()
{
if (hGroup != IntPtr.Zero) Close();
uint hr = PeerGroupNative.PeerGroupOpen(owner.Identity,
properties.InternalGroupName,
IntPtr.Zero, out hGroup);
if (hr != 0)
throw new PeerGroupException(hr);
RegisterForEvents();
hr = PeerGroupNative.PeerGroupConnect(hGroup);
if (hr != 0) throw new PeerGroupException(hr);
}
Events
Before connecting, the wrapper registers a callback for event notifications which allows the application to receive events when something changes in the group. The following code shows how the internal RegisterForEvents
method uses the PeerGroupRegisterEvent
method to register for events:
private void RegisterForEvents()
{
if (hPeerEvent != IntPtr.Zero) return;
AutoResetEvent sendEvent = new AutoResetEvent(false);
Handle = ThreadPool.RegisterWaitForSingleObject(sendEvent,
new WaitOrTimerCallback(PeerEventWorker), null, -1, false);
PEER_GROUP_EVENT_REGISTRATION[] info =
{
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_STATUS_CHANGED),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_PROPERTY_CHANGED),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_RECORD_CHANGED),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_DIRECT_CONNECTION),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_NEIGHBOR_CONNECTION),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_INCOMING_DATA),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_MEMBER_CHANGED),
new PEER_GROUP_EVENT_REGISTRATION(
PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_CONNECTION_FAILED)
};
int size = Marshal.SizeOf(info[0]);
IntPtr infoptr = Marshal.AllocCoTaskMem(info.Length*size);
int offset = 0;
foreach (PEER_GROUP_EVENT_REGISTRATION item in info)
{
Marshal.StructureToPtr(item,
(IntPtr)(infoptr.ToInt32()+offset), false);
offset += size;
}
uint result = PeerGroupNative.PeerGroupRegisterEvent(hGroup,
sendEvent.Handle, info.Length, infoptr, out hPeerEvent);
if (result != 0)
Marshal.ThrowExceptionForHR((int)result);
}
The PEER_GROUP_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 group. RegisterWaitForSingleObject
registers this delegate with the default thread pool using an infinite timeout. Finally, the event handle and data structure is passed to PeerGroupRegisterEvent
.
The following code shows how the worker thread handles incoming events:
private void PeerEventWorker(object xdata, bool timedOut)
{
while (true)
{
IntPtr evptr;
uint result = PeerGroupNative.PeerGroupGetEventData(hPeerEvent,
out evptr);
if (result == Peer.Graph.PeerGraphNative.PEER_S_NO_EVENT_DATA
|| evptr == IntPtr.Zero)
break;
if (result != 0) Marshal.ThrowExceptionForHR((int)result);
PEER_GROUP_EVENT_DATA data = (PEER_GROUP_EVENT_DATA)
Marshal.PtrToStructure(evptr,
typeof(PEER_GROUP_EVENT_DATA));
IntPtr dataptr = (IntPtr)(evptr.ToInt32() +
Marshal.SizeOf(typeof(PEER_GROUP_EVENT_DATA)));
switch (data.eventType)
{
case PEER_GROUP_EVENT_TYPE.PEER_GROUP_EVENT_STATUS_CHANGED:
HandleEventStatusChanged(dataptr);
break;
}
PeerGroupNative.PeerFreeData(evptr);
}
}
The PeerGroupGetEventData
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 is used to handle each event type.
Event Types
The following list summarizes the event types exposed by the PeerGroup
class and describes when each event is fired:
StatusChanged |
- |
indicates the peer's status within the group (listening, has connections) |
PropertyChanged |
- |
one or more properties of the group 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 |
MemberChanged |
- |
a member's status within the group has changed (connected, disconnected, joined, updated) |
The current 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 select an existing identity and create, open, close, and delete peer-to-peer groups.
The Identities tab shown above allows you to view existing identities. To create an identity, enter a name and click the Create button. To delete an identity, select the identity in the list and click the Delete button.
The Create tab allows you to create a group that is associated with the selected identity. Enter a group name and a friendly name (can be blank), then click the Create button to create a group.
Switch to the Groups tab to see a list of the groups to which the identity belongs. Select a group to see its properties. Click the Open button to open and connect to the group. Once opened, any changes to the Comment or FriendlyName properties are immediately updated. Click the Close button to close the selected group. Click the Delete button to delete a group. Note that the demo does not keep track of which groups are opened and closed when selecting different groups in the list.
Point of Interest
netsh
Use the netsh command to discover another way at looking at what's going on behind the scenes when opening groups. Using netsh, switch to the 'p2p pnrp cloud' context. You will notice PNRP entries are added to the Global_
cloud after opening a group. Use the 'show names' to see the register names. The following is an example of the output you should see:
P2P Name: 8264f208a7cddc30933f3e0b0c7ea2e143e6953e.participant
Identity: 47e322b5473094981ad20777bed3fe71d009ec8a.User1
Comment:
PNRP ID: 7e154375ac617a5e2c1d86cb5ab45793.3ffe831f400419520f2185acc317053e
State: OK
IP Addresses: [3ffe:831f:4004:1952:8000:5739:bb6f:38ea]:3587 tcp
Switch to the 'p2p idmgr' context and use the 'show groups ALL' command to see all the configured groups for the identity. The following is an example of the output you should see:
Identity P2PID = d3d9e1b0ec8accfe3fc55a0e68ef2f6a
Peername = 47e322b5473094981ad20777bed3fe71d009ec8a.User1
Group P2PID = 3d0e6725c882cea35327d7b533ebaf45 GMC chain len=2
Peername = 8264f208a7cddc30933f3e0b0c7ea2e143e6953e.Test Group
Friendly = Hi there!
Valid: Start = 2/4/2006 13:15:32 End =2/4/3006 14:15:32 (Valid)
Switch to the 'p2p group' context and use the 'show address <Group P2PID>' command to see the address currently bound to the group. In the above case, 'show address 3d0e6725c882cea35327d7b533ebaf45' results in the following output:
Resolving participant node...
Found a participant node.
Participant node is listening on following addresses:
[3ffe:831f:4004:1952:8000:5739:bb6f:38ea]:3587
Extensibility
A real application that uses peer-to-peer groups to share files or provide a threaded discussion must derive a custom class from the PeerGroup
class. By default, the Groups
property of the PeerIdentity
class returns a PeerGroupCollection
class which uses a PeerGroupFactory
class to create the PeerGroup
class. However, setting the Factory
property of the PeerIdentity
class allows you to provide your own factory to create your own derived, custom class.
identity.Factory = new MyPeerGroupFactory();
public class MyPeerGroup : PeerGroup
{
}
public class MyPeerGroupFactory : PeerGroupFactory
{
public override PeerGroup CreatePeerGroup(PeerIdentity Owner)
{
return new MyPeerGroup(Owner);
}
}
Future articles will show extensibility points that allow custom classes for connections, peers, records, and searching similar to those provided for the PeerGraph
class.
Troubleshooting
When creating an identity or group, it's possible you might get a 'No Key Access' error. The following knowledge base article helps to explain what this means and how to work around the problem: Peer-to-Peer Framework APIs Return a "PEER_E_NO_KEY_ACCESS" Error Message.
Links to Resources
I have found the following resources to be very useful in understanding peer groups:
Conclusion
I will be covering more of the grouping APIs in future articles. Stay tuned for the next article which will describe creating invitations to invite other identities to a group and how these invitations are used by a peer to join a group.
- Peer Group - Invitations and Joining
- Peer Collaboration - People Near Me
- Peer Collaboration - EndPoints
- Peer Collaboration - Capabilities
- Peer Collaboration - Presence
- Peer Collaboration - Invitations
- Peer Name Resolution - Windows Vista Enhancements
If you have suggestions for other topics, please leave a comment. Finally, if you've read this far, don't forget to vote!
History