我想从服务器上的一个可用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执行相同的功能。
我认为您为ServicePoint设置的BindIPEndPointDelegate不会在每次使用HttpWebRequest时被调用,因为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连接在该请求中为false。
设置此值会影响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
的调用实际上是使用"默认"ip来完成请求的,甚至不调用对您指定的URI的绑定委托。至少在我的机器中,BindIPEndPointDelegate
没有按照您所提供的方式调用(我知道请求是因为我没有设置代理,并且出现了代理身份验证错误) - 在ServicePointManager的文档中,它指出,"如果该主机和方案有一个现有的ServicePoint对象,ServicePointManager对象将返回现有的ServicePointobject;否则,ServicePointManager对象将创建一个新的ServicePoint object",如果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;
}
}