Использование прокси в Kotlin: Ktor Client, OkHttp и residential-прокси в продакшене

Практическое руководство по настройке residential-прокси в Kotlin через Ktor 3 и OkHttp: geo-таргетинг, sticky-сессии, SOCKS5, конкурентный скрапинг на корутинах и продакшен-харденинг.

Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp
В этой статье

Использование прокси в Kotlin — это базовый навык для Android- и backend-разработчиков, которым нужно собирать публичные данные, тестировать геозависимый контент или обходить ограничения по IP. В этом руководстве мы пройдём от минимального примера с OkHttpClient до полноценного скрапера на Ktor 3 с residential-прокси ProxyHat, geo-таргетингом, sticky-сессиями и конкурентными запросами на корутинах.

Зачем нужен Kotlin прокси и почему residential

Kotlin прокси — это не просто «прокинуть трафик через другой IP». Тип прокси определяет, пройдёт ли ваш запрос через антибот-системы. Datacenter-прокси выдают себя ASN-ами облачных провайдеров (AWS, Hetzner, OVH), которые крупные сайты и социальные платформы блокируют по умолчанию. Residential-прокси используют IP реальных домашних провайдеров, поэтому выглядят как обычные пользователи.

По данным Imperva Bad Bot Report 2024, на автоматизированный трафик приходится более 40% всего интернет-трафика, и антибот-системы всё чаще классифицируют запросы по репутации ASN. Если ваша цель — SERP, социальные сети или e-commerce, residential-прокси дают значительно более высокий success rate, чем datacenter.

Mobile-прокси — подмножество residential, где IP принадлежат мобильным операторам. Они ещё довереннее, но дороже. Datacenter-прокси остаются оптимальными для задач, где блокировка по ASN не критична: внутренний мониторинг, QA-тестирование, парсинг открытых API.

Технический контекст: как прокси-аутентификация работает в JVM

В JVM-стеке прокси-аутентификация — это не тривиальный HTTP-заголовок, а отдельный механизм. Когда HTTP-клиент подключается к прокси-серверу, прокси может ответить 407 Proxy Authentication Required с заголовком Proxy-Authenticate. Клиент должен ответить заголовком Proxy-Authorization. Для HTTP-прокси это обычно Basic-аутентификация, для SOCKS5 — встроенный в протокол механизм username/password (см. RFC 1929).

Проблема в том, что каждый HTTP-движок в Kotlin/JVM обрабатывает это по-своему:

  • OkHttp — поддерживает Authenticator для прокси-челленджей через proxyAuthenticator().
  • Ktor CIO engine — не имеет встроенного прокси-аутентификатора, заголовок нужно добавлять вручную.
  • Ktor OkHttp engine — делегирует в OkHttp, можно использовать нативный proxyAuthenticator.
  • java.net.Proxy + SOCKS5 — требует системных свойств java.net.socks.username и java.net.socks.password.

Поэтому универсального «одного способа» нет — подход зависит от движка и типа прокси.

Настройка проекта: Ktor 3 и OkHttp

Добавьте зависимости в build.gradle.kts:

// build.gradle.kts
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-cio:3.0.3")
implementation("io.ktor:ktor-client-okhttp:3.0.3")
implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")

Базовый OkHttpClient с прокси

Начнём с сырого OkHttp — это самый прозрачный способ увидеть, как прокси-аутентификация работает:

import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit

fun buildOkHttpProxyClient(
    proxyHost: String = "gate.proxyhat.com",
    proxyPort: Int = 8080,
    username: String,
    password: String
): OkHttpClient {
    val proxyAuth = Authenticator { _, response ->
        val credential = Credentials.basic(username, password)
        response.request.newBuilder()
            .header("Proxy-Authorization", credential)
            .build()
    }

    return OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort)))
        .proxyAuthenticator(proxyAuth)
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .build()
}

fun main() {
    val client = buildOkHttpProxyClient(
        username = "user-country-DE-city-berlin",
        password = "YOUR_PASSWORD"
    )
    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

    client.newCall(request).execute().use { response ->
        println(response.code)
        println(response.body?.string())
    }
}

