You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CoroutineWorker的doWork在FTP登录失败抛异常时为何挂起?

FTP错误登录时异常延迟抛出的原因与解决方法

问题场景

使用Android Studio + Kotlin连接配置SSL/TLS(隐式被动模式)的vsftpd服务器,通过WorkManager调度周期性任务。当使用错误的用户名/密码登录时,本应立即抛出ConnectException并触发Worker重试,但实际异常要等待数分钟才会抛出,无法及时触发重试逻辑。

依赖配置

dependencies {
    implementation 'commons-net:commons-net:3.6'
    implementation 'androidx.work:work-runtime-ktx:2.5.0'
}

核心代码

PeriodicWorker类

class PeriodicWorker(
    appContext: Context,
    private val workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    companion object {
        private const val TAG: String = "PeriodicWorker"

        private const val TIME: Long = 3
        private val UNIT_TIME: TimeUnit = TimeUnit.HOURS

        fun enqueueTask(workManager: WorkManager) {
            val periodicWork = PeriodicWorkRequestBuilder<PeriodicWorker>(
                TIME, UNIT_TIME
            )
                .setInitialDelay(10, TimeUnit.SECONDS) // 测试用初始延迟
                .setBackoffCriteria(
                    BackoffPolicy.LINEAR,
                    PeriodicWorkRequest.MIN_BACKOFF_MILLIS,
                    TimeUnit.SECONDS
                )
                .build()

            workManager.enqueueUniquePeriodicWork("unique", ExistingPeriodicWorkPolicy.KEEP, periodicWork)
        }
    }

    private val ftpHandler: FtpHandler = FtpHandler()

    override suspend fun doWork(): Result {
        return withContext(Dispatchers.IO) {
            try {
                val filesInFtpServer: Array<String>? = ftpHandler.listFileNamesFtpServer()
                Result.success()
            } catch (e: Exception) {
                Result.retry()
            }
        }
    }
}

FtpHandler类

class FtpHandler {

    companion object {
        private const val DATA_TIMEOUT_MS = 1000 * 5 
        private const val CONNECTION_TIMEOUT_MS = 1000 * 5
        private const val TAG = "FtpHandler"
    }
    
    private val client: FTPSClient = FTPSClient("TLS", true)
    private val ftpHost = ""
    private val ftpPort = 21
    private val ftpUsername = "wrong_user"
    private val ftpPassword = "wrong_pass"
    
    init {
        client.setDataTimeout(DATA_TIMEOUT_MS)
        client.connectTimeout = CONNECTION_TIMEOUT_MS
    }
    
    private fun login(): Boolean {
        client.connect(ftpHost, ftpPort)
        client.execPBSZ(0)
        client.execPROT("P") 
        client.enterLocalPassiveMode()
        return client.login(ftpUsername, ftpPassword)
    }
    
    fun listFileNamesFtpServer(directory: String = "."): Array<String>? {
        var res: Array<String>? = null
        
        try {
            val loggedIn: Boolean = login()

            if (loggedIn) {
                if (FTPReply.isPositiveCompletion(client.replyCode)) {
                    client.changeWorkingDirectory(directory)
                    res = client.listNames()
                } else {
                    throw ConnectException("Reply: ${client.reply}")
                }
            } else {
                throw ConnectException("Reply: ${client.reply}")
            }
            
        } catch (e: Exception) {
            throw e
        } finally {
            try {
                client.logout()
                client.disconnect()
            } catch (e: IOException) {
                throw e
            }
        }
        
        return res
    }
}

问题补充

  • 更新1:FTP服务器离线时,FTPSClient会正常抛出SocketTimeoutException;但服务器在线时,登录失败返回530 Login incorrect.,FTPSClient不会立即断开,导致线程挂起数分钟才抛出异常。
  • 更新2:添加client.soTimeout = 5 * 1000后,最终会抛出SocketTimeoutException: Read timed out,而非预期的ConnectException,说明异常传播被后续操作阻塞。

原因分析

  1. 缺少套接字读取超时设置:FTPSClient默认soTimeout为0(无限等待),登录失败后,finally块执行client.logout()时,会尝试读取服务器的响应,但此时服务器已停止发送数据,线程会一直阻塞直到操作系统的默认超时(数分钟)。
  2. finally块的无效操作阻塞异常传播:登录失败时,调用logout()是无效操作(未成功登录),但该操作仍会尝试与服务器通信,导致线程阻塞;且finally块抛出的异常会覆盖原始的ConnectException,进一步干扰异常处理逻辑。
  3. 异常抛出后未立即断开连接:抛出ConnectException后,未及时断开与服务器的连接,导致后续清理操作仍在等待服务器响应。

解决方案

1. 配置套接字读取超时

在FTPSClient初始化时设置合理的soTimeout,确保所有与服务器的通信操作都有超时限制,避免无限阻塞。

2. 优化资源清理逻辑

  • 跟踪登录状态,仅在登录成功时调用logout(),避免无效操作。
  • 清理操作中捕获的异常仅记录日志,不抛出,防止掩盖原始异常。
  • 无论登录是否成功,确保断开连接释放资源。

修改后的FtpHandler代码

class FtpHandler {

    companion object {
        private const val DATA_TIMEOUT_MS = 1000 * 5 
        private const val CONNECTION_TIMEOUT_MS = 1000 * 5
        private const val SOCKET_TIMEOUT_MS = 1000 * 5 // 新增套接字读取超时
        private const val TAG = "FtpHandler"
    }
    
    private val client: FTPSClient = FTPSClient("TLS", true)
    private val ftpHost = ""
    private val ftpPort = 21
    private val ftpUsername = "wrong_user"
    private val ftpPassword = "wrong_pass"
    
    init {
        client.setDataTimeout(DATA_TIMEOUT_MS)
        client.connectTimeout = CONNECTION_TIMEOUT_MS
        client.soTimeout = SOCKET_TIMEOUT_MS // 设置套接字读取超时
    }
    
    private fun login(): Boolean {
        client.connect(ftpHost, ftpPort)
        client.execPBSZ(0)
        client.execPROT("P") 
        client.enterLocalPassiveMode()
        return client.login(ftpUsername, ftpPassword)
    }
    
    fun listFileNamesFtpServer(directory: String = "."): Array<String>? {
        var res: Array<String>? = null
        var isLoggedIn = false // 跟踪登录状态
        
        try {
            isLoggedIn = login()

            if (isLoggedIn) {
                if (FTPReply.isPositiveCompletion(client.replyCode)) {
                    client.changeWorkingDirectory(directory)
                    res = client.listNames()
                } else {
                    throw ConnectException("Reply: ${client.reply}")
                }
            } else {
                throw ConnectException("Reply: ${client.reply}")
            }
            
        } catch (e: Exception) {
            throw e
        } finally {
            try {
                // 仅登录成功时执行logout
                if (isLoggedIn) {
                    client.logout()
                }
                // 确保断开连接
                if (client.isConnected) {
                    client.disconnect()
                }
            } catch (e: IOException) {
                // 记录清理异常,不抛出避免掩盖原始异常
                Log.e(TAG, "Failed to clean up FTP connection", e)
            }
        }
        
        return res
    }
}

内容的提问来源于stack exchange,提问作者alexSDE

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 21:20:55