Swift中URLSession streamTask无法启动?需实现终端式长连接功能
解决URLSessionStreamTask连接问题与终端风格长连接应用实现
一、为什么调用.resume()后任务没启动?
我之前也踩过类似的坑,大概率是这几个细节没处理好:
未持有Session和StreamTask的引用
如果你的session和streamTask是方法内的局部变量,方法执行完毕后会被ARC自动释放,任务自然启动不了。必须把它们作为类属性保存:class YourViewController: UIViewController, URLSessionStreamDelegate { var session: URLSession? var streamTask: URLSessionStreamTask? // 额外持有读写流,方便后续操作 var inputStream: InputStream? var outputStream: OutputStream? // ... 其他代码 }缺失关键代理方法实现
StreamTask的完全依赖代理回调驱动,没实现核心代理方法的话,系统不会触发任何操作,看起来就像任务没启动。至少要实现这个方法获取读写流:func streamTask(_ task: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) { self.inputStream = inputStream self.outputStream = outputStream // 配置流的代理与调度队列 inputStream.delegate = self outputStream.delegate = self inputStream.schedule(in: .main, forMode: .common) outputStream.schedule(in: .main, forMode: .common) // 打开流 inputStream.open() outputStream.open() }别忘了让你的类遵守
StreamDelegate协议,处理流的读写事件。输入合法性检查缺失
直接强制解包iptxt.text和porttxt.text风险极高:如果输入为空会崩溃,端口字符串转Int失败会得到nil,导致创建的streamTask无效。一定要加安全校验:guard let host = iptxt.text, !host.isEmpty, let portStr = porttxt.text, let port = Int(portStr), port > 0 && port <= 65535 else { // 提示用户输入合法的IP/域名和端口 return }
二、实现终端风格的长连接应用
完全不需要每次触发后断开!URLSessionStreamTask天生支持长连接,只要你不主动调用cancel(),连接会一直保持到手动断开或网络异常中断。这里给你一套完整的实现思路:
核心交互逻辑
连接触发(点击连接按钮):
@IBAction func connectButtonTapped(_ sender: UIButton) { guard let host = iptxt.text, !host.isEmpty, let portStr = porttxt.text, let port = Int(portStr), port > 0 && port <= 65535 else { showAlert(message: "请输入合法的IP和端口") return } // 先断开已有连接(如果存在) disconnect() session = URLSession(configuration: .default, delegate: self, delegateQueue: .main) streamTask = session?.streamTask(withHostName: host, port: port) streamTask?.resume() sender.setTitle("断开", for: .normal) }断开操作(点击断开按钮或异常触发):
func disconnect() { streamTask?.cancel() streamTask = nil session?.invalidateAndCancel() session = nil // 清理流资源 inputStream?.close() inputStream?.remove(from: .main, forMode: .common) outputStream?.close() outputStream?.remove(from: .main, forMode: .common) inputStream = nil outputStream = nil connectButton.setTitle("连接", for: .normal) }处理流的读写事件(实现
StreamDelegate):func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: // 读取服务器返回的数据 guard let inputStream = aStream as? InputStream else { return } var buffer = [UInt8](repeating: 0, count: 1024) while inputStream.hasBytesAvailable { let bytesRead = inputStream.read(&buffer, maxLength: buffer.count) if bytesRead > 0 { let receivedText = String(bytes: buffer[0..<bytesRead], encoding: .utf8) ?? "" DispatchQueue.main.async { // 将收到的内容追加到终端视图 self.terminalTextView.text.append(receivedText) } } } case .errorOccurred: // 处理连接错误 let errorMsg = aStream.streamError?.localizedDescription ?? "未知错误" showAlert(message: "连接出错:\(errorMsg)") disconnect() case .endEncountered: // 服务器主动断开连接 showAlert(message: "连接已断开") disconnect() default: break } }发送自定义命令(点击发送按钮):
@IBAction func sendCommandTapped(_ sender: UIButton) { guard let command = commandInputField.text, !command.isEmpty, let outputStream = outputStream, outputStream.hasSpaceAvailable else { return } // 命令行通常需要换行符结尾,根据服务器要求调整为\r\n或\n let commandData = (command + "\r\n").data(using: .utf8)! commandData.withUnsafeBytes { buffer in outputStream.write(buffer.baseAddress!.assumingMemoryBound(to: UInt8.self), maxLength: commandData.count) } // 把发送的命令也显示到终端视图 terminalTextView.text.append("> \(command)\n") commandInputField.text = "" }
额外注意事项
- 流操作本身不强制要求主线程,但更新UI时必须切回主线程;
- 若连接频繁自动断开,可考虑定期发送心跳包(比如空命令或特定指令)维持连接;
- 记得处理后台状态下的连接逻辑,避免应用进入后台后连接被系统回收。
内容的提问来源于stack exchange,提问作者Howard
相关产品推荐
相关产品推荐