Здесь user-country-DE-city-berlin — это geo-таргетинг, закодированный в username. ProxyHat читает флаги из username, поэтому вам не нужны отдельные параметры. Для sticky-сессии добавьте -session-abc123: user-country-DE-city-berlin-session-abc123.

Ktor 3 HttpClient на CIO engine

Ktor CIO — чисто Kotlin-движок без зависимости от OkHttp. Удобен для серверных и кроссплатформенных проектов. Но CIO не поддерживает автоматический Proxy-Authorization, поэтому заголовок нужно добавлять вручную через defaultRequest:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64

fun buildKtorCioProxyClient(
    username: String,
    password: String
): HttpClient {
    val proxyAuthHeader = "Basic " +
        Base64.getEncoder().encodeToString("$username:$password".toByteArray())

    return HttpClient(CIO) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
        }
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }
        defaultRequest {
            header("Proxy-Authorization", proxyAuthHeader)
            header(HttpHeaders.UserAgent, "ProxyHat-Kotlin/1.0")
        }
        install(HttpTimeout) {
            connectTimeoutMillis = 15_000
            requestTimeoutMillis = 30_000
            socketTimeoutMillis = 30_000
        }
    }
}

suspend fun fetchIp(client: HttpClient): String {
    val resp = client.get("https://httpbin.org/ip")
    return resp.bodyAsText()
}

suspend fun main() {
    val client = buildKtorCioProxyClient(
        username = "user-country-US-session-sess-001",
        password = "YOUR_PASSWORD"
    )
    client.use {
        println(fetchIp(it))
    }
}

Важно: Proxy-Authorization в defaultRequest добавляется к каждому запросу. Это работает, потому что HTTP-прокси-аутентификация в Basic-форме — stateless: прокси проверяет заголовок на каждый запрос.

Geo-таргетинг и sticky-сессии

ProxyHat кодирует параметры в username по схеме user-{flag}-{value}. Основные флаги:

ФлагПримерНазначение
countryuser-country-DEСтрана по ISO 3166-1 alpha-2
cityuser-country-DE-city-berlinГород (в нижнем регистре)
sessionuser-session-abc123Sticky-сессия — один IP на все запросы
комбинацияuser-country-DE-city-berlin-session-abc123Geo + sticky в одной строке

Sticky-сессии критичны для сайтов с сессионным state: корзины, логины, многошаговые формы. Без sticky каждый запрос получит новый IP, и сессия «слетит». Срок жизни sticky-сессии у ProxyHat — до 30 минут в зависимости от тарифа.

SOCKS5 на порту 1080

SOCKS5 работает на транспортном уровне, не модифицируя HTTP-заголовки. Это даёт два преимущества: прокси не видит содержимое запросов (только адрес назначения) и работает с любым TCP-протоколом. В JVM SOCKS5-аутентификация настраивается через системные свойства:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import java.net.InetSocketAddress
import java.net.Proxy

fun configureSocks5(username: String, password: String) {
    // SOCKS5 credentials для JVM
    System.setProperty("java.net.socks.username", username)
    System.setProperty("java.net.socks.password", password)
}

suspend fun main() {
    configureSocks5("user-country-GB", "YOUR_PASSWORD")

    val client = HttpClient(CIO) {
        engine {
            proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080))
        }
    }

    client.use {
        val resp = it.get("https://httpbin.org/ip")
        println(resp.bodyAsText())
    }
}

Внимание: системные свойства java.net.socks.username и java.net.socks.password глобальны для всей JVM. В многопоточных приложениях с разными credentials это создаёт гонку. Для production используйте OkHttp с кастомным Authenticator или Ktor OkHttp engine.

Почему residential-прокси нужны для социальных и app-таргетов

Социальные сети (Instagram, TikTok, X) и крупные приложения используют многоуровневую защиту: IP-репутация по ASN, TLS-fingerprinting (JA3/JA4), поведенческий анализ, CAPTCHA. Datacenter-IP блокируются на первом уровне — запрос даже не доходит до контента.

Residential-прокси проходят первый барьер, потому что их ASN принадлежат реальным ISP (Comcast, Deutsche Telekom, Vodafone). Но это не серебряная пуля — частота запросов и TLS-fingerprint всё равно важны. См. наш разбор SERP-трекинга для деталей.

