C# HTTPプロキシ完全ガイド:.NET 8+でのHttpClient実装ベストプラクティス

.NET 8+でHttpClientを使用したHTTPプロキシ設定の完全ガイド。HttpClientHandler、SocketsHttpHandler、Pollyによるリトライ、並行スクレイピング、ローテーションプロキシプールの実装方法を解説。

C# HTTPプロキシ完全ガイド:.NET 8+でのHttpClient実装ベストプラクティス

はじめに:.NETでプロキシを扱うのが意外と難しい理由

.NET開発者がHTTPプロキシを扱う際、最も一般的な問題は「設定したはずなのにプロキシが使われない」「接続がタイムアウトする」「IPがブロックされる」の3つです。これらは全て、HttpClientの内部動作を理解していないことが根本原因です。

本記事では、C# HTTPプロキシの設定から、認証、バイパスリスト、接続プーリングのチューニング、リトライ戦略、並行スクレイピング、そしてローテーションプロキシプールの構築まで、実践的なコードと共に解説します。.NET 8+を前提とし、最新のAPIを活用します。

1. HttpClientHandlerとWebProxy:基本設定

HttpClientでプロキシを使用する最も基本的な方法は、HttpClientHandlerWebProxyを組み合わせることです。このセクションでは、認証付きプロキシの設定と、特定のホストをプロキシから除外するバイパスリストの実装を示します。

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

public class BasicProxyExample
{
    private readonly HttpClient _httpClient;

    public BasicProxyExample()
    {
        // ProxyHatの認証情報
        var proxyUrl = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080";
        var webProxy = new WebProxy(proxyUrl)
        {
            // ローカルアドレスをバイパス(開発環境用)
            BypassProxyOnLocal = true,
            // 特定のホストをプロキシから除外
            BypassList = new string[]
            {
                "localhost",
                "127.0.0.1",
                "*.internal.example.com",
                "10.*",
                "192.168.*"
            }
        };

        var handler = new HttpClientHandler
        {
            Proxy = webProxy,
            UseProxy = true,
            // 認証情報がURLに含まれる場合、自動的に使用される
            // 明示的に設定する場合:
            // Credentials = new NetworkCredential("user-country-US", "PASSWORD")
            UseDefaultCredentials = false,
            // SSL検証(開発環境では無効化可能、本番では有効にすること)
            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
        };

        _httpClient = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }

    public async Task<string> FetchAsync(string url)
    {
        try
        {
            var response = await _httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Request failed: {ex.Message}");
            throw;
        }
    }

    // 使用例
    public static async Task Main()
    {
        var example = new BasicProxyExample();
        var content = await example.FetchAsync("https://httpbin.org/ip");
        Console.WriteLine(content);
    }
}

認証情報の安全な管理

認証情報をコードにハードコードすることは避けてください。.NETの設定APIを使用して、環境変数やappsettings.jsonから読み込むのがベストプラクティスです。

// appsettings.json
// {
//   "ProxySettings": {
//     "Host": "gate.proxyhat.com",
//     "Port": 8080,
//     "Username": "user-country-US",
//     "Password": "your-password"
//   }
// }

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

public class ProxySettings
{
    public string Host { get; set; } = "gate.proxyhat.com";
    public int Port { get; set; } = 8080;
    public string Username { get; set; } = "";
    public string Password { get; set; } = "";
}

public class SecureProxyExample
{
    private readonly HttpClient _httpClient;

    public SecureProxyExample(IOptions<ProxySettings> settings)
    {
        var proxySettings = settings.Value;
        var proxyUrl = $"http://{proxySettings.Username}:{proxySettings.Password}@{proxySettings.Host}:{proxySettings.Port}";
        
        var handler = new HttpClientHandler
        {
            Proxy = new WebProxy(proxyUrl),
            UseProxy = true
        };

        _httpClient = new HttpClient(handler);
    }
}

2. SocketsHttpHandler:高度な接続管理とリクエストごとのプロキシ

