android - 后台套接字连接

标签 android sockets android-background

我需要制作一个应用程序,在用户获得授权的同时,它会保持套接字连接直到注销。为此,创建了一个前台服务,在用户授权后启动,注销时停止。它在套接字上实现连接和重新连接。

一切正常,直到您按下电源按钮并关闭充电。在此之后,用户停止从服务器接收 pong,并且在 OkHttp 上接收到 SocketTimeoutException,并且也停止在套接字上接收消息。在JavaWebsocket上,收到The connection was closed because the other endpoint didn't respond with a pong time,之后可以成功创建新的socket连接,但是会在循环中重复同样的问题.

在设置中,禁用了此应用程序的电池优化。我该怎么做才能使稳定的连接套接字在后台工作?

Activity 的实现:

class MainActivity : BaseFragmentPermissionActivity(), MainMvpView {   
    private var mIsSocketBound = false   
    private var mSocketBroadcastReceiver = SocketBroadcastReceiver(this)   
    private var mSocketConnection = SocketConnection(this)
    private var mSocketService: SocketService? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {   
        super.onCreate(savedInstanceState)   
        ...
        doBindService()
    }
    
    private fun doBindService() {
        bindService(Intent(this, SocketService::class.java), mSocketConnection, Context.BIND_AUTO_CREATE)
        mIsSocketBound = true
    }
    
    override fun onStart() {
        super.onStart()
        ...
        mSocketService?.doStopForeground()
    }
    
    override fun onStop() {
        mSocketService?.doStartForeground()
        ...
        super.onStop()
    }
    
    override fun onDestroy() {
        doUnbindService()
        ...
        super.onDestroy()
    }    

    private fun doUnbindService() {
        if (mIsSocketBound) {
            unbindService(mSocketConnection)
            mIsSocketBound = false
            mSocketService = null
        }
    }

    class SocketConnection(mainActivity: MainActivity) : ServiceConnection {
        private val mMainActivity: WeakReference<MainActivity> = WeakReference(mainActivity)

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val socketService = (service as SocketService.LocalBinder).getService()
            mMainActivity.get()?.mSocketService = socketService
            if (socketService.isForeground()) {
                socketService.doStopForeground()
            }
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            mMainActivity.get()?.mSocketService = null
        }
    }
}

服务的实现:

class SocketService : Service(), MvpErrorHandler {   
    private val mConnectingHandler = Handler()
    private val mConnectingTask = ConnectingTask(this)
    private var mIsRunningForeground = false

    override fun onBind(intent: Intent?): IBinder {
        startService(Intent(this, SocketService::class.java))
        return mBinder
    }

    override fun onCreate() {
        super.onCreate()
        DaggerServiceComponent.builder()
                .serviceModule(ServiceModule(this))
                .applicationComponent(PatrolApplication.applicationComponent)
                .build()
                .inject(this)

        startConnecting()
        ...
    }

    override fun onDestroy() {
        ...
        stopConnecting()
        super.onDestroy()
    }

    private fun startConnecting() {
        if (!mIsConnecting) {
            mIsConnecting = true
            mConnectingHandler.post(mConnectingTask)
        }
    }

    private fun stopConnecting() {
        mConnectingHandler.removeCallbacks(mConnectingTask)
        mIsConnecting = false
    }

    private fun openConnection() {
        mCompositeDisposable.add(mDataManager.getSocketToken()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(false, this, {
                stopConnecting()
                mDataManager.openSocketConnection(it.token)
            }, {
                mConnectingHandler.postDelayed(mConnectingTask, RECONNECT_TIME.toLong())
                return@subscribe ErrorHandlerUtil.handleGetSocketError(it, this)
            }))
    }

    class ConnectingTask(socketService: SocketService) : Runnable {
        private val mSocketService: WeakReference<SocketService> = WeakReference(socketService)

        override fun run() {
            mSocketService.get()?.openConnection()
        }
    }
}

使用 JavaWebsocket 实现 SocketHelper:

class CustomApiSocketHelper @Inject constructor() : ApiSocketHelper {

    private var mCustomSocketClient: WebSocketClient? = null

    override fun openSocketConnection(token: String) {
        mCustomSocketClient = CustomSocketClient(URI(CONNECTION_URL + token))
        mCustomSocketClient?.connect()
    }

    override fun sendMessage(text: String) {
        if (mCustomSocketClient?.isOpen == true) {
            try {
                mCustomSocketClient?.send(text)
            } catch (t: Throwable) {
                Log.e(TAG, Log.getStackTraceString(t))
                Crashlytics.logException(t)
            }
        }
    }

    override fun closeSocketConnection() {
        mCustomSocketClient?.close(CLOSE_REASON_OK)
    }

    class CustomSocketClient(uri: URI) : WebSocketClient(uri) {
        init {
            connectionLostTimeout = PING_TIMEOUT
        }

        override fun onOpen(handshakedata: ServerHandshake?) {
            sendBroadcast(SocketActionType.OPEN.action)
        }

        override fun onMessage(message: String?) {
            sendBroadcast(SocketActionType.MESSAGE.action, message)
        }

        override fun onClose(code: Int, reason: String?, remote: Boolean) {
            if (code != CLOSE_REASON_OK) {
                //call startConnecting() in service
                sendBroadcast(SocketActionType.CLOSE.action)
            }
        }

        override fun onError(ex: Exception?) {
            sendBroadcast(SocketActionType.FAILURE.action)
        }

        private fun sendBroadcast(type: Int) {
            val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
            intent.putExtra(SOCKET_MESSAGE_TYPE, type)
            LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
        }