Конкурентный скрапинг на корутинах с Semaphore

Когда нужно собрать 1000 страниц, последовательные запросы слишком медленные. Но параллельная отправка всех 1000 запросов приведёт к блокировке по rate limit. Решение — async + awaitAll с Semaphore для ограничения конкурентности:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64
import kotlin.random.Random

suspend fun scrapeConcurrent(
    urls: List<String>,
    proxyUser: String,
    proxyPass: String,
    concurrency: Int = 20
): List<Result<String>> = coroutineScope {
    val authHeader = "Basic " +
        Base64.getEncoder().encodeToString("$proxyUser:$proxyPass".toByteArray())

    val client = HttpClient(CIO) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
        }
        install(HttpTimeout) {
            connectTimeoutMillis = 10_000
            requestTimeoutMillis = 20_000
        }
        defaultRequest {
            header("Proxy-Authorization", authHeader)
        }
    }

    val semaphore = Semaphore(concurrency)

    val results = urls.map { url ->
        async(Dispatchers.IO) {
            semaphore.withPermit {
                try {
                    val resp = client.get(url)
                    if (resp.status.value in 200..299) {
                        Result.success(resp.bodyAsText())
                    } else {
                        Result.failure(RuntimeException("HTTP ${resp.status.value}"))
                    }
                } catch (e: Exception) {
                    Result.failure(e)
                }
            }
        }
    }

    val all = results.awaitAll()
    client.close()
    all
}

suspend fun main() = runBlocking {
    val urls = (1..50).map { "https://httpbin.org/delay/${Random.nextInt(1, 3)}" }
    val results = scrapeConcurrent(
        urls = urls,
        proxyUser = "user-country-US",
        proxyPass = "YOUR_PASSWORD",
        concurrency = 15
    )
    val success = results.count { it.isSuccess }
    println("Success: $success / ${urls.size}")
}

Semaphore(15) гарантирует, что одновременно выполняется не более 15 запросов. Это даёт баланс между скоростью и аккуратностью — слишком высокая конкурентность триггерит anti-bot, слишком низкая — медленный сбор. Для residential-прокси оптимальный диапазон обычно 10–30 параллельных запросов на одну сессию; ротация IP между запросами позволяет поднять этот потолок.

Production-харденинг

OkHttp Authenticator для 407-челленджей

В production прокси-сервер может периодически отвечать 407 Proxy Authentication Required — например, при истечении сессии или балансировке нагрузки. OkHttp Authenticator автоматически повторяет запрос с обновлёнными credentials:

import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Route
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit

fun buildHardenedOkHttp(
    username: String,
    password: String
): OkHttpClient {
    val proxyAuth = Authenticator { route: Route?, response ->
        // Предотвращаем бесконечный цикл — максимум 2 попытки
        if (response.request.header("Proxy-Authorization") != null) {
            return@Authenticator null
        }
        val credential = Credentials.basic(username, password)
        response.request.newBuilder()
            .header("Proxy-Authorization", credential)
            .build()
    }

    return OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
        .proxyAuthenticator(proxyAuth)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        // Connection pool для переиспользования TCP-соединений
        .connectionPool(okhttp3.ConnectionPool(5, 5, TimeUnit.MINUTES))
        .build()
}

Ретраи с экспоненциальной задержкой

Для Ktor можно реализовать ретраи через перехватчик. Простой паттерн с экспоненциальной задержкой:

import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.statement.*
import kotlinx.coroutines.delay
import kotlin.math.min
import kotlin.math.pow

suspend fun fetchWithRetry(
    client: HttpClient,
    url: String,
    maxRetries: Int = 3,
    baseDelayMs: Long = 500
): HttpResponse {
    var attempt = 0
    while (true) {
        try {
            val resp = client.get(url)
            if (resp.status.value !in 500..599 && resp.status.value != 429) {
                return resp
            }
            if (attempt >= maxRetries) return resp
        } catch (e: Exception) {
            if (attempt >= maxRetries) throw e
        }
        val delayMs = min(baseDelayMs * 2.0.pow(attempt).toLong(), 10_000)
        delay(delayMs)
        attempt++
    }
}

