Background
Microsoft's Peer-to-Peer Graphing technology provides a stable, reliable, and robust infrastructure for Windows peer-to-peer applications to communicate. Peers use the Peer Name Resolution Protocol (PNRP - a serverless DNS) to register and discover other peers within the graph. Graphs are the foundation for connecting peers, services, and resources within a peer network. A peer can be a user-interactive application, service, or resource. Graphing allows data to be passed between peers efficiently and reliably.
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 concept of associating meta-data with peer records within a peer-to-peer graph. As we learned in the previous article, a record could represent the presence of a service, the availability of the CPU to do more work, the status of a resource that is updated periodically, or the URL of your latest blog entry. To simplify searching, rather than bury this information in the data of the record, meta-data can be attached to each record. In Microsoft terms, this meta-data is called attributes. Attributes are represented as XML in order to allow a wider range of capabilities.
Microsoft provides a simple schema against which the attribute XML fragments must conform. The low level PeerGraph APIs treat this XML fragment as a simple string. This article focuses on a PeerAttributes
helper class for managing attributes and providing methods to convert this information to and from the required XML format. It also describes an update to the PeerRecord
class now that attribute support has been implemented.
XML Schema
The XML schema is very simple. The root element must be called <attributes>
. It contains sub-elements that must be called <attribute>
. Each <attribute>
tag has two XML attributes; name
and type
. name
represents the name of the attribute, and type
its type. Only three data types are supported; int
, date
, and string
. The value is specified between the beginning and ending tags. Here is a simple example:
<attributes>
<attribute name="Type" type="string">
4d5b2f11-6522-433b-84ef-a298e60757b0
</attribute>
<attribute name="CreationTime" type="date">
2005-12-31T17:00:00.0000000-07:00
</attribute>
</attributes>
The Attributes.xsd file included in the sample code contains the schema. It's copied from the MSDN library web page that describes the Record Attribute Schema. This schema is used by the Validate
method described below.
PeerAttribute Class
The PeerAttribute
class is a simple wrapper for the storage of an attribute. The class contains three properties; name
which is read-only, type
and value
which are both updatable.
public enum PeerAttributeType
{
Int,
String,
Date
}
public class PeerAttribute
{
private string name;
private PeerAttributeType type;
private object data;
public PeerAttribute(string Name, PeerAttributeType Type, object Value)
{
name = Name;
type = Type;
data = Value;
}
public string Name
{
get { return name; }
}
public PeerAttributeType Type
{
get { return type; }
set { type = value; }
}
public object Value
{
get { return data; }
set { data = value; }
}
}
PeerAttributes Collection
The PeerAttribues
class is derived from CollectionBase
to provide convenient storage for PeerAttribute
objects and standard collection methods. Beyond the methods described below, it includes an indexer (Item[int]
and Item[string]
), and overrides the ToString
method to return the XML equivalent of the attributes stored in the collection.
Simple Add Methods
Four methods are provided to add a single attribute at a time to the collection.
private void Add(string Name, PeerAttributeType Type, object Value)
{
this.InnerList.Add(new PeerAttribute(Name, Type, Value));
}
public void Add(string Name, int Value)
{
Add(Name, PeerAttributeType.Int, Value);
}
public void Add(string Name, DateTime Value)
{
Add(Name, PeerAttributeType.Date, Value);
}
public void Add(string Name, string Value)
{
Add(Name, PeerAttributeType.String, Value);
}
The first method creates a PeerAttribute
object with the given name, type, and value. The other three methods are type specific versions which call the first method.
Add Object Method
A typical peer-to-peer application will use its own run-time objects to represent the meta data associated with a node or record being shared. The remaining Add
method allows the developer to pass this run-time object, and using reflection, extract the current values of its properties into attributes. The sample application extracts the properties from a System.IO.FileInfo
object.
public void Add(object Instance)
{
foreach (PropertyInfo prop in Instance.GetType().GetProperties())
{
try
{
object oValue = prop.GetValue(Instance, null);
if (prop.PropertyType.FullName == "System.Int32")
Add(prop.Name, (int)oValue);
else if (prop.PropertyType.FullName == "System.DateTime")
Add(prop.Name, (DateTime)oValue);
else
Add(prop.Name, oValue.ToString());
}
catch (Exception ex)
{
}
}
}
For each property the run-time object exposes, its current value and type are extracted to create a PeerAttribute
object.
Remove Method
In some cases, the Add Object method may extract properties that the developer does not want to expose as meta-data. The Remove
method allows unwanted attributes to be removed by name.
public void Remove(string Name)
{
int i = 0;
foreach (PeerAttribute attr in this.List)
{
if (attr.Name == Name)
{
this.List.RemoveAt(i);
break;
}
i++;
}
}
XML Property
The get
part of the XML
property converts the attributes stored in the collection into an XML fragment using the XmlTextWriter
class.
get
{
if (this.List.Count == 0) return null;
XmlTextWriter xt = new XmlTextWriter(new MemoryStream(),
System.Text.UTF8Encoding.Unicode);
xt.Formatting = Formatting.Indented;
xt.WriteStartElement("attributes");
foreach (PeerAttribute attr in this.List)
{
xt.WriteStartElement("attribute");
xt.WriteAttributeString("name", attr.Name);
xt.WriteAttributeString("type", StringFromType(attr.Type));
string s = attr.Value.ToString();
if (attr.Type == PeerAttributeType.Date)
s = XmlConvert.ToString(Convert.ToDateTime(attr.Value));
xt.WriteString(s);
xt.WriteEndElement();
}
xt.WriteEndElement();
xt.Flush();
xt.BaseStream.Position = 0;
return new StreamReader(xt.BaseStream).ReadToEnd();
}
The set
part of the XML
property converts each <attribute>
element of the XML fragment and stores them as PeerAttribute
objects in the collection. The XmlTextReader
is used to identify each element for conversion.
set
{
if (value == null || value == string.Empty) return;
this.Clear();
XmlTextReader xr = new XmlTextReader(new StringReader(value));
string Name = string.Empty;
PeerAttributeType Type = PeerAttributeType.String;
object Value = null;
while (xr.Read())
{
if (xr.NodeType == XmlNodeType.Element &&
xr.Name == "attribute")
{
xr.MoveToFirstAttribute();
int count = xr.AttributeCount;
for (int i = 0; i < count; i++)
{
xr.MoveToAttribute(i);
if (xr.Name == "name")
{
xr.ReadAttributeValue();
Name = xr.Value;
}
else if (xr.Name == "type")
{
xr.ReadAttributeValue();
Type = TypeFromString(xr.Value);
}
}
}
else if (xr.NodeType == XmlNodeType.Text)
{
Value = xr.Value;
}
else if (xr.NodeType == XmlNodeType.EndElement &&
xr.Name == "attribute")
{
switch (Type)
{
case PeerAttributeType.Int:
Add(Name, Convert.ToInt32(Value));
break;
case PeerAttributeType.Date:
Add(Name, Convert.ToDateTime(Value));
break;
default:
Add(Name, Value.ToString());
break;
}
}
}
}
The first if
-block handles extracting the name
and type
attributes. The middle if
-block handles extracting the value as text. The last if
-block uses the information gathered from the previous two if
-blocks to call the appropriate Add
method and perform the correct data type conversion of the value.
Validate Method
The sample file Attributes.xsd contains the schema for validating XML attribute fragments and is stored as an embedded resource in the sample application. This schema is loaded and compiled as a static (shared) field of the PeerAttributes
collection. The Validate
method uses the schema to validate the XML fragment equivalent to the current attributes stored in the collection.
public bool Validate()
{
XmlValidatingReader vr = new XmlValidatingReader(Xml,
XmlNodeType.Element, null);
vr.Schemas.Add(schema,null);
bool valid;
try
{
while (vr.Read()) { }
valid = true;
}
catch (XmlSchemaException ex)
{
valid = false;
}
catch (XmlException ex)
{
valid = false;
}
return valid;
}
The Validate
method returns true
if the XML fragment matches the schema, otherwise, false
if an exception is thrown. There should never be a need to validate the XML fragment, but this feature is included for completeness.
Update to PeerRecord
The source code from a previous article had attributes of the PeerRecord
class exposed as a string
property. In this article, the source code has been updated to expose a read-only property that returns a PeerAttributes
collection.
private PeerAttributes attributes;
public PeerAttributes Attributes
{
get { return attributes; }
}
Attributes are now automatically converted to or from the XML string fragment as the record is marshaled for use with the underlying API methods.
Using the Sample Application
The sample application lets you first create a graph (unsecured peer name 0.SharedFiles
) with an initial identity. The first instance should be opened with this identity. It will pause a few seconds looking for other instances, then begin to listen. Each subsequent instance of the application should open the graph with a different identity. These instances will connect to the nearest peer and synchronize. Each instance of the application is a peer.
Use the Add Folder button to select a folder containing files.
private void Add_Click(object sender, System.EventArgs e)
{
DialogResult result = folderBrowserDialog1.ShowDialog();
if (result == DialogResult.OK)
{
string path = folderBrowserDialog1.SelectedPath;
DirectoryInfo dinfo = new DirectoryInfo(path);
foreach (FileInfo info in dinfo.GetFiles())
{
PeerRecord record = graph.CreatePeerRecord(FILE_RECORD_TYPE,
new TimeSpan(0,1,0));
record.Attributes.Add(info);
record.DataAsString = info.Name;
Guid recordId = graph.AddRecord(record);
}
}
}
After selecting a folder, each file in the folder is published to the graph as a record. Note that the record is set to expire after 1 minute. The attributes of the record are set to the properties of the FileInfo
class. The data of the record is set to the name of the file.
private void OnRecordChanged(object sender, PeerGraphRecordChangedEventArgs e)
{
PeerRecord record;
FileItem item;
switch (e.Action)
{
case PeerRecordAction.Added:
record = graph.GetRecord(e.RecordId);
LogMessage(@"Added", record.DataAsString);
item = new FileItem(record.DataAsString, e.RecordId);
listBox2.Items.Add(item);
break;
...
After a record is added, the RecordChanged
event fires with the action set to Added
. The PeerRecord
corresponding to the RecordID
added and the record data containing the file name are used to create a simple FileItem
object that associates the name with the record ID in the list.
public class FileItem
{
private string name;
private Guid recordid;
public FileItem(string Name, Guid RecordId)
{
name = Name;
recordid = RecordId;
}
public string Name
{
get { return name; }
}
public Guid RecordId
{
get { return recordid; }
}
}
Click on a file name in the list box to show the XML attributes associated with the record in the right text box. The PeerAttributes
XML property is used to read the attributes as an XML fragment.
private void listBox2_SelectedIndexChanged(object sender, System.EventArgs e)
{
if (listBox2.SelectedIndex == -1)
textBox3.Text = string.Empty;
else
{
FileItem item = (FileItem)listBox2.SelectedItem;
PeerRecord record = graph.GetRecord(item.RecordId);
textBox3.Text = record.Attributes.Xml;
}
}
The lower list shows a diagnostic log of all actions and incoming events. Double-click to clear the list.
Points of Interest
While implementing the helper class, I ran across a bug / limitation in the attribute's ability to handle dates.
<attributes>
<attribute name="CreationTime"
type="date">1600-12-31T17:00:00.0000000-07:00</attribute>
</attributes>
It seems, a year any earlier than 1754 will cause the PeerGraphAddRecord
or PeerGraphUpdateRecord
methods to throw a 0x80040E21 exception. After some investigation, I discovered a Microsoft Knowledge Base article that explains how the Access ODBC Driver Cannot Insert Dates Prior to the Year 1753. It would appear that the underlying storage mechanism that peer-to-peer graphing uses is based on the Jet engine.
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. The next article will focus on searching for records matching information located in the meta-data. Stay tuned for more articles on the following topics:
- Peer Name Resolution - Windows Vista Enhancements
- Peer Graph - Searching
- Peer Graph - Importing and Exporting a Database
- 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.
History