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