TLS-конфигурация

Для OkHttp можно явно настроить TLS для совместимости с прокси, которые требуют определённые версии протокола:

import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import java.security.KeyStore

fun buildTlsClient(username: String, password: String): OkHttpClient {
    val tmf = TrustManagerFactory.getInstance(
        TrustManagerFactory.getDefaultAlgorithm()
    )
    tmf.init(null as KeyStore?)
    val trustManagers = tmf.trustManagers
    val x509Tm = trustManagers.first { it is X509TrustManager } as X509TrustManager

    val sslContext = SSLContext.getInstance("TLSv1.2")
    sslContext.init(null, arrayOf(trustManagers), null)

    return OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
        .proxyAuthenticator { _, response ->
            response.request.newBuilder()
                .header("Proxy-Authorization",
                    okhttp3.Credentials.basic(username, password))
                .build()
        }
        .sslSocketFactory(sslContext.socketFactory, x509Tm)
        .build()
}

Android NetworkSecurityConfig

На Android 7.0+ (API 24+) по умолчанию не доверяются пользовательские CA-сертификаты, а cleartext-трафик заблокирован. Если ваше приложение ходит через прокси, добавьте network_security_config.xml:

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">gate.proxyhat.com</domain>
    </domain-config>
</network-security-config>

И ссылку в AndroidManifest.xml:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

Это нужно только если прокси-соединение использует cleartext (HTTP CONNECT). Для HTTPS через прокси TLS-туннель устанавливается после CONNECT, и cleartext-флаг не требуется.

ProxyHat SDK и интеграция

ProxyHat предоставляет endpoint gate.proxyhat.com:8080 для HTTP и :1080 для SOCKS5. Параметры сессии и geo-таргетинга кодируются в username — это значит, что любой HTTP-клиент в Kotlin работает без специального SDK. Паттерн «флаги в username» повторяет подход, описанный в документации ProxyHat.

Для управления тарифом, ротацией IP и мониторингом используйте дашборд ProxyHat. Доступные локации и покрытие смотрите на странице локаций. Примеры использования для сбора данных — в разделе веб-скрапинг.

Этика и правовые аспекты

Kotlin веб-скрапинг с прокси — мощный инструмент, но с правовыми границами. Ключевые принципы:

  • Публичные данные только. Не собирайте данные за логином, платным контентом или с нарушением CFAA в США.
  • GDPR в ЕС. Персональные данные欧盟 граждан защищены GDPR. Сбор персональных данных без законного основания — нарушение.
  • robots.txt. Уважайте директивы robots.txt — это отраслевой стандарт, игнорирование которого может трактоваться как недобросовестное поведение.
  • Официальные API. Если у сервиса есть публичный API — используйте его. Это надёжнее, дешевле и законнее, чем скрапинг HTML.
  • Rate limits. Не отправляйте запросы с частотой, которая деградирует сервис. Semaphore и задержки — не только техническая необходимость, но и этическая.

Key Takeaways

  • Выбирайте движок по задаче: OkHttp — для максимального контроля и Android; Ktor CIO — для серверных и кроссплатформенных проектов.
  • Прокси-аутентификация engine-specific: OkHttp использует proxyAuthenticator, Ktor CIO требует ручной Proxy-Authorization в defaultRequest.
  • Geo-таргетинг и sticky-сессии кодируются в username: user-country-DE-city-berlin-session-abc123.
  • SOCKS5 на :1080 требует системных свойств java.net.socks.username/password —小心 с глобальным состоянием в многопоточных приложениях.
  • Residential-прокси необходимы для социальных платформ и app-таргетов, блокирующих datacenter ASN.
  • Конкурентность через Semaphore: 10–30 параллельных запросов — безопасный диапазон для residential-прокси.
  • Production-харденинг: ретраи с экспоненциальной задержкой, connection pooling, TLS-конфигурация, обработка 407.
  • Этика: публичные данные, robots.txt, официальные API, GDPR и CFAA.

FAQ

Что такое использование прокси в Kotlin?