        private fun sendBroadcast(type: Int, text: String?) {
            val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
            intent.putExtra(SOCKET_MESSAGE_TYPE, type)
            intent.putExtra(SOCKET_MESSAGE, text)
            LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
        }
    }
}

使用 OkHttp 实现 SocketHelper:

class CustomApiSocketHelper @Inject constructor() : ApiSocketHelper {
    
        private var mCustomSocketClient: WebSocket? = null
    
        override fun openSocketConnection(token: String) {
            val request = Request.Builder()
                .url(CONNECTION_URL + token)
                .build()
            mCustomSocketClient = CustomApplication.applicationComponent.authorizedClient().newWebSocket(request, CustomSocketClient())
        }
    
        override fun sendMessage(text: String) {
            mPatrolSocketClient?.send(text)
        }
    
        override fun closeSocketConnection() {
            mCustomSocketClient?.close(CLOSE_REASON_OK, null)
        }
    
        class CustomSocketClient : WebSocketListener() {
    
            override fun onOpen(webSocket: WebSocket, response: Response) {
                super.onOpen(webSocket, response)
                sendBroadcast(SocketActionType.OPEN.action)
            }
    
            override fun onMessage(webSocket: WebSocket, text: String) {
                super.onMessage(webSocket, text)
                sendBroadcast(SocketActionType.MESSAGE.action, text)
            }
    
            override fun onClosed(webSocket: WebSocket?, code: Int, reason: String?) {
                super.onClosed(webSocket, code, reason)
                if (code != CLOSE_REASON_OK) {
                    sendBroadcast(SocketActionType.CLOSE.action)
                }
            }
    
            override fun onFailure(webSocket: WebSocket?, t: Throwable?, response: Response?) {
                super.onFailure(webSocket, t, response)
                sendBroadcast(SocketActionType.FAILURE.action)
            } 
    
            private fun sendBroadcast(type: Int) {
                val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
                intent.putExtra(SOCKET_MESSAGE_TYPE, type)
                LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
            }
    
            private fun sendBroadcast(type: Int, text: String?) {
                val intent = Intent().apply { action = SOCKET_BROADCAST_ACTION }
                intent.putExtra(SOCKET_MESSAGE_TYPE, type)
                intent.putExtra(SOCKET_MESSAGE, text)
                LocalBroadcastManager.getInstance(CustomApplication.application).sendBroadcast(intent)
            }
        }
    }
    ...
    @Provides
    @Singleton
    @Named(AUTHORIZED_CLIENT)
        fun provideAuthorizedClient(builder: OkHttpClient.Builder, interceptor: Interceptor, authenticator: Authenticator): OkHttpClient = builder
            .addInterceptor(interceptor)
            .authenticator(authenticator)
            .pingInterval(PING_TIMEOUT.toLong(), TimeUnit.SECONDS)
            .build()

    @Provides
    @Singleton
        fun provideOkHttpBuilder() = CustomApiHelper.getOkHttpBuilder()

        fun getOkHttpBuilder(): OkHttpClient.Builder {
            val builder = OkHttpClient.Builder()
            builder.readTimeout(NETWORK_CALL_TIMEOUT, TimeUnit.SECONDS)
            builder.writeTimeout(NETWORK_CALL_TIMEOUT, TimeUnit.SECONDS)
            if (BuildConfig.DEBUG) {
                val logger = HttpLoggingInterceptor()
                logger.level = HttpLoggingInterceptor.Level.BASIC
                builder.addInterceptor(logger)
            }
            return builder
        }

最佳答案

经过对不同设备的一些研究和测试,发现要在网络上稳定运行,设备必须正在充电或启用屏幕。在另一种情况下,无论是 PARTIAL_WAKE_LOCK 还是在设置中禁用电池优化本身都无法解决问题。

解决此问题的推荐方法是将此代码添加到您的 Activity 中:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

这可以防止屏幕关闭并提供稳定的套接字连接。但是我们仍然有用户可以按下电源按钮的情况。而且,如果此时设备正在充电,一切都会像以前一样工作,否则,我们将断开 socket 。要解决这个问题,您需要定期唤醒设备,以支持乒乓进程。这不是一个推荐的解决方案,因为它会导调用池耗尽,并且不能保证 100% 的性能,但如果这个时刻对你来说很关键,那么你可以使用这个解决方案。您需要在适合您的地方添加这段代码,在本例中是在 ping 时使用的。

 @Suppress("DEPRECATION")
 override fun onWebsocketPing(conn: WebSocket?, f: Framedata?) {
     if (mSocketWakeLock == null) {
         mSocketWakeLock = mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK or PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG)
     }
     mSocketWakeLock?.takeIf { !it.isHeld }?.run { acquire(WAKE_TIMEOUT) }
     super.onWebsocketPing(conn, f)
     mSocketWakeLock?.takeIf { it.isHeld }?.run { release() }
}

使用此解决方案,在具有良好互联网的测试设备套接字连接上,可稳定保持 2 小时或更长时间。没有它,它会不断断开连接。

关于android - 后台套接字连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51133783/

相关文章:

linux - 在对方接受请求之前防止写入套接字

android - android服务是单例吗?

java - Java中Socket客户端如何获取动态端口号

android - 为什么我的通知在从异步后台服务回调创建时没有显示?

android - FCM 高优先级消息如何以及在什么条件下被取消优先级?如何从服务器获取缓存的 FCM 消息?

Android 在布局中以编程方式对齐一组按钮

安卓 keystore : Unsupported secret key algorithm: AES/CBC/PKCS5Padding

android - 辅助功能服务读取所有内容如果用户点击其父 View ,则所有按钮的描述

java - 将 "findViewById"用于 map View 时出错

python - 与 python3 asyncio 建立连接