绑定IP地址仅在第一次有效。

11

我想从服务器的可用IP地址之一发起网络请求,因此我使用了这个类:

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

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


是的,我想快速更改我的IP地址。我应该采取什么方法? - Xaqron
你可以尝试只在第一次绑定时进行,并将其保存在静态或实例变量中。 - Cilvic
@alexD:不是的。只有第一个绑定的IP有效。后续的UseIP类实例将使用相同的IP地址。 - Xaqron
赏金将授予解决“频繁更改Web请求的IP地址”的方案。类似于可行的“UseIP”类。 - Xaqron
我不确定哪里出了问题。在测试项目中运行代码示例(转换为VB.NET),它按预期工作。CreateWebRequest方法返回的IP地址具有更新后的IP地址。我没有将我的示例发布为答案,因为我似乎没有做任何特殊的事情。 - Frazell Thomas
显示剩余2条评论
4个回答

17

一种理论:

HttpWebRequest 依赖于基础的 ServicePoint。ServicePoint 表示与 URL 的实际连接。就像您的浏览器在请求之间保持与 URL 的连接并重用该连接(以消除每个请求打开和关闭连接的开销)一样,ServicePoint 为 HttpWebRequest 执行相同的功能。

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

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

两种解决方案(每种方案都有稍微不同的结果)

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

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));
    }

总结 - 如果您希望每个请求都来自一个新的IP地址,请确保HttpWebRequest.KeepAlive对于每个请求为false。由于您需要在每个请求中打开和关闭连接,因此性能会受到影响。如果您希望偶尔强制使用给定URI的新IP地址,则可以使用ConnectionLeaseTimeout。 - Joe Enzminger
@Joe:我已经测试了将HttpWebRequestKeepAlive设置为false。一些网站拒绝为这样的客户提供服务。 - Xaqron
好的 - 如果是这种情况,您有两个选择:使用ConnectionLeaseTimeout=0。您偶尔会重用IP地址,但大约60%(根据我的测试)的请求将调用Bind委托。如果这不可接受,则无法使用HttpWebRequest。您需要编写自己的Web客户端版本,该版本发送KeepAlive标头但在请求后关闭连接。您可以使用System.Net.Socket类来实现此操作。 - Joe Enzminger
@Joe:那么欺骗远程服务器怎么样?使用没有“KeepAlive”的HttpWebServer,但手动设置HTTP标头呢? - Xaqron
@Xaqron:当我尝试使用req.Headers.Add("Connection", "Keep-Alive");时,它会抛出一个异常,说你必须使用适当的属性。我不认为可以手动设置这个特定的头文件。我觉得有些服务器拒绝没有Keep-Alive头的请求很奇怪——就HTTP协议而言,他们不应该依赖它。这让我想到了一个建议,为了获得所需的行为,你可能需要编写自己的HttpWebRequest实现。 - Joe Enzminger
@Joe:使用反射有一个技巧可以设置这些头文件。 - Xaqron

1

问题可能出在每个新请求上委托被重置了。请尝试以下操作:

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

据我所知,端点是被缓存的,因此即使清除委托可能在某些情况下也无法起作用,并且它们可能会被重置。最坏的情况下,您可以卸载/重新加载应用程序域。


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之前的某些缓存。

我以前试过这个。如果你快速更改IP地址,你会发现它不起作用。问题在于委托应该是“静态的”,因此您不能同时从不同的IP地址连接相同的“Uri”。 - Xaqron
回复:对于相同的Uri,它很可能总是返回相同的ServicePoint。没错!奇怪的是,它会为任何指向相同远程IPAddress的Uri返回相同的ServicePoint。例如,http://1.2.3.4/FirstTarget和http://1.2.3.4/SecondTarget会返回相同的ServicePoint。 - Jesse Chisholm

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);
}

关于多次调用绑定方法的问题:

对我有效的做法是在添加之前移除任何委托链接。

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;
    }
}

抱歉,我把属性IP的类型改成了IPAddress,这样我就不必每次都解析它了。我忘记提到这一点了。 - Jesse Chisholm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接