.NET Core 2.1以降、SocketsHttpHandlerが既定のHTTPハンドラーとなりました。HttpClientHandlerよりも細かい制御が可能で、接続プーリングのチューニングやリクエストごとのプロキシ設定に適しています。

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class AdvancedProxyExample
{
    private readonly HttpMessageHandler _handler;
    private readonly HttpClient _httpClient;

    public AdvancedProxyExample()
    {
        _handler = new SocketsHttpHandler
        {
            // 接続プールのライフタイム(デフォルトは無制限)
            // ローテーションプロキシでは短く設定
            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
            // アイドル接続のタイムアウト
            PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
            // 最大接続数
            MaxConnectionsPerServer = 100,
            // 自動リダイレクト
            AllowAutoRedirect = true,
            MaxAutomaticRedirections = 10,
            // SSL検証
            SslOptions = new SslClientAuthenticationOptions
            {
                // カスタム検証ロジック(後述)
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true
            },
            // プロキシ設定(リクエストごとに動的に変更可能)
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
        };

        _httpClient = new HttpClient(_handler)
        {
            Timeout = TimeSpan.FromSeconds(60)
        };
    }

    // リクエストごとに異なるプロキシを使用する場合
    public async Task<string> FetchWithDynamicProxyAsync(
        string url, 
        string proxyUsername, 
        string proxyPassword,
        CancellationToken ct = default)
    {
        // リクエストごとにプロキシを設定
        var request = new HttpRequestMessage(HttpMethod.Get, url);
        
        // Proxy-Authorizationヘッダーを手動で設定
        var credentials = Convert.ToBase64String(
            System.Text.Encoding.ASCII.GetBytes($"{proxyUsername}:{proxyPassword}"));
        request.Headers.Add("Proxy-Authorization", $"Basic {credentials}");

        var response = await _httpClient.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync(ct);
    }

    // セッションIDを使用したスティッキーセッション
    public async Task<string> FetchWithSessionAsync(
        string url,
        string sessionId,
        CancellationToken ct = default)
    {
        // ProxyHatでは、ユーザー名にセッションIDを含める
        var proxyUrl = $"http://user-session-{sessionId}:PASSWORD@gate.proxyhat.com:8080";
        
        // 新しいハンドラーを作成(またはハンドラープールから取得)
        using var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy(proxyUrl),
            PooledConnectionLifetime = TimeSpan.FromMinutes(5)
        };
        using var client = new HttpClient(handler);
        
        var response = await client.GetAsync(url, ct);
        return await response.Content.ReadAsStringAsync(ct);
    }
}

PooledConnectionLifetimeの重要性

ローテーションプロキシを使用する場合、PooledConnectionLifetimeの設定が重要です。既定では、HttpClientは接続を再利用するため、プロキシがIPをローテーションしても、既存の接続経由でリクエストが送られ、同じIPが使用され続ける可能性があります。接続のライフタイムをプロキシのローテーション間隔より短く設定することで、新しいIPを取得できます。

3. Pollyによるリトライとサーキットブレーカー

スクレイピングやAPI呼び出しでは、一時的な障害(429 Too Many Requests、503 Service Unavailable、タイムアウト)に対処するためにリトライ戦略が不可欠です。Pollyは.NETの標準的なレジリエンスライブラリです。

using System;
using System.Net;
using System.Net.Http;
using Polly;
using Polly.Retry;
using Polly.CircuitBreaker;
using Polly.Contrib.WaitAndRetry;

public class ResilientProxyClient
{
    private readonly HttpClient _httpClient;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;
    private readonly AsyncCircuitBreakerPolicy<HttpResponseMessage> _circuitBreakerPolicy;

    public ResilientProxyClient()
    {
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://user-country-US:PASSWORD@gate.proxyhat.com:8080"),
            PooledConnectionLifetime = TimeSpan.FromMinutes(1)
        };
        _httpClient = new HttpClient(handler);

        // リトライポリシー:指数バックオフ + ジッター
        var delays = Backoff.DecorrelatedJitterBackoffV2(
            medianFirstRetryDelay: TimeSpan.FromSeconds(1),
            retryCount: 5);
        
