Binding an IP address just works for the first time

Xaqron picture Xaqron · May 24, 2011 · Viewed 9.6k times · Source

I want to make a web request from one of available IP addresses on server so I use this class:

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

Then:

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

But the solution just works the first time!

Answer

Joe Enzminger picture Joe Enzminger · May 29, 2011

A theory:

HttpWebRequest relies on an underlying ServicePoint. The ServicePoint represents the actual connection to the URL. Much in the same way your browser keeps a connection to a URL open between requests and reuses that connection (to eliminate the overhead of opening and closing the connection with each request), ServicePoint performs the same function for HttpWebRequest.

I think that the BindIPEndPointDelegate that you are setting for the ServicePoint is not being called on each use of HttpWebRequest because the ServicePoint is reusing the connection. If you could force the connection to close, then the next call to that URL should cause the ServicePoint to need to call BindIPEndPointDelegate again.

Unfortunately, it doesn't appear that the ServicePoint interface gives you the ability to directly force a connection to close.

Two solutions (each with slightly different results)

1) For each request, set HttpWebRequest.KeepAlive = false. In my test, this caused the Bind delegate to get called one-for-one with each request.

2) Set the ServicePoint ConnectionLeaseTimeout property to zero or some small value. This will have the effect of periodically forcing the Bind delegate to be called (not one-for-one with each request).

From the documentation:

You can use this property to ensure that a ServicePoint object's active connections do not remain open indefinitely. This property is intended for scenarios where connections should be dropped and reestablished periodically, such as load balancing scenarios.

By default, when KeepAlive is true for a request, the MaxIdleTime property sets the time-out for closing ServicePoint connections due to inactivity. If the ServicePoint has active connections, MaxIdleTime has no effect and the connections remain open indefinitely.

When the ConnectionLeaseTimeout property is set to a value other than -1, and after the specified time elapses, an active ServicePoint connection is closed after servicing a request by setting KeepAlive to false in that request.

Setting this value affects all connections managed by the ServicePoint object.

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

The following (basic) test results in the Bind delegate getting called for each request:

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