Использование прокси в Kotlin — это настройка HTTP-клиентов (Ktor, OkHttp, java.net.HttpURLConnection) для маршрутизации трафика через промежуточный сервер. В Kotlin/JVM прокси настраивается через java.net.Proxy для транспортного уровня и заголовок Proxy-Authorization для аутентификации. Для residential-прокси параметры сессии и geo-таргетинга кодируются в username.

Зачем нужен прокси при разработке на Kotlin?

Прокси нужен для обхода IP-блокировок, geo-тестирования, сбора публичных данных и анонимизации трафика. Без прокси запросы идут с вашего серверного IP, который быстро попадает в blacklist антибот-систем. Residential-прокси маскируют запросы под реальных пользователей, повышая success rate с 10–20% (datacenter) до 90%+ (residential) для защищённых сайтов.

Какой тип прокси лучше для Kotlin-проектов?

Зависит от задачи. Datacenter-прокси — для API-мониторинга и QA, где блокировка по ASN не критична. Residential — для SERP-скрапинга, социальных платформ и e-commerce. Mobile — для максимальной доверенности (Instagram, TikTok). Для большинства production-скраперов на Kotlin оптимальны residential с ротацией IP и geo-таргетингом.

Как избежать блокировок при использовании прокси в Kotlin?

Используйте residential-прокси вместо datacenter, ограничивайте конкурентность через Semaphore (10–30 запросов), добавляйте задержки между запросами, ротируйте IP между запросами (без sticky-сессии для массового сбора), используйте реалистичный User-Agent, обрабатывайте 429 и 503 с экспоненциальной задержкой. Уважайте robots.txt и не превышайте разумный rate limit.

Можно ли использовать SOCKS5-прокси в Ktor?

Да, через Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080)) в конфигурации engine. Аутентификация SOCKS5 в JVM настраивается через системные свойства java.net.socks.username и java.net.socks.password. В многопоточных приложениях с разными credentials это создаёт гонку — используйте OkHttp с кастомным Authenticator вместо глобальных свойств.

Часто задаваемые вопросы

Что такое использование прокси в Kotlin?

Использование прокси в Kotlin — это настройка HTTP-клиентов (Ktor, OkHttp, java.net.HttpURLConnection) для маршрутизации трафика через промежуточный сервер. В Kotlin/JVM прокси настраивается через java.net.Proxy для транспортного уровня и заголовок Proxy-Authorization для аутентификации. Для residential-прокси параметры сессии и geo-таргетинга кодируются в username.

Зачем нужен прокси при разработке на Kotlin?

Прокси нужен для обхода IP-блокировок, geo-тестирования, сбора публичных данных и анонимизации трафика. Без прокси запросы идут с вашего серверного IP, который быстро попадает в blacklist антибот-систем. Residential-прокси маскируют запросы под реальных пользователей, повышая success rate с 10–20% (datacenter) до 90%+ (residential) для защищённых сайтов.

Какой тип прокси лучше для Kotlin-проектов?

Зависит от задачи. Datacenter-прокси — для API-мониторинга и QA, где блокировка по ASN не критична. Residential — для SERP-скрапинга, социальных платформ и e-commerce. Mobile — для максимальной доверенности. Для большинства production-скраперов на Kotlin оптимальны residential с ротацией IP и geo-таргетингом.

Как избежать блокировок при использовании прокси в Kotlin?

Используйте residential-прокси вместо datacenter, ограничивайте конкурентность через Semaphore (10–30 запросов), добавляйте задержки между запросами, ротируйте IP между запросами, используйте реалистичный User-Agent, обрабатывайте 429 и 503 с экспоненциальной задержкой. Уважайте robots.txt и не превышайте разумный rate limit.

Можно ли использовать SOCKS5-прокси в Ktor?

Да, через Proxy(Proxy.Type.SOCKS, InetSocketAddress) в конфигурации engine. Аутентификация SOCKS5 в JVM настраивается через системные свойства java.net.socks.username и java.net.socks.password. В многопоточных приложениях с разными credentials это создаёт гонку — используйте OkHttp с кастомным Authenticator.

Готовы начать?

Резидентные, ISP и мобильные прокси в 148+ странах. Создайте бесплатный аккаунт.

Создать бесплатный аккаунт
← Вернуться в Блог