        _retryPolicy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => 
                r.StatusCode == HttpStatusCode.TooManyRequests ||
                r.StatusCode == HttpStatusCode.ServiceUnavailable ||
                r.StatusCode == HttpStatusCode.BadGateway ||
                r.StatusCode == HttpStatusCode.GatewayTimeout)
            .WaitAndRetryAsync(delays, onRetry: (outcome, timeSpan, retryCount, context) =>
            {
                Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalSeconds:F1}s due to {outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()}");
            });

        // サーキットブレーカー:5回連続失敗で30秒間遮断
        _circuitBreakerPolicy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => !r.IsSuccessStatusCode)
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (outcome, breakDuration) =>
                {
                    Console.WriteLine($"Circuit broken for {breakDuration.TotalSeconds}s");
                },
                onReset: () =>
                {
                    Console.WriteLine("Circuit reset");
                });
    }

    public async Task<string> FetchWithRetryAsync(string url, CancellationToken ct = default)
    {
        // リトライ → サーキットブレーカーの順でラップ
        var strategy = Policy.WrapAsync(_retryPolicy, _circuitBreakerPolicy);
        
        var response = await strategy.ExecuteAsync(async () =>
        {
            var result = await _httpClient.GetAsync(url, ct);
            result.EnsureSuccessStatusCode();
            return result;
        });

        return await response.Content.ReadAsStringAsync(ct);
    }

    // プロキシローテーションと組み合わせたリトライ
    public async Task<string> FetchWithProxyRotationAsync(
        string url, 
        string[] proxyCredentials,
        CancellationToken ct = default)
    {
        var proxyIndex = 0;
        var delays = Backoff.DecorrelatedJitterBackoffV2(
            medianFirstRetryDelay: TimeSpan.FromSeconds(2),
            retryCount: proxyCredentials.Length);

        var response = await _retryPolicy.ExecuteAsync(async () =>
        {
            // 各リトライで異なるプロキシを使用
            var credentials = proxyCredentials[proxyIndex++];
            var proxyUrl = $"http://{credentials}@gate.proxyhat.com:8080";
            
            // 新しいクライアントを作成(またはプールから取得)
            using var handler = new SocketsHttpHandler
            {
                Proxy = new WebProxy(proxyUrl),
                PooledConnectionLifetime = TimeSpan.FromSeconds(30)
            };
            using var client = new HttpClient(handler);
            
            var result = await client.GetAsync(url, ct);
            result.EnsureSuccessStatusCode();
            return result;
        });

        return await response.Content.ReadAsStringAsync(ct);
    }
}

4. Parallel.ForEachAsync:並行スクレイピングの実装

大量のページをスクレイピングする場合、Parallel.ForEachAsyncを使用して並行処理を行います。.NET 6以降で導入されたこのAPIは、非同期処理の並行度を簡単に制御できます。

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class ParallelScrapingExample
{
    private readonly HttpClient _httpClient;
    private readonly SemaphoreSlim _rateLimiter;

    public ParallelScrapingExample(int maxConcurrentRequests = 20)
    {
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://user-country-US:PASSWORD@gate.proxyhat.com:8080"),
            PooledConnectionLifetime = TimeSpan.FromMinutes(1),
            MaxConnectionsPerServer = maxConcurrentRequests
        };
        _httpClient = new HttpClient(handler);
        
        // レート制限用セマフォ
        _rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
    }

    public async Task<Dictionary<string, string>> ScrapeUrlsAsync(
        IEnumerable<string> urls,
        CancellationToken ct = default)
    {
        var results = new ConcurrentDictionary<string, string>();
        var errors = new ConcurrentBag<Exception>();

        // 並行度を制御して処理
        await Parallel.ForEachAsync(urls, new ParallelOptions
        {
            MaxDegreeOfParallelism = 20,
            CancellationToken = ct
        }, async (url, token) =>
        {
            await _rateLimiter.WaitAsync(token);
            try
            {
                var content = await FetchWithRetryAsync(url, token);
                results.TryAdd(url, content);
            }
            catch (Exception ex)
            {
                errors.Add(ex);
                Console.WriteLine($"Failed to fetch {url}: {ex.Message}");
            }
            finally
            {
                _rateLimiter.Release();
            }
        });

        if (errors.Count > 0)
        {
            Console.WriteLine($"{errors.Count} requests failed");
        }

        return results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }

    private async Task<string> FetchWithRetryAsync(string url, CancellationToken ct)
    {
        int retryCount = 0;
        const int maxRetries = 3;
        
        while (retryCount < maxRetries)
        {
            try
            {
                var response = await _httpClient.GetAsync(url, ct);
                if (response.StatusCode == HttpStatusCode.TooManyRequests)
                {
                    var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(5);
                    await Task.Delay(retryAfter, ct);
                    retryCount++;
                    continue;
                }
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync(ct);
            }
            catch (HttpRequestException) when (retryCount < maxRetries - 1)
            {
                retryCount++;
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)), ct);
            }
        }
        throw new HttpRequestException($"Max retries exceeded for {url}");
    }

    // 使用例
    public static async Task Main()
    {
        var scraper = new ParallelScrapingExample();
        var urls = Enumerable.Range(1, 100)
            .Select(i => $"https://example.com/page/{i}");
        
        var results = await scraper.ScrapeUrlsAsync(urls);
        Console.WriteLine($"Successfully fetched {results.Count} pages");
    }
}

