绑定IP地址只是第一次工作

我想从服务器上的一个可用IP地址发出Web请求,所以我使用这个类:

public class UseIP { public string IP { get; private set; } public UseIP(string IP) { this.IP = IP; } public HttpWebRequest CreateWebRequest(Uri uri) { ServicePoint servicePoint = ServicePointManager.FindServicePoint(uri); servicePoint.BindIPEndPointDelegate = new BindIPEndPoint(Bind); return WebRequest.Create(uri) as HttpWebRequest; } private IPEndPoint Bind(ServicePoint servicePoint, IPEndPoint remoteEndPoint, int retryCount) { IPAddress address = IPAddress.Parse(this.IP); return new IPEndPoint(address, 0); } } 

然后:

 UseIP useIP = new UseIP("Valid IP address here..."); Uri uri = new Uri("http://ip.nefsc.noaa.gov"); HttpWebRequest request = useIP.CreateWebRequest(uri); // Then make the request with the specified IP address 

但解决方案刚刚第一次运行!

一个理论:

HttpWebRequest依赖于底层的ServicePoint。 ServicePoint表示与URL的实际连接。 与您的浏览器保持连接到请求之间的URL并重用该连接(以消除打开和关闭每个请求的连接的开销)的方式大致相同,ServicePoint为HttpWebRequest执行相同的function。

我认为,每次使用HttpWebRequest时都不会调用为ServicePoint设置的BindIPEndPointDelegate,因为ServicePoint正在重用连接。 如果您可以强制关闭连接,则对该URL的下一次调用应该导致ServicePoint需要再次调用BindIPEndPointDelegate。

不幸的是,ServicePoint接口似乎没有让您能够直接强制关闭连接。

两种解决方案(每种解决方案略有不同)

1)对于每个请求,设置HttpWebRequest.KeepAlive = false。 在我的测试中,这导致Bind委托与每个请求一对一调用。

2)将ServicePoint ConnectionLeaseTimeout属性设置为零或一些小值。 这将产生定期强制调用绑定委托的效果(不是每个请求一对一)。

从文档 :

您可以使用此属性来确保ServicePoint对象的活动连接不会无限期保持打开状态。 此属性适用于应定期删除和重新建立连接的情况,例如负载平衡方案。

默认情况下,当KeepAlive对于请求为true时,MaxIdleTime属性设置由于不活动而关闭ServicePoint连接的超时。 如果ServicePoint具有活动连接,则MaxIdleTime无效,连接将无限期保持打开状态。

当ConnectionLeaseTimeout属性设置为-1以外的值时,在指定的时间过去之后,通过在该请求中将KeepAlive设置为false,在服务请求后关闭活动的ServicePoint连接。

设置此值会影响ServicePoint对象管理的所有连接。

 public class UseIP { public string IP { get; private set; } public UseIP(string IP) { this.IP = IP; } public HttpWebRequest CreateWebRequest(Uri uri) { ServicePoint servicePoint = ServicePointManager.FindServicePoint(uri); servicePoint.BindIPEndPointDelegate = (servicePoint, remoteEndPoint, retryCount) => { IPAddress address = IPAddress.Parse(this.IP); return new IPEndPoint(address, 0); }; //Will cause bind to be called periodically servicePoint.ConnectionLeaseTimeout = 0; HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri); //will cause bind to be called for each request (as long as the consumer of the request doesn't set it back to true! req.KeepAlive = false; return req; } } 

以下(基本)测试结果为每个请求调用绑定委托:

 static void Main(string[] args) { //Note, I don't have a multihomed machine, so I'm not using the IP in my test implementation. The bind delegate increments a counter and returns IPAddress.Any. UseIP ip = new UseIP("111.111.111.111"); for (int i = 0; i < 100; ++i) { HttpWebRequest req = ip.CreateWebRequest(new Uri("http://www.yahoo.com")); using (WebResponse response = req.GetResponse()) { } } Console.WriteLine(string.Format("Req: {0}", UseIP.RequestCount)); Console.WriteLine(string.Format("Bind: {0}", UseIP.BindCount)); } 

问题可能是代表在每个新请求上重置。 试试以下:

 //servicePoint.BindIPEndPointDelegate = null; // Clears all delegates first, for testing servicePoint.BindIPEndPointDelegate += delegate { var address = IPAddress.Parse(this.IP); return new IPEndPoint(address, 0); }; 

另外据我所知,端点是缓存的,因此即使清除代理也可能在某些情况下无效,无论如何都可以重置。 您可以卸载/重新加载应用程序域作为最坏的情况。

我稍微改变了你的例子,让它在我的机器上工作:

 public HttpWebRequest CreateWebRequest(Uri uri) { HttpWebRequest wr = WebRequest.Create(uri) as HttpWebRequest; wr.ServicePoint.BindIPEndPointDelegate = new BindIPEndPoint(Bind); return wr; } 

我这样做是因为:

  • 我认为对FindServicePoint的调用实际上使用“default”ip执行请求,甚至没有调用绑定委托,也指向您指定的URI。 在我的机器中,至少, BindIPEndPointDelegate没有以您呈现的方式调用(我知道请求是因为我没有设置代理并且得到代理身份validation错误);
  • 在ServicePointManager的文档中,它声明“如果该主机和方案存在现有的ServicePoint对象,则ServicePointManager对象将返回现有的ServicePoint对象;否则,ServicePointManager对象将创建一个新的ServicePoint对象”,该对象可能会始终返回ServicePoint,如果URI相同(可能解释了为什么后续调用发生在同一个EndPoint中)。
  • 通过这种方式,我们可以确定,即使已经请求了URI,它也将使用所需的IP,而不是使用ServicePointManager之前的某些“缓存”。

我喜欢这个新类UseIP

指定要与WCF客户端一起使用的传出IP地址有关保护自己免受IPv4 / IPv6差异的问题。

唯一需要改变的是Bind方法是这样的:

 private IPEndPoint Bind(ServicePoint servicePoint, IPEndPoint remoteEndPoint, int retryCount) { if ((null != IP) && (IP.AddressFamily == remoteEndPoint.AddressFamily)) return new IPEndPoint(this.IP, 0); if (AddressFamily.InterNetworkV6 == remoteEndPoint.AddressFamily) return new IPEndPoint(IPAddress.IPv6Any, 0); return new IPEndPoint(IPAddress.Any, 0); } 

re: 多次调用绑定方法

对我有用的是在添加之前删除任何委托链接。

 ServicePoint servicePoint = ServicePointManager.FindServicePoint(uri); servicePoint.BindIPEndPointDelegate -= this.Bind; // avoid duplicate calls to Bind servicePoint.BindIPEndPointDelegate += this.Bind; 

我也喜欢缓存UseIP对象的想法。 所以我将这个静态方法添加到UseIP类中。

 private static Dictionary _eachNIC = new Dictionary(); public static UseIP ForNIC(IPAddress nic) { lock (_eachNIC) { UseIP useIP = null; if (!_eachNIC.TryGetValue(nic, out useIP)) { useIP = new UseIP(nic); _eachNIC.Add(nic, useIP); } return useIP; } }