Swift URLSession连接立即关闭,Postman可保持(索尼电视PIN请求异常)
问题描述
我在iOS应用中实现索尼电视的PIN请求功能,目标是让电视上的PIN输入窗口保持打开状态,直到用户输入PIN。但遇到异常:使用Swift的URLSession时连接会立即关闭,而相同请求在Postman中可正常运行。
Postman中发送请求时,电视上的PIN窗口能正常保持打开,会收到预期的401响应;仅当连续快速发送两次请求时,才会关闭PIN窗口。但在iPhone模拟器中运行Swift代码时,PIN窗口短暂出现后立即关闭。
我的Swift代码
let parameters = """ { "method": "actRegister", "params": [ { "clientid": "MyDevice:1", "nickname": "My Device", "level": "private" }, [ { "value": "yes", "function": "WOL" } ] ], "id": 1, "version": "1.0" } """ guard let postData = parameters.data(using: .utf8) else { completion(.failure(NSError(domain: "Invalid data", code: 0, userInfo: nil))) return } var request = URLRequest(url: URL(string: "http://\(ipAddress)/sony/accessControl")!,timeoutInterval: 30) request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" request.httpBody = postData let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NSError(domain: "No data received", code: 0, userInfo: nil))) return } if let responseString = String(data: data, encoding: .utf8) { print("Response: \(responseString)") } } task.resume()
已尝试的解决方法
- 增加timeoutInterval时长
- 使用URLSession.shared.dataTask和自定义配置的URLSession
- 实现URLSessionDataDelegate相关方法
预期与实际行为
- 预期行为:PIN窗口应保持打开,直到用户输入PIN或超时;
- 实际行为:PIN窗口短暂出现后立即关闭。
问题
- 为何Postman与Swift代码的行为存在差异?
- 如何修改Swift代码以保持连接并让电视上的PIN窗口持续显示?
- 是否可以阻止URLSession在收到401响应后自动关闭连接?
环境信息
- iOS 15+
- Swift 5
- Xcode 13+
解答
1. Postman与Swift代码行为差异的原因
Postman在处理401响应时,默认不会立即关闭连接,甚至会保持连接等待后续身份验证交互;而URLSession的默认行为是收到响应后立即终止连接,尤其是当索尼电视API需要持续连接来维持PIN窗口时,Swift请求会快速结束,触发电视关闭PIN窗口。
另外,URLSession可能自动处理连接复用或关闭逻辑,比如收到401后直接断开;而Postman的底层HTTP客户端默认开启Keep-Alive且不会主动断开,连接保持策略存在差异。
2. 修改Swift代码的方案
要维持PIN窗口,核心是让HTTP连接保持打开状态,直到用户输入PIN或超时,可尝试以下方式:
方式一:通过自定义Delegate控制连接生命周期
实现URLSessionDataDelegate,在收到401响应时保持连接活跃,不立即结束任务:
class PinSessionDelegate: NSObject, URLSessionDataDelegate { var completion: ((Result<Data, Error>) -> Void)? var receivedData = Data() func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { // 收到401时允许继续保持连接 if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 { completionHandler(.allow) } else { completionHandler(.cancel) } } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { receivedData.append(data) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { completion?(.failure(error)) } else { completion?(.success(receivedData)) } } } // 发起请求的方法 func sendPinRequest(ipAddress: String, completion: @escaping (Result<Data, Error>) -> Void) { let parameters = """ { "method": "actRegister", "params": [ { "clientid": "MyDevice:1", "nickname": "My Device", "level": "private" }, [ { "value": "yes", "function": "WOL" } ] ], "id": 1, "version": "1.0" } """ guard let postData = parameters.data(using: .utf8), let url = URL(string: "http://\(ipAddress)/sony/accessControl") else { completion(.failure(NSError(domain: "Invalid data or URL", code: 0, userInfo: nil))) return } let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 60 // 延长超时时间 config.httpShouldUsePipelining = false config.httpMaximumConnectionsPerHost = 1 let delegate = PinSessionDelegate() delegate.completion = completion let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil) var request = URLRequest(url: url, timeoutInterval: 60) request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("keep-alive", forHTTPHeaderField: "Connection") // 显式指定保持连接 request.httpMethod = "POST" request.httpBody = postData let task = session.dataTask(with: request) task.resume() }
方式二:完全复刻Postman的请求头
检查Postman发送请求时的所有请求头,将其全部复制到Swift请求中,尤其是User-Agent、Connection等字段——索尼电视API可能对这些字段有校验,导致不同行为:
// 添加Postman风格的请求头 request.addValue("PostmanRuntime/7.32.3", forHTTPHeaderField: "User-Agent") request.addValue("keep-alive", forHTTPHeaderField: "Connection") request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
3. 是否可以阻止URLSession在收到401后自动关闭连接?
可以。通过自定义URLSessionDataDelegate的didReceive response:方法,在收到401响应时调用completionHandler(.allow)而非.cancel,即可让URLSession保持连接。不过需要手动管理任务生命周期,比如用户输入PIN后手动取消任务,或等待超时结束。
内容的提问来源于stack exchange,提问作者Sorrow91