並行度の最適化

並行度は、プロキシプロバイダーの制限、ターゲットサイトの容量、ネットワーク帯域幅を考慮して設定する必要があります。一般的には、HttpClientHandler.MaxConnectionsPerServerParallelOptions.MaxDegreeOfParallelismを同じ値に設定し、10〜50の範囲で調整します。

5. DI対応ローテーションプロキシプールサービス

実用的なアプリケーションでは、依存性注入(DI)を使用してプロキシプールを管理します。以下は、Microsoft.Extensions.DependencyInjectionと統合した実装例です。

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

// 設定オプション
public class ProxyPoolOptions
{
    public string GatewayHost { get; set; } = "gate.proxyhat.com";
    public int HttpPort { get; set; } = 8080;
    public int Socks5Port { get; set; } = 1080;
    public string[] Countries { get; set; } = Array.Empty<string>();
    public string BaseUsername { get; set; } = "";
    public string Password { get; set; } = "";
    public int PoolSize { get; set; } = 10;
    public TimeSpan RotationInterval { get; set; } = TimeSpan.FromMinutes(5);
    public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
}

// プロキシエントリ
public class ProxyEntry
{
    public string SessionId { get; set; } = Guid.NewGuid().ToString("N")[..8];
    public string Country { get; set; } = "US";
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public int RequestCount { get; set; }
    public bool IsHealthy { get; set; } = true;
}

// プロキシプールサービスインターフェース
public interface IProxyPoolService
{
    Task<HttpClient> GetClientAsync(CancellationToken ct = default);
    void ReturnClient(HttpClient client);
    Task RotateAsync();
}

// プロキシプールサービス実装
public class ProxyPoolService : IProxyPoolService, IDisposable
{
    private readonly ProxyPoolOptions _options;
    private readonly ILogger<ProxyPoolService> _logger;
    private readonly ConcurrentBag<HttpClient> _clientPool;
    private readonly ConcurrentDictionary<string, ProxyEntry> _activeProxies;
    private readonly Timer _rotationTimer;
    private int _roundRobinIndex = 0;

    public ProxyPoolService(
        IOptions<ProxyPoolOptions> options, 
        ILogger<ProxyPoolService> logger)
    {
        _options = options.Value;
        _logger = logger;
        _clientPool = new ConcurrentBag<HttpClient>();
        _activeProxies = new ConcurrentDictionary<string, ProxyEntry>();

        // 初期プール作成
        InitializePool();

        // 定期的なローテーション
        _rotationTimer = new Timer(
            _ => _ = RotateAsync(), 
            null, 
            _options.RotationInterval, 
            _options.RotationInterval);
    }

    private void InitializePool()
    {
        for (int i = 0; i < _options.PoolSize; i++)
        {
            var entry = CreateProxyEntry();
            var client = CreateClient(entry);
            _clientPool.Add(client);
            _activeProxies.TryAdd(entry.SessionId, entry);
        }
        _logger.LogInformation("Initialized proxy pool with {Count} proxies", _options.PoolSize);
    }

    private ProxyEntry CreateProxyEntry()
    {
        var country = _options.Countries.Length > 0 
            ? _options.Countries[Interlocked.Increment(ref _roundRobinIndex) % _options.Countries.Length]
            : "US";

        return new ProxyEntry
        {
            SessionId = Guid.NewGuid().ToString("N")[..8],
            Country = country,
            CreatedAt = DateTime.UtcNow
        };
    }

    private HttpClient CreateClient(ProxyEntry entry)
    {
        var proxyUrl = $"http://{_options.BaseUsername}-country-{entry.Country}-session-{entry.SessionId}:{_options.Password}@{_options.GatewayHost}:{_options.HttpPort}";

        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy(proxyUrl),
            PooledConnectionLifetime = _options.RotationInterval,
            PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
            MaxConnectionsPerServer = 10,
            ConnectTimeout = TimeSpan.FromSeconds(10)
        };

