Привязка IP-адреса работает только в первый раз
Я хочу сделать веб-запрос с одного из доступных 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
Но решение работает с первого раза!
4 ответа
Теория:
HttpWebRequest опирается на базовую ServicePoint. ServicePoint представляет фактическое соединение с URL. Во многом аналогично тому, как ваш браузер поддерживает соединение с URL-адресом открытым между запросами и повторно использует это соединение (чтобы исключить накладные расходы по открытию и закрытию соединения с каждым запросом), ServicePoint выполняет ту же функцию для HttpWebRequest.
Я думаю, что BindIPEndPointDelegate, который вы устанавливаете для ServicePoint, не вызывается при каждом использовании HttpWebRequest, потому что ServicePoint повторно использует соединение. Если вы можете принудительно закрыть соединение, то при следующем вызове этого URL-адреса ServicePoint потребуется снова вызвать BindIPEndPointDelegate.
К сожалению, похоже, что интерфейс ServicePoint не позволяет напрямую принудительно закрыть соединение.
Два решения (каждое с немного отличающимися результатами)
1) Для каждого запроса установите HttpWebRequest.KeepAlive = false. В моем тесте это привело к тому, что делегат Bind вызывался один на один с каждым запросом.
2) Установите для свойства ServicePoint ConnectionLeaseTimeout ноль или небольшое значение. Это будет периодически приводить к вызову делегата Bind (не один к одному с каждым запросом).
Из документации:
Вы можете использовать это свойство, чтобы гарантировать, что активные соединения объекта ServicePoint не остаются открытыми бесконечно. Это свойство предназначено для сценариев, в которых соединения должны периодически сбрасываться и восстанавливаться, например, для сценариев балансировки нагрузки.
По умолчанию, когда KeepAlive имеет значение true для запроса, свойство MaxIdleTime устанавливает время ожидания для закрытия подключений ServicePoint из-за неактивности. Если у ServicePoint есть активные соединения, MaxIdleTime не имеет никакого эффекта, и соединения остаются открытыми в течение неопределенного времени.
Если для свойства ConnectionLeaseTimeout установлено значение, отличное от -1, и по истечении указанного времени активное соединение ServicePoint закрывается после обслуживания запроса, установив для KeepAlive значение 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;
}
}
Следующие (базовые) результаты теста при вызове делегата 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
фактически выполняет запрос, используя ip "по умолчанию", даже не вызывая делегат привязки к указанному вами URI. В моей машине, по крайней мере,BindIPEndPointDelegate
не был вызван так, как вы представили (я знаю, что запрос был сделан, потому что я не установил прокси и получил ошибку аутентификации прокси); - В документации ServicePointManager говорится, что "если для этого хоста и схемы существует существующий объект ServicePoint, объект ServicePointManager возвращает существующий объект ServicePoint; в противном случае объект ServicePointManager создает новый объект ServicePoint", который, вероятно, будет всегда возвращать то же самое ServicePoint, если URI был тем же (возможно, объясняя, почему последующие вызовы происходят в той же конечной точке).
- Таким образом, мы можем быть уверены, что даже когда URI уже был запрошен, он будет использовать нужный IP-адрес вместо некоторого предыдущего "кэширования"
ServicePointManager
,
Мне нравится этот новый класс UseIP.
В пункте " Укажите исходящий IP-адрес для использования с клиентом WCF" есть смысл защитить себя от различий в 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;
}
}