在这个实现中有几点需要注意:
- 单个端口与子网标识符一起用于将请求路由到正确的处理程序。子网标识符使得在同一台机器上测试多个“服务器”变得更加容易,因为我们不需要为每个“服务器”打开(并“允许”)一个唯一的端口号
- JSON 被用作请求和响应数据的序列化格式。
- RPC 调用是同步的,因为它们等待响应。还有其他基于随机标识和处理程序的实现,我选择不实现。
分布式哈希表发出的请求从以下类序列化到 JSON。
代码清单 95:基本请求类
public abstract class BaseRequest
{
public object Protocol { get; set; }
public string ProtocolName { get; set; }
public BigInteger RandomID { get; set; }
public BigInteger Sender { get; set; }
public
BaseRequest()
{
RandomID = ID.RandomID.Value;
}
}
public class FindNodeRequest : BaseRequest
{
public BigInteger Key { get; set; }
}
public class FindValueRequest : BaseRequest
{
public BigInteger Key { get; set; }
}
public class PingRequest : BaseRequest { }
public class StoreRequest : BaseRequest
{
public BigInteger Key { get; set; }
public string Value { get; set; }
public bool IsCached { get; set; }
public int ExpirationTimeSec { get;
set; }
}
public interface ITcpSubnet
{
int
Subnet { get; set;
}
}
public class FindNodeSubnetRequest : FindNodeRequest, ITcpSubnet
{
public int Subnet { get; set; }
}
public class FindValueSubnetRequest : FindValueRequest, ITcpSubnet
{
public int Subnet { get; set; }
}
public class PingSubnetRequest : PingRequest, ITcpSubnet
{
public int Subnet { get; set; }
}
public class StoreSubnetRequest : StoreRequest, ITcpSubnet
{
public int Subnet { get; set; }
}
在接收这些消息的服务器端,它们由一个公共请求类处理。
代码清单 96:公共请求—服务器端
/// <summary>
/// For
passing to Node handlers with common parameters.
/// </summary>
public class CommonRequest
{
public object Protocol { get; set; }
public string ProtocolName { get; set; }
public BigInteger RandomID { get; set; }
public BigInteger Sender { get; set; }
public BigInteger Key { get; set; }
public string Value { get; set; }
public bool IsCached { get; set; }
public int ExpirationTimeSec { get;
set; }
}
正如注释所述,公共请求通过让 RPC 处理程序方法具有相同的参数来简化服务器实现。
请求处理程序提取CommonRequest
的相关部分,并调用Node
类的适当方法。这里重要的部分是联系协议必须作为FindNode
和FindValue
响应的一部分返回。请注意,返回的是匿名对象。
代码清单 97:请求处理程序
public object ServerPing(CommonRequest request)
{
IProtocol protocol = Protocol.InstantiateProtocol(
request.Protocol, request.ProtocolName);
Ping(new Contact(protocol, new ID(request.Sender)));
return new { RandomID = request.RandomID };
}
public object ServerStore(CommonRequest request)
{
IProtocol protocol = Protocol.InstantiateProtocol(
request.Protocol, request.ProtocolName);
Store(new Contact(protocol, new ID(request.Sender)),
new ID(request.Key), request.Value, request.IsCached,
request.ExpirationTimeSec);
return new { RandomID = request.RandomID };
}
public object ServerFindNode(CommonRequest request)
{
IProtocol protocol = Protocol.InstantiateProtocol(
request.Protocol, request.ProtocolName);
var
(contacts, val) =
FindNode(new Contact(protocol, new ID(request.Sender)), new ID(request.Key));
return new
{
Contacts = contacts.Select(c =>
new
{
Contact = c.ID.Value,
Protocol = c.Protocol,
ProtocolName = c.Protocol.GetType().Name
}).ToList(),
RandomID = request.RandomID
};
}
public object ServerFindValue(CommonRequest request)
{
IProtocol protocol = Protocol.InstantiateProtocol(
request.Protocol, request.ProtocolName);
var
(contacts, val) =
FindValue(new Contact(protocol, new ID(request.Sender)), new ID(request.Key));
return new
{
Contacts = contacts?.Select(c =>
new
{
Contact = c.ID.Value,
Protocol = c.Protocol,
ProtocolName = c.Protocol.GetType().Name
})?.ToList(),
RandomID = request.RandomID,
Value = val
};
}
JSON 响应被反序列化为以下类。
代码清单 98:服务器响应
public abstract class BaseResponse
{
public BigInteger RandomID { get; set; }
}
public class ErrorResponse : BaseResponse
{
public string ErrorMessage { get; set; }
}
public class ContactResponse
{
public BigInteger Contact { get; set; }
public object Protocol { get; set; }
public string ProtocolName { get; set; }
}
public class FindNodeResponse : BaseResponse
{
public List<ContactResponse> Contacts { get; set; }
}
public class FindValueResponse : BaseResponse
{
public List<ContactResponse> Contacts { get; set; }
public string Value { get; set; }
}
public class PingResponse : BaseResponse { }
public class StoreResponse : BaseResponse { }
服务器是一个简单的HttpListener
实现为 C# HttpListenerContext
对象,但是请注意子网 ID 是如何被用来将请求路由到与子网相关联的特定节点的。
代码清单 99:流程请求
protected override async void ProcessRequest(HttpListenerContext context)
{
string
data = new StreamReader(context.Request.InputStream,
context.Request.ContentEncoding).ReadToEnd();
if
(context.Request.HttpMethod == "POST")
{
Type
requestType;
string path = context.Request.RawUrl;
//
Remove "//"
//
Prefix our call with "Server" so that the method name is
unambiguous.
string methodName = "Server" + path.Substring(2);
if (routePackets.TryGetValue(path, out requestType))
{
CommonRequest commonRequest =
JsonConvert.DeserializeObject<CommonRequest>(data);
int subnet = ((ITcpSubnet)JsonConvert.DeserializeObject(
data, requestType)).Subnet;
INode node;
if (subnets.TryGetValue(subnet, out node))
{
await Task.Run(() =>
CommonRequestHandler(methodName, commonRequest, node, context));
}
else
{
SendErrorResponse(context,
new ErrorResponse()
{ ErrorMessage = "Method not recognized." });
}
}
else
{
SendErrorResponse(context,
new ErrorResponse()
{ ErrorMessage = "Subnet node not found." });
}
context.Response.Close();
}
}
该协议实现了四个 RPC 调用,向服务器发出HTTP POST
s。请注意协议是如何从 JSON 返回实例化的,如果协议不受支持,联系人将从FindNode
和FindValue
返回的联系人中删除。
代码清单 100:查找节点、查找值、Ping 和存储处理程序
public (List<Contact>
contacts, RpcError error) FindNode(Contact sender, ID key)
{
ErrorResponse error;
ID id
= ID.RandomID;
bool timeoutError;
var ret = RestCall.Post<FindNodeResponse, ErrorResponse>(
url + ":" + port + "//FindNode",
new FindNodeSubnetRequest()
{
Protocol
= sender.Protocol,
ProtocolName
= sender.Protocol.GetType().Name,
Subnet
= subnet,
Sender
= sender.ID.Value,
Key
= key.Value,
RandomID
= id.Value
}, out error, out
timeoutError);
try
{
var contacts = ret?.Contacts?.Select(
val => new Contact(Protocol.InstantiateProtocol(
val.Protocol, val.ProtocolName), new ID(val.Contact))).ToList();
//
Return only contacts with supported protocols.
return (contacts?.Where(c => c.Protocol
!= null).ToList() ?? EmptyContactList(),
GetRpcError(id, ret, timeoutError,
error));
}
catch (Exception ex)
{
return (null,
new RpcError() { ProtocolError = true,
ProtocolErrorMessage = ex.Message
});
}
}
/// <summary>
/// Attempt
to find the value in the peer network.
/// </summary>
/// <returns>A null contact list is acceptable
here as it is a valid return
/// if the value is found.
/// The
caller is responsible for checking the timeoutError flag to make
/// sure null contacts is not
/// the
result of a timeout error.</returns>
public (List<Contact>
contacts, string val, RpcError error)
FindValue(Contact sender, ID key)
{
ErrorResponse error;
ID id
= ID.RandomID;
bool timeoutError;
var ret = RestCall.Post<FindValueResponse, ErrorResponse>(
url + ":" + port + "//FindNode",
new FindValueSubnetRequest()
{
Protocol
= sender.Protocol,
ProtocolName
= sender.Protocol.GetType().Name,
Subnet
= subnet,
Sender
= sender.ID.Value,
Key
= key.Value,
RandomID
= id.Value
}, out error, out
timeoutError);
try
{
var contacts = ret?.Contacts?.Select(
val => new Contact(Protocol.InstantiateProtocol(val.Protocol,
val.ProtocolName), new ID(val.Contact))).ToList();
//
Return only contacts with supported protocols.
return (contacts?.Where(c => c.Protocol
!= null).ToList(),
ret.Value, GetRpcError(id, ret, timeoutError,
error));
}
catch (Exception ex)
{
return (null,
null, new
RpcError() { ProtocolError = true,
ProtocolErrorMessage = ex.Message
});
}
}
public RpcError Ping(Contact sender)
{
ErrorResponse error;
ID id
= ID.RandomID;
bool timeoutError;
var ret = RestCall.Post<FindValueResponse, ErrorResponse>(
url + ":" + port + "//Ping",
new PingSubnetRequest()
{
Protocol
= sender.Protocol,
ProtocolName
= sender.Protocol.GetType().Name,
Subnet
= subnet,
Sender
= sender.ID.Value,
RandomID
= id.Value
},
out error, out timeoutError);
return GetRpcError(id, ret, timeoutError,
error);
}
public RpcError Store(Contact sender, ID key, string val,
bool isCached = false,
int expirationTimeSec = 0)
{
ErrorResponse error;
ID id
= ID.RandomID;
bool timeoutError;
var ret = RestCall.Post<FindValueResponse, ErrorResponse>(
url + ":" + port + "//Store",
new StoreSubnetRequest()
{
Protocol
= sender.Protocol,
ProtocolName
= sender.Protocol.GetType().Name,
Subnet
= subnet,
Sender
= sender.ID.Value,
Key
= key.Value,
Value
= val,
IsCached
= isCached,
ExpirationTimeSec
= expirationTimeSec,
RandomID
= id.Value
},
out error, out timeoutError);
return GetRpcError(id, ret, timeoutError,
error);
}
RpcError
类管理我们可能遇到的各种错误,并在GetRpcError
方法中实例化。
代码清单 101:处理 RPC 错误
public class RpcError
{
public bool HasError
{
get { return TimeoutError || IDMismatchError || PeerError || ProtocolError;
}
}
public bool TimeoutError { get; set; }
public bool IDMismatchError { get;
set; }
public bool PeerError { get; set; }
public bool ProtocolError { get; set; }
public string PeerErrorMessage { get;
set; }
public string ProtocolErrorMessage { get;
set; }
}
protected RpcError GetRpcError(
ID id,
BaseResponse resp,
bool timeoutError,
ErrorResponse peerError)
{
return new RpcError()
{
IDMismatchError = id != resp.RandomID,
TimeoutError = timeoutError,
PeerError = peerError != null,
PeerErrorMessage = peerError?.ErrorMessage
};
}
请注意,此类反映了可能发生的几种不同的错误:
- 超时:对等方在
RestCall.REQUEST_TIMEOUT
期间未能响应,默认为 500 ms - 标识不匹配:对等方响应了,但其标识与发送方的随机标识不匹配。
- 对等体:对等体遇到异常,在这种情况下,异常消息被返回给调用者。
- 反序列化:
Post
方法捕获 JSON 反序列化错误,这也表明对等响应有错误。
就像协议本身的单元测试一样,研究这些单元测试对于如何设置服务器和客户端非常有用。下面的单元测试验证了往返调用,测试了协议调用和服务器。每个测试都会初始化服务器,然后将其拆除。
代码清单 102: TCP 子网设置和拆除
[TestClass]
public class TcpSubnetTests
{
protected
string localIP = "http://127.0.0.1";
protected
int port = 2720;
protected
TcpSubnetServer server;
[TestInitialize]
public void Initialize()
{
server = new TcpSubnetServer(localIP, port);
}
[TestCleanup]
public void TestCleanup()
{
server.Stop();
}
...
单元测试练习四个 RPC 调用中的每一个,以及一个超时错误。
该测试验证Ping
RPC 调用。
代码清单 103: PingRouteTest
[TestMethod]
public void PingRouteTest()
{
TcpSubnetProtocol p1 = new TcpSubnetProtocol(localIP, port, 1);
TcpSubnetProtocol p2 = new TcpSubnetProtocol(localIP, port, 2);
ID
ourID = ID.RandomID;
Contact
c1 = new Contact(p1, ourID);
Node
n1 = new Node(c1, new VirtualStorage());
Node
n2 = new Node(new Contact(p2, ID.RandomID), new VirtualStorage());
server.RegisterProtocol(p1.Subnet, n1);
server.RegisterProtocol(p2.Subnet, n2);
server.Start();
p2.Ping(c1);
}
奇怪的是,这里没有断言,因为没有值得注意的事情发生。这一点的关键是没有抛出异常。
该测试验证Store
RPC 调用。
代码清单 104:存储路由测试
[TestMethod]
public void StoreRouteTest()
{
TcpSubnetProtocol p1 = new TcpSubnetProtocol(localIP, port, 1);
TcpSubnetProtocol p2 = new TcpSubnetProtocol(localIP, port, 2);
ID
ourID = ID.RandomID;
Contact
c1 = new Contact(p1, ourID);
Node
n1 = new Node(c1, new VirtualStorage());
Node
n2 = new Node(new Contact(p2, ID.RandomID), new VirtualStorage());
server.RegisterProtocol(p1.Subnet,
n1);
server.RegisterProtocol(p2.Subnet,
n2);
server.Start();
Contact
sender = new Contact(p1, ID.RandomID);
ID
testID = ID.RandomID;
string testValue = "Test";
p2.Store(sender,
testID, testValue);
Assert.IsTrue(n2.Storage.Contains(testID),
"Expected remote peer to have
value.");
Assert.IsTrue(n2.Storage.Get(testID) ==
testValue,
"Expected remote peer to
contain stored value.");
}
该测试验证FindNodes
RPC 调用。
代码清单 105: FindNodesRouteTest
[TestMethod]
public void FindNodesRouteTest()
{
TcpSubnetProtocol p1 = new TcpSubnetProtocol(localIP, port, 1);
TcpSubnetProtocol p2 = new TcpSubnetProtocol(localIP, port, 2);
ID
ourID = ID.RandomID;
Contact
c1 = new Contact(p1, ourID);
Node
n1 = new Node(c1, new VirtualStorage());
Node
n2 = new Node(new Contact(p2, ID.RandomID), new VirtualStorage());
//
Node 2 knows about another contact, that isn't us (because we're excluded.)
ID
otherPeer = ID.RandomID;
n2.BucketList.Buckets[0].Contacts.Add(new Contact(
new TcpSubnetProtocol(localIP, port, 3), otherPeer));
server.RegisterProtocol(p1.Subnet,
n1);
server.RegisterProtocol(p2.Subnet,
n2);
server.Start();
ID id
= ID.RandomID;
List<Contact> ret = p2.FindNode(c1,
id).contacts;
Assert.IsTrue(ret.Count
== 1, "Expected
1 contact.");
Assert.IsTrue(ret[0].ID
== otherPeer,
"Expected contact to the
other peer (not us).");
}
该测试验证FindValue
RPC 调用。
代码清单 106: FindValueRouteTest
[TestMethod]
public void FindValueRouteTest()
{
TcpSubnetProtocol p1 = new TcpSubnetProtocol(localIP, port, 1);
TcpSubnetProtocol p2 = new TcpSubnetProtocol(localIP, port, 2);
ID
ourID = ID.RandomID;
Contact
c1 = new Contact(p1, ourID);
Node
n1 = new Node(c1, new VirtualStorage());
Node
n2 = new Node(new Contact(p2, ID.RandomID), new VirtualStorage());
server.RegisterProtocol(p1.Subnet,
n1);
server.RegisterProtocol(p2.Subnet,
n2);
server.Start();
ID
testID = ID.RandomID;
string testValue = "Test";
p2.Store(c1,
testID, testValue);
Assert.IsTrue(n2.Storage.Contains(testID),
"Expected remote peer to have
value.");
Assert.IsTrue(n2.Storage.Get(testID)
== testValue,
"Expected remote peer to
contain stored value.");
var ret = p2.FindValue(c1, testID);
Assert.IsTrue(ret.contacts
== null, "Expected to find value.");
Assert.IsTrue(ret.val
== testValue,
"Value does not match
expected value from peer.");
}
此测试验证无响应的节点是否会导致超时错误。
代码清单 107:无响应节点测试
[TestMethod]
public void UnresponsiveNodeTest()
{
TcpSubnetProtocol p1 = new TcpSubnetProtocol(localIP, port, 1);
TcpSubnetProtocol p2 = new TcpSubnetProtocol(localIP, port, 2);
p2.Responds
= false;
ID
ourID = ID.RandomID;
Contact
c1 = new Contact(p1, ourID);
Node
n1 = new Node(c1, new VirtualStorage());
Node
n2 = new Node(new Contact(p2, ID.RandomID), new VirtualStorage());
server.RegisterProtocol(p1.Subnet,
n1);
server.RegisterProtocol(p2.Subnet,
n2);
server.Start();
ID
testID = ID.RandomID;
string testValue = "Test";
RpcError
error = p2.Store(c1, testID, testValue);
Assert.IsTrue(error.TimeoutError,
"Expected
timeout.");
}