        return new HttpClient(handler)
        {
            Timeout = _options.RequestTimeout
        };
    }

    public async Task<HttpClient> GetClientAsync(CancellationToken ct = default)
    {
        if (_clientPool.TryTake(out var client))
        {
            return client;
        }

        // プールが空の場合は新しいクライアントを作成
        var entry = CreateProxyEntry();
        _activeProxies.TryAdd(entry.SessionId, entry);
        return CreateClient(entry);
    }

    public void ReturnClient(HttpClient client)
    {
        _clientPool.Add(client);
    }

    public async Task RotateAsync()
    {
        _logger.LogInformation("Rotating proxy pool...");
        
        // 古いクライアントを破棄
        while (_clientPool.TryTake(out var oldClient))
        {
            oldClient.Dispose();
        }

        // 新しいエントリで再初期化
        _activeProxies.Clear();
        InitializePool();
        
        await Task.CompletedTask;
    }

    public void Dispose()
    {
        _rotationTimer?.Dispose();
        foreach (var client in _clientPool)
        {
            client.Dispose();
        }
    }
}

// DI登録
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddProxyPool(
        this IServiceCollection services,
        Action<ProxyPoolOptions> configure)
    {
        services.Configure(configure);
        services.AddSingleton<IProxyPoolService, ProxyPoolService>();
        return services;
    }
}

// 使用例
public class ScraperService
{
    private readonly IProxyPoolService _proxyPool;

    public ScraperService(IProxyPoolService proxyPool)
    {
        _proxyPool = proxyPool;
    }

    public async Task<string> FetchAsync(string url, CancellationToken ct = default)
    {
        var client = await _proxyPool.GetClientAsync(ct);
        try
        {
            var response = await client.GetAsync(url, ct);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync(ct);
        }
        finally
        {
            _proxyPool.ReturnClient(client);
        }
    }
}

// Program.cs
// var builder = Host.CreateApplicationBuilder(args);
// builder.Services.AddProxyPool(options =>
// {
//     options.BaseUsername = "user";
//     options.Password = "your-password";
//     options.Countries = new[] { "US", "DE", "JP", "GB" };
//     options.PoolSize = 20;
//     options.RotationInterval = TimeSpan.FromMinutes(10);
// });
// builder.Services.AddTransient<ScraperService>();

6. TLS設定:証明書ピンニングとカスタムルートCA

セキュリティ要件が厳しい環境や、自己署名証明書を使用する内部システムとの通信では、TLSのカスタマイズが必要です。SocketsHttpHandlerSslOptionsを使用して、証明書の検証ロジックを制御します。

using System;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

public class TlsConfigurationExample
{
    private readonly HttpClient _httpClient;

    public TlsConfigurationExample()
    {
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://user-country-US:PASSWORD@gate.proxyhat.com:8080"),
            SslOptions = new SslClientAuthenticationOptions
            {
                // カスタム証明書検証
                RemoteCertificateValidationCallback = ValidateServerCertificate,
                // クライアント証明書(必要な場合)
                ClientCertificates = new X509Certificate2Collection()
            }
        };

