11

我想从服务器上的一个可用 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

但解决方案只是第一次起作用!

4

4 回答 4

16

一个理论:

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

我认为您为 ServicePoint 设置的 BindIPEndPointDelegate 不会在每次使用 HttpWebRequest 时被调用,因为 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;
    }
}

以下(基本)测试导致每个请求都调用 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));
    }
于 2011-05-28T23:33:58.137 回答
1

问题可能在于委托在每个新请求上都被重置。试试下面:

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

另外据我所知,端点是缓存的,所以在某些情况下即使清除委托也可能不起作用,无论如何它们都可能被重置。作为最坏的情况,您可能会卸载/重新加载应用程序域。

于 2011-05-28T23:57:26.530 回答
0

我喜欢这个新课程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;
    }
}
于 2013-01-28T01:52:53.427 回答
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 对象返回现有的 ServicePoint 对象;否则,ServicePointManager 对象将创建一个新的 ServicePoint 对象”女巫可能会返回始终相同如果 URI 相同,则为 ServicePoint(也许可以解释为什么后续调用发生在同一个 EndPoint 中)。
  • 通过这种方式,我们可以确定,即使已经请求了 URI,它也会使用所需的 IP,而不是使用ServicePointManager.
于 2011-05-27T14:32:08.410 回答