Background
This is the first article of many, where I will talk about the practical uses of Microsoft's Peer-to-Peer technology. Peer-to-Peer technology has been around since the late 90's. There have been several articles written for CodeProject that discuss the various aspects and implementations of a peer-to-peer infrastructure. There are also several open-source implementations including Gnucleus and Shareaza.
Microsoft recognized the value of this technology and spun up various research projects to investigate and document implementation approaches. Pastry was the earliest project that I recall that tackled the issues involved in building a peer-to-peer network. By late 2003, Microsoft released the Advanced Networking Pack for Windows XP SP1 which added basic Peer-to-Peer technology plumbing into Windows. While characteristically late to the game, their implementation recognized the value of using IPv6 as the foundation for building peer-to-peer networks. Microsoft has subsequently rolled this technology into Windows XP Service Pack 2 and continues to expand the infrastructure's capabilities into Windows Vista. Finally, given the nature of the technology, it's interesting to note that Microsoft has not included this support on any of its server platforms.
Introduction
Microsoft's entire Peer-to-Peer technology is exposed through the latest Platform SDK as C/C++ API calls. That's great for anyone who still develops applications in unmanaged code or managed C++. However, I'm a big fan of .NET managed code, and have been busy importing each API call into C# and creating a managed framework to simplify, as much as possible, peer-to-peer application development in .NET. So, without further delay, let's roll our sleeves up and dig in.
Clouds
As previously mentioned, IPv6 is at the heart of Microsoft's Peer-to-Peer technology. The beauty of IPv6 is that besides an address and port, a registered resource also has a scope. Scope was added to further identify the services/resources listening on a port. A group of resources connected in a peer-to-peer network using the same scope is also known as a Cloud. Clouds are closely related to IPv6 scopes but there are some minor differences.
A Cloud is like a membership at the YMCA, where only members (registered peers) can use the facilities (in this case, communicate). All members can use the facilities at any YMCA and so belong to the organization. In Peer-to-Peer terms, this organization is called the Global Cloud. The equivalent to individual YMCA facilities are called Link Clouds. That is, each LAN the computer is connected to has a Link Cloud.
The Global Cloud has the greatest range and allows applications to communicate over the Internet. Link clouds allow applications behind a firewall or connected through a common subnet to communicate.
To enumerate the available clouds, you must indicate the scope of the search:
Any | | Return any cloud the computer is connected to |
Global | | Return the global cloud |
LinkLocal | | Return any link local clouds |
Next comes a series of of Windows Socket calls to synchronously begin a lookup and iterate over the results:
- Call
WSALookupServiceBegin
to begin the enumeration and return a handle.
- Call
WSALookupServiceNext
to retrieve a set of clouds matching the scope. Call this function until the application has retrieved all the clouds.
- Call
WSALookupServiceEnd
to finish the enumeration.
To encapsulate this functionality, I created a Collection class that implements IEnumerable
with an embedded class that implements IEnumerator
. This allows a simple foreach
loop to enumerate the clouds.
public class PeerCloudCollection : IEnumerable
{
private static WSAService service = WSAService.Instance;
private IEnumerator clouds;
public PeerCloudCollection(PNRP_SCOPE Scope)
{
clouds = new PeerCloudEnumerator(Scope);
}
public PeerCloudCollection()
{
clouds = new PeerCloudEnumerator(PNRP_SCOPE.Any);
}
private class PeerCloudEnumerator : IEnumerator
{
private static Guid SVCID_PNRPCLOUD = new Guid(0xc2239ce6,
0xc0, 0x4fbf, 0xba, 0xd6, 0x18, 0x13, 0x93, 0x85, 0xa4, 0x9a);
private IntPtr hLookup;
private IntPtr pResults;
private PNRP_SCOPE scope;
public PeerCloudEnumerator(PNRP_SCOPE Scope)
{
scope = Scope;
Reset();
}
~PeerCloudEnumerator()
{
if (pResults != IntPtr.Zero) Marshal.FreeHGlobal(pResults);
if (hLookup != IntPtr.Zero) PnrpNative.WSALookupServiceEnd(hLookup);
}
#region IEnumerator Members
public void Reset()
{
int err;
if (hLookup != IntPtr.Zero)
{
err = PnrpNative.WSALookupServiceEnd(hLookup);
if (err != 0)
throw new
System.Net.Sockets.SocketException(PnrpNative.WSAGetLastError());
hLookup = IntPtr.Zero;
pResults = IntPtr.Zero;
}
CSADDR_INFO csaAddr = new CSADDR_INFO();
PNRPCLOUDINFO pnrpCloudInfo = new PNRPCLOUDINFO();
BLOB blPnrpData = new BLOB();
WSAQUERYSET querySet = new WSAQUERYSET();
pnrpCloudInfo.dwSize = Marshal.SizeOf(typeof(PNRPCLOUDINFO));
pnrpCloudInfo.Cloud.Scope = scope;
blPnrpData.cbSize = Marshal.SizeOf(typeof(PNRPCLOUDINFO));
blPnrpData.pBlobData = Marshal.AllocHGlobal(blPnrpData.cbSize);
Marshal.StructureToPtr(pnrpCloudInfo, blPnrpData.pBlobData, false);
querySet.dwSize = Marshal.SizeOf(typeof(WSAQUERYSET));
querySet.dwNameSpace = 39;
querySet.lpServiceClassId =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid)));
Marshal.StructureToPtr(SVCID_PNRPCLOUD,
querySet.lpServiceClassId, false);
querySet.lpBlob =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(BLOB)));
Marshal.StructureToPtr(blPnrpData, querySet.lpBlob, false);
IntPtr qryptr =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WSAQUERYSET)));
Marshal.StructureToPtr(querySet, qryptr, false);
err = PnrpNative.WSALookupServiceBegin(qryptr,
WSALookup.LUP_RETURN_ALL, out hLookup);
if (err != 0)
throw new
System.Net.Sockets.SocketException(PnrpNative.WSAGetLastError());
}
public object Current
{
get
{
WSAQUERYSET querySet = (WSAQUERYSET)
Marshal.PtrToStructure(pResults, typeof(WSAQUERYSET));
BLOB blPnrpData = (BLOB)
Marshal.PtrToStructure(querySet.lpBlob, typeof(BLOB));
PNRPCLOUDINFO pnrpCloudInfo = (PNRPCLOUDINFO)
Marshal.PtrToStructure(blPnrpData.pBlobData,
typeof(PNRPCLOUDINFO));
PeerCloud cloud = new PeerCloud(querySet.lpszServiceInstanceName,
pnrpCloudInfo.Cloud.Scope, pnrpCloudInfo.Cloud.ScopeId);
Marshal.FreeHGlobal(pResults);
return cloud;
}
}
public bool MoveNext()
{
int err = Pnrp.LookupServiceNext(hLookup, out pResults);
if (err != 0) return false;
return true;
}
#endregion
public IEnumerator GetEnumerator()
{
return clouds;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
The Reset
function passes a complicated set of data structures into the WSALookupServiceBegin
function. The LUP_RETURN_ALL
parameter tells the subsequent call to WSALookupServiceNext
to fully populate the PNRPCLOUDINFO
data structure. MoveNext
uses the WSALookupServiceNext
to retrieve the results. true
or false
is returned depending on whether a result was returned. The Current
function marshals the data structures into managed equivalents and creates a PeerCloud
class to represent each cloud as a managed object. The following VB code shows how to use this cloud collection:
For Each cloud As PeerCloud In New PeerCloudCollection(scope)
Debug.Writeline(cloud.Name)
Next
Of course, before peers in a cloud can communicate, they must be able to locate each other. This is were Peer Name Resolution comes in.
Peer Name Resolution (PNRP)
PNRP; it's a mouthful. It's also a serverless DNS technology that allows nodes to discover each other. Think about that for a second. It allows your Windows XP box to become its own DNS server. No need to pay $10 to register each domain name. But there's a catch; it's limited to the domain name pnrp.net.
As the first letter in PNRP suggests, you must register a Peer Name. Peer names are fixed names for resources such as computers, users, groups, or services. This is similar to today's DNS except, instead of just IP addresses, the resources can be more granular. A Peer Name is a case-sensitive text string that has the format "Authority.Classifier". The value of Authority depends on whether the name is secured or unsecured. The value is always 0 for an unsecured Authority (secured names will be discussed in a later article). The value of Classifier is a text string name you give for the resource and cannot contain spaces. The following list shows some examples of peer names:
- 0.test
- 0.my.peername
- 6520c005f63fc1864b7d8f3cabebd4916ae7f33d.test
PNRP uses peer names to identify resources in a peer network. The key here is "Peer Network". This isn't the whole IPv6 network that the computer is connected to, it's limited to just the resources available within a Cloud. Registering any resource not managed by the Peer-to-Peer networking APIs either will result in an error or won't be resolved later.
Register and Unregister a Peer Name
To register an unsecured Peer Name, you must provide a valid unsecured Peer Name and an IP address. Optionally, you can indicate the Cloud name (Global by default) and an additional comment or description associated with the resource. This information is stored in the WSAQUERYSET
data structure and passed to the WSASetService
with the Register
option.
public static void Register(PeerRegistration Registration)
{
CSADDR_INFO csaAddr = new CSADDR_INFO();
PNRPINFO pnrpInfo = new PNRPINFO();
BLOB blPnrpData = new BLOB();
WSAQUERYSET querySet = new WSAQUERYSET();
csaAddr.iProtocol = 6;
csaAddr.iSocketType = 1;
csaAddr.LocalAddr.iSockaddrLength =
Marshal.SizeOf(typeof(SOCKADDR_IN6));
csaAddr.LocalAddr.lpSockaddr =
Marshal.AllocHGlobal(csaAddr.LocalAddr.iSockaddrLength);
Marshal.StructureToPtr(Registration.address,
csaAddr.LocalAddr.lpSockaddr, false);
pnrpInfo.dwSize = Marshal.SizeOf(typeof(PNRPINFO));
pnrpInfo.dwLifetime = 60*60*8;
if (Registration.Identity != string.Empty)
pnrpInfo.lpwszIdentity =
Marshal.StringToHGlobalUni(Registration.Identity);
blPnrpData.cbSize = Marshal.SizeOf(typeof(PNRPINFO));
blPnrpData.pBlobData = Marshal.AllocHGlobal(blPnrpData.cbSize);
Marshal.StructureToPtr(pnrpInfo, blPnrpData.pBlobData, false);
querySet.dwSize = Marshal.SizeOf(typeof(WSAQUERYSET));
querySet.dwNameSpace = 38;
querySet.dwNumberOfCsAddrs = 1;
querySet.lpServiceClassId =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid)));
Marshal.StructureToPtr(SVCID_PNRPNAMEV1,
querySet.lpServiceClassId, false);
querySet.lpszServiceInstanceName = Registration.PeerName;
if (Registration.CloudName != string.Empty)
querySet.lpszContext =
Marshal.StringToHGlobalUni(Registration.CloudName);
if (Registration.Comment != string.Empty)
querySet.lpszComment =
Marshal.StringToHGlobalUni(Registration.Comment);
querySet.lpcsaBuffer =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(CSADDR_INFO)));
Marshal.StructureToPtr(csaAddr, querySet.lpcsaBuffer, false);
querySet.lpBlob = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(BLOB)));
Marshal.StructureToPtr(blPnrpData, querySet.lpBlob, false);
IntPtr qryptr =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WSAQUERYSET)));
Marshal.StructureToPtr(querySet, qryptr, false);
int err = PnrpNative.WSASetService(qryptr,
WSAESETSERVICEOP.Register, 0);
if (err != 0)
throw new
System.Net.Sockets.SocketException(PnrpNative.WSAGetLastError());
}
To unregister an unsecured Peer Name, you must provide a valid unsecured Peer Name and optionally the Cloud name (Global by default). This information is stored in the WSAQUERYSET
data structure and passed to the WSASetService
with the Delete
option. While not listed here, the code is included in the download.
Searching for a Peer Name
PNRP includes the ability to search for registered Peer Names. To resolve a Peer Name, you must provide the Peer Name, search criteria and optional cloud name (Global by default) and IP address hint. Typically, a lookup is used to determine if a Peer Name already exists or to contact it directly. The following search criteria options are supported:
Default | | Uses the NonCurrentProcessPeerName option. |
AnyPeerName | | The matching peer name can be registered locally or remotely. |
NearestNonCurrentProcessName | | The matching peer name can be registered locally or remotely, but the resolve request excludes any peer name registered by the process making the resolve request and looks for the service closest to the local IP address. |
NearestPeerName | | The matching peer name can be registered locally or remotely, but the resolve request looks for the service closest to the local IP address. |
NearestRemotePeerName | | The resolve request excludes any peer name registered locally on this computer and looks for the service closest to the local IP address. |
NonCurrentProcessPeerName | | The matching peer name can be registered locally or remotely, but the resolve request excludes any peer name registered by the process making the resolve request. |
RemotePeerName | | The resolve request excludes any peer name registered locally on this computer. |
Again, a series of Windows Socket calls are used to synchronously begin a lookup.
- Call
WSALookupServiceBegin
to begin the enumeration and return a handle.
- Call
WSALookupServiceNext
to resolve the peer name.
- Call
WSALookupServiceEnd
to complete the enumeration.
The following code shows this:
public static bool Lookup(PeerRegistration Registration)
{
PNRPINFO pnrpInfo = new PNRPINFO();
BLOB blPnrpData = new BLOB();
WSAQUERYSET querySet = new WSAQUERYSET();
pnrpInfo.dwSize = Marshal.SizeOf(typeof(PNRPINFO));
pnrpInfo.nMaxResolve = 1;
pnrpInfo.dwTimeout = 30;
pnrpInfo.enResolveCriteria = Registration.Criteria;
blPnrpData.cbSize = Marshal.SizeOf(typeof(PNRPINFO));
blPnrpData.pBlobData = Marshal.AllocHGlobal(blPnrpData.cbSize);
Marshal.StructureToPtr(pnrpInfo, blPnrpData.pBlobData, false);
querySet.dwSize = Marshal.SizeOf(typeof(WSAQUERYSET));
querySet.dwNameSpace = 38;
querySet.lpServiceClassId =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid)));
Marshal.StructureToPtr(SVCID_PNRPNAMEV1,
querySet.lpServiceClassId, false);
querySet.lpszServiceInstanceName = Registration.PeerName;
if (Registration.CloudName != string.Empty)
querySet.lpszContext =
Marshal.StringToHGlobalUni(Registration.CloudName);
querySet.lpBlob = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(BLOB)));
Marshal.StructureToPtr(blPnrpData, querySet.lpBlob, false);
IntPtr qryptr =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WSAQUERYSET)));
Marshal.StructureToPtr(querySet, qryptr, false);
IntPtr hLookup;
int err = PnrpNative.WSALookupServiceBegin(qryptr,
WSALookup.LUP_RETURN_NAME | WSALookup.LUP_RETURN_ADDR |
WSALookup.LUP_RETURN_COMMENT, out hLookup);
if (err != 0)
throw new
System.Net.Sockets.SocketException(PnrpNative.WSAGetLastError());
IntPtr pResults;
err = LookupServiceNext(hLookup, out pResults);
bool found = false;
if (err == 0)
{
querySet = (WSAQUERYSET)Marshal.PtrToStructure(pResults,
typeof(WSAQUERYSET));
CSADDR_INFO csaAddr = (CSADDR_INFO)
Marshal.PtrToStructure(querySet.lpcsaBuffer,
typeof(CSADDR_INFO));
for (int i = 0; i < querySet.dwNumberOfCsAddrs; i++)
{
if (csaAddr.iProtocol == (int)System.Net.Sockets.ProtocolType.Tcp
&& csaAddr.RemoteAddr.iSockaddrLength ==
Marshal.SizeOf(typeof(SOCKADDR_IN6)))
{
SOCKADDR_IN6 addr = (SOCKADDR_IN6)
Marshal.PtrToStructure(csaAddr.RemoteAddr.lpSockaddr,
typeof(SOCKADDR_IN6));
Registration.Address = new System.Net.IPEndPoint(new
System.Net.IPAddress(addr.sin6_addr), addr.sin6_port);
found = true;
break;
}
}
Marshal.FreeHGlobal(pResults);
}
PnrpNative.WSALookupServiceEnd(hLookup);
return found;
}
DNS Name Corresponding to a Peer Name
Since PNRP is a serverless DNS, it makes sense to be able to lookup the DNS name associated with a Peer Name. It's also useful to determine the Peer Name given a DNS name. Starting in Windows Vista, two additional Peer-to-Peer APIs are provided to do just this. The code below shows calling these functions from C#:
public static string PnrpToDns(string PnrpName)
{
int length = 255;
System.Text.StringBuilder DnsName =
new System.Text.StringBuilder(length);
int err = PnrpNative.PeerNameToPeerHostName(PnrpName,
DnsName, ref length);
if (err != 0)
throw new System.Net.Sockets.SocketException(err);
return DnsName.ToString();
}
public static string DnsToPnrp(string DnsName)
{
int length = 255;
System.Text.StringBuilder PnrpName =
new System.Text.StringBuilder(255);
int err = PnrpNative.PeerHostNameToPeerName(DnsName,
PnrpName, ref length);
if (err != 0)
throw new System.Net.Sockets.SocketException(err);
return PnrpName.ToString();
}
Points of Interest
Now that you've seen the gory details of using the WSAxx functions to interact with PNRP, you'll be glad to know that Microsoft will be providing wrapper functions to hide these details. These wrapper functions will be available for Windows Vista Beta 2 and later. I can only guess that these functions will also be available in Windows XP Service Pack 3. While there is currently no documentation for these functions, I've taken a guess at their purpose.
PeerPnrpGetCloudInfo
- returns an array of PNRPCLOUDINFO
structures matching the given scope.
PeerPnrpGetEndpoint
- lookup a peer name to get its address.
PeerPnrpRegister
, PeerPnrpUnregister
- register and unregister a peer name.
PeerPnrpUpdateRegistration
- allows info such as address and comment to be updated for a previously registered peer name.
PeerPnrpResolve
- synchronously lookup if a peer name exists.
PeerPnrpStartResolve
, PeerPnrpEndResolve
- asynchronously lookup a peer name.
Links to Resources
I have found the following resources to be very useful in understanding Microsoft's Peer-to-Peer technology:
Conclusion
I hope you have found this article as a useful starting point. I'm considering writing more articles on the following subjects to further your understanding of Microsoft's Peer-to-Peer technology:
- Peer Name Resolution - netsh.
- Peer Graph - The Basics
- 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 or other topics, please leave a comment. Oh! and don't forget to vote.
History
Adrian Moore is the Development Manager for the SCADA Vision system developed by ABB Inc in Calgary, Alberta.
He has been interested in compilers, parsers, real-time database systems and peer-to-peer solutions since the early 90's. In his spare time, he is currently working on a SQL parser for querying .NET DataSets (http://www.queryadataset.com).
Adrian is a Microsoft MVP for Windows Networking.