        _httpClient = new HttpClient(handler);
    }

    // 証明書ピンニングの実装
    private static readonly HashSet<string> PinnedCertificates = new()
    {
        // 期待される証明書の公開鍵ハッシュ(SHA-256)
        "A1B2C3D4E5F6...", // 実際のハッシュに置き換え
        "G7H8I9J0K1L2..."
    };

    private static bool ValidateServerCertificate(
        object sender,
        X509Certificate? certificate,
        X509Chain? chain,
        SslPolicyErrors sslPolicyErrors)
    {
        // 開発環境では全て許可(本番では使用しないこと)
        // return true;

        if (certificate == null)
        {
            Console.WriteLine("No certificate provided");
            return false;
        }

        // 証明書ピンニング:公開鍵ハッシュを確認
        var certBytes = certificate.GetPublicKey();
        using var sha256 = System.Security.Cryptography.SHA256.Create();
        var hashBytes = sha256.ComputeHash(certBytes);
        var hashString = BitConverter.ToString(hashBytes).Replace("-", "");

        if (PinnedCertificates.Contains(hashString))
        {
            return true;
        }

        // ピンニングに失敗した場合、標準の検証を実行
        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            return true;
        }

        Console.WriteLine($"Certificate validation failed: {sslPolicyErrors}");
        return false;
    }

    // カスタムルートCAを使用する場合
    public static HttpClient CreateClientWithCustomRootCA(string rootCaPath)
    {
        var rootCa = new X509Certificate2(rootCaPath);
        
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://user-country-US:PASSWORD@gate.proxyhat.com:8080"),
            SslOptions = new SslClientAuthenticationOptions
            {
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
                {
                    if (cert == null || chain == null)
                    {
                        return false;
                    }

                    // カスタムルートCAを信頼チェーンに追加
                    chain.ChainPolicy.ExtraStore.Add(rootCa);
                    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
                    
                    var isValid = chain.Build(cert as X509Certificate2 ?? new X509Certificate2(cert));
                    return isValid;
                }
            }
        };

        return new HttpClient(handler);
    }

    // 特定のホストに対してのみ証明書検証をスキップ(開発用)
    public static HttpClient CreateClientWithBypassForDevelopment()
    {
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://user-country-US:PASSWORD@gate.proxyhat.com:8080"),
            SslOptions = new SslClientAuthenticationOptions
            {
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
                {
                    // 本番環境では使用しないこと
                    var httpRequestMessage = sender as HttpRequestMessage;
                    var host = httpRequestMessage?.RequestUri?.Host;
                    
                    if (host == "localhost" || host == "internal-dev.example.com")
                    {
                        return true; // 開発環境の自己署名証明書を許可
                    }
                    
                    return errors == SslPolicyErrors.None;
                }
            }
        };

        return new HttpClient(handler);
    }

    // 使用例
    public static async Task Main()
    {
        var example = new TlsConfigurationExample();
        var clientWithCustomCA = CreateClientWithCustomRootCA("/path/to/root-ca.crt");
        var devClient = CreateClientWithBypassForDevelopment();
        
        // 本番APIへのリクエスト
        var response = await example._httpClient.GetAsync("https://api.example.com/data");
        Console.WriteLine(await response.Content.ReadAsStringAsync());
    }
}

プロキシタイプの比較

C#でのプロキシ使用において、どのプロキスタイプを選択するかは重要な決定です。以下の表は、主要なプロキスタイプの特徴を比較しています。

プロキスタイプ速度検出困難度コストユースケース
データセンタープロキシ高速低(検出されやすい)制限の少ないサイト、API呼び出し
レジデンシャルプロキシ中程度高(本物のISP IP)SERP、eコマース、ソーシャルメディア
モバイルプロキシ可変最高(キャリアIP)最高厳格なボット検出、アカウント管理
SOCKS5プロキシ高速中程度中程度TCP接続、非HTTPプロトコル

C# residential proxiesを使用する場合、user-country-USのような国指定でターゲット地域を設定できます。詳細なロケーション設定についてはプロキシロケーションを参照してください。

Key Takeaways

C# HTTPプロキシ実装の重要ポイント:

  • HttpClientのライフ管理HttpClientは再利用する設計ですが、ローテーションプロキシではPooledConnectionLifetimeを設定して接続を定期的に更新する
  • SocketsHttpHandlerの活用HttpClientHandlerより高度な制御が可能で、接続プーリング、TLS設定、プロキシ設定をきめ細かく調整できる
  • レジリエンスパターン:Pollyで指数バックオフとジッターを組み合わせたリトライ、サーキットブレーカーを実装し、一時的な障害に対処する
  • 並行処理の制御Parallel.ForEachAsyncSemaphoreSlimで並行度を制御し、プロキシプロバイダーの制限を超えないようにする
  • DIパターン:プロキシプールをサービスとして抽象化し、設定、ローテーション、ヘルスチェックを一元管理する
  • TLS設定:証明書ピンニングやカスタムルートCAが必要な場合、SslClientAuthenticationOptionsで検証ロジックをカスタマイズする

まとめと次のステップ

.NET 8+でのHTTPプロキシ実装は、SocketsHttpHandler、Polly、DIを組み合わせることで、堅牢でスケーラブルなシステムを構築できます。特にスクレイピングや自動化タスクでは、プロキシのローテーション、リトライ戦略、並行度の制御が成功の鍵となります。

ProxyHatの料金プランでは、レジデンシャルプロキシ、データセンタープロキシ、モバイルプロキシを提供しています。WebスクレイピングSERPトラッキングのユースケースに合わせて最適なプランを選択できます。

実装に関するご質問や、特定のユースケースの相談については、サポートチームまでお問い合わせください。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る