square / okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM.
https://square.github.io/okhttp/
Apache License 2.0
45.87k stars 9.16k forks source link

Cellular network affects socket read_buffer_size of WiFi connections #8540

Closed pavlosharhan2 closed 3 weeks ago

pavlosharhan2 commented 1 month ago

I am investigating an issue where Wi-Fi transfers between my android app and GoPro are inconsistent across different cellular network conditions. Initially, I noticed that GoPro Quik (Official GoPro app) was able to transfer data efficiently even on 2G networks which wasn’t the case for my app. After some digging into their source code, I realized that Quik overrides OkHttp’s read_buffer_size to 512kb instead of the value okhttp uses depending on different quality of Cellular connection. Once I made the same adjustment in my app, it started transferring data well on 2G, which improved performance in those cases. However, this solution wasn’t perfect. Sometimes, transfers slowed down even on 4G, and I noticed that GoPro Quik also experienced slow transfer speeds in those instances. Interestingly, the speeds improved significantly once the cellular connection was disabled, suggesting there might be some interaction between the cellular and Wi-Fi networks affecting performance. I’m looking for ways of logging buffer sizes to investigate further and exploring how Android or the underlying Linux kernel might be influencing these behaviors.

Any suggestions or theories or where exactly the quality of cellular network may contribute are appreciated.

Here is my code for creating Okhttp client with a custom socket factory that overrides read_buffer_size:

class CustomBufferSocketFactory(
    private val sendBufferSizeBytes: Int,
    private val receiveBufferSizeBytes: Int,
    private val tcpNoDelayEnabled: Boolean = false,
    private val socketFactory: SocketFactory = SocketFactory.getDefault()
) : SocketFactory() {

    override fun createSocket(): Socket {
        return configureSocket(socketFactory.createSocket())
    }

    override fun createSocket(host: String, port: Int): Socket {
        return configureSocket(socketFactory.createSocket(host, port))
    }

    override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
        return configureSocket(socketFactory.createSocket(host, port, localHost, localPort))
    }

    override fun createSocket(address: InetAddress, port: Int): Socket {
        return configureSocket(socketFactory.createSocket(address, port))
    }

    override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
        return configureSocket(socketFactory.createSocket(address, port, localAddress, localPort))
    }

    private fun configureSocket(socket: Socket): Socket {
        socket.sendBufferSize = sendBufferSizeBytes
        socket.receiveBufferSize = receiveBufferSizeBytes
        socket.tcpNoDelay = tcpNoDelayEnabled
        return socket
    }
}

suspend fun connectToLocalNetwork(
    context: Context,
    ssid: String,
    password: String
): OkHttpClient? {
    return withTimeoutOrNull(15000L) { // Timeout after 15 seconds
        suspendCancellableCoroutine { continuation ->
            var isResumed = false

            val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

            // Use WifiNetworkSpecifier to build a request for the desired Wi-Fi network
            val wifiSpecifier = WifiNetworkSpecifier.Builder()
                .setSsid(ssid)
                .setWpa2Passphrase(password)
                .build()

            // Build the network request
            val networkRequest = NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .removeTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
                .setNetworkSpecifier(wifiSpecifier)
                .build()

            // Define the network callback
            val networkCallback = object : ConnectivityManager.NetworkCallback() {
                override fun onAvailable(network: Network) {
                    if (!isResumed && !continuation.isCancelled) {
                        isResumed = true

                        // Bind your app's network traffic to the specific network
                        connectivityManager.bindProcessToNetwork(network)

                        Logger.getLogger(OkHttpClient::class.java.name).setLevel(Level.FINE)

                        // Use OkHttpClientFactory for large file downloads
                        val client =
                            OkHttpClient.Builder()
                                .connectTimeout(60, TimeUnit.SECONDS)
                                .readTimeout(60, TimeUnit.SECONDS)
                                .writeTimeout(60, TimeUnit.SECONDS)
                                .socketFactory(CustomBufferSocketFactory(
                                    524288, 524288, true, network.socketFactory))
                                .build()

                        continuation.resume(client)
                    }
                }

                override fun onUnavailable() {
                    if (!isResumed && !continuation.isCancelled) {
                        isResumed = true
                        continuation.resume(null)
                    }
                }

                override fun onLost(network: Network) {
                    if (!isResumed && !continuation.isCancelled) {
                        isResumed = true
                        continuation.resume(null)
                    }
                }
            }

            connectivityManager.requestNetwork(networkRequest, networkCallback)
        }
    }
}
yschimke commented 3 weeks ago

In truth I think this is a better question for Android folk, it might vary by device, OEM or platform version. The conscrypt folk (@prbprbprb ) might have more idea also.

I just checked on an emulator and got 1048576 as the default, so reducing on a slow network probably makes sense. But I don't think OkHttp has enough visibility to set this correctly.

Closing in favour of other discussions https://www.reddit.com/r/androiddev/comments/1fknl99/cellular_network_affects_socket_read_buffer_size/ and https://stackoverflow.com/questions/79002931/android-cellular-network-affects-socket-read-buffer-size-of-wifi-connections