Skip to content

Files

Latest commit

590e50c · Jan 11, 2022

History

History
767 lines (637 loc) · 17.9 KB

File metadata and controls

767 lines (637 loc) · 17.9 KB

十三、基本的 TCP 子网协议

在这个实现中有几点需要注意:

  • 单个端口与子网标识符一起用于将请求路由到正确的处理程序。子网标识符使得在同一台机器上测试多个“服务器”变得更加容易,因为我们不需要为每个“服务器”打开(并“允许”)一个唯一的端口号
  • 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类的适当方法。这里重要的部分是联系协议必须作为FindNodeFindValue响应的一部分返回。请注意,返回的是匿名对象。

代码清单 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();
     }
   }

TCP 子网协议处理程序

该协议实现了四个 RPC 调用,向服务器发出HTTP POST s。请注意协议是如何从 JSON 返回实例化的,如果协议不受支持,联系人将从FindNodeFindValue返回的联系人中删除。

代码清单 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 反序列化错误,这也表明对等响应有错误。

TCP 子网单元测试

就像协议本身的单元测试一样,研究这些单元测试对于如何设置服务器和客户端非常有用。下面的单元测试验证了往返调用,测试了协议调用和服务器。每个测试都会初始化服务器,然后将其拆除。

代码清单 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 调用中的每一个,以及一个超时错误。

PingRouteTest

该测试验证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);
   }

奇怪的是,这里没有断言,因为没有值得注意的事情发生。这一点的关键是没有抛出异常。

storerouterest

该测试验证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.");
   }

findnodesrouterest

该测试验证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).");
   }

findvaluerouterest

该测试验证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.");
   }