问题描述
|
我想从服务器上的可用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执行相同的功能。
我认为每次使用HttpWebRequest时都不会调用您为ServicePoint设置的BindIPEndPointDelegate,因为ServicePoint正在重新使用连接。如果您可以强制关闭连接,则下一次对该URL的调用将导致ServicePoint需要再次调用BindIPEndPointDelegate。
不幸的是,似乎没有ServicePoint接口使您能够直接强制关闭连接。
两种解决方案(每种解决方案的结果略有不同)
1)对于每个请求,设置HttpWebRequest.KeepAlive = false。在我的测试中,这导致Bind委托在每个请求中被一对一调用。
2)将ServicePoint ConnectionLeaseTimeout属性设置为零或某个较小的值。这将具有定期强制调用Bind委托的效果(不是每个请求都一对一)。
从文档中:
您可以使用此属性来确保ServicePoint对象的
活动连接不会无限期保持打开状态。该属性是
适用于应该断开连接并
定期重新建立,例如负载平衡方案。
默认情况下,当请求的KeepAlive为true时,MaxIdleTime
属性设置由于以下原因而关闭ServicePoint连接的超时
不活动。如果ServicePoint具有活动连接,则MaxIdleTime
无效,连接将无限期保持打开状态。
当ConnectionLeaseTimeout属性设置为其他值时
-1,并且在经过指定的时间后,通过将KeepAlive设置为来服务请求后,将关闭活动的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;
}
}
以下(基本)测试结果将为每个请求调用Bind委托:
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。至少在我的机器上,没有以您提出的方式调用ѭ7(我知道请求是因为我没有设置Proxy并收到代理身份验证错误);
在ServicePointManager的文档中,它声明\“如果该主机和方案存在现有的ServicePoint对象,则ServicePointManager对象将返回现有的ServicePoint对象;否则,该ServicePointManager对象将创建一个新的ServicePoint对象\”可能会始终返回如果URI相同,则返回相同的ServicePoint(也许可以解释为什么随后的调用在同一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:Bind方法被多次调用。
对我有用的是在添加任何委托链接之前将其删除。
ServicePoint servicePoint = ServicePointManager.FindServicePoint(uri);
servicePoint.BindIPEndPointDelegate -= this.Bind; // avoid duplicate calls to Bind
servicePoint.BindIPEndPointDelegate += this.Bind;
我也喜欢缓存UseIP对象的想法。因此,我将此静态方法添加到UseIP类。
private static Dictionary<IPAddress,UseIP> _eachNIC = new Dictionary<IPAddress,UseIP>();
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;
}
}