如何使用ndjson在iOS上打开HTTP流

问题描述 投票:0回答:3

有人有在 iOS 上打开 HTTP 流的经验吗?我尝试了多种解决方案,但没有任何运气(示例如下)。

为了更好的上下文,以下是在打开连接时将传输值(作为 ndjson)的端点示例:

GET /v2/path/{id}
Accept: application/x-ndjson

尝试#1:

问题:从未调用完成处理程序

let keyID = try keyAdapter.getKeyID(for: .signHash)
let url = baseURL.appendingPathComponent("/v2/path/\(keyID)")

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
urlRequest.setValue("application/x-ndjson", forHTTPHeaderField: "Accept")

session.dataTask(with: urlRequest) { data, response, error in
   // This never gets called.
   // I would expect that the completion is called every time backend emits new value.
}.resume()

尝试#2:

问题:调试器显示此消息:

Connection 0: encountered error(12:1)

private var stream: URLSessionStreamTask? = nil

func startStream() {
    let keyID = try keyAdapter.getKeyID(for: .signHash)
    let url = baseURL.appendingPathComponent("/v2/path/\(keyID)")

    let stream = session.streamTask(withHostName: url, port: 443)
    // Not sure how to set headers. 
    // Header needs to be set so backend knows client wants to connect a stream.
    self.stream = stream

    stream.startSecureConnection()
    startRead(stream: stream)
}

private func startRead(stream: URLSessionStreamTask) {
    stream.readData(ofMinLength: 1, maxLength: 4096, timeout: 120.0) { data, endOfFile, error in
        if let error = error {
            Logger.shared.log(level: .error, "Reading data from stream failed with error: \(error.localizedDescription)")
        } else if let data = data {
            Logger.shared.log(level: .error, "Received data from stream (\(data.count)B)")
            if !endOfFile {
                self.startRead(stream: stream)
            } else {
                Logger.shared.log(level: .info, "End of file")
            }
        } else {
            Logger.shared.log(level: .error, "Reading stream endup in unspecified state (both data and error are nil).")
        }
    }
}

有人有这方面的经验吗?如何保持 HTTP 连接打开并监听后端正在流式传输的新值?

ios swift http stream ndjson
3个回答
1
投票

我今天也在寻找相同的解决方案。起初,我尝试使用

session.streamTask
,但不知道如何使用。对于 TCP 来说,这是一个低级任务,但我想要的是 HTTP 级解决方案。我也不想使用
URLConnection
,它已被弃用。

经过一番研究,我终于弄清楚了:在

URLSessionDataDelegate
的文档中, https://developer.apple.com/documentation/foundation/urlsessiondatadelegate

URLSession 对象不需要有委托。如果没有分配委托,当您在该会话中创建任务时,您必须提供完成处理程序块来获取数据。

完成处理程序块主要用作使用自定义委托的替代方案。 如果您使用采用完成处理程序块的方法创建任务,则不会调用用于响应和数据传递的委托方法

关键是不要在

dataTask()
中设置完成处理程序块,并实现
URLSessionDataDelegate
的2个委托方法:

// This will be triggered repeatedly when new data comes
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: Data) {
    var resultString = String(data: didReceive, encoding: .utf8)
    print("didReceive: \(resultString)")
}
    
// This will be triggered when the task ends. Handle errors here as well
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    print("didCompleteWithError: \(error)")
}

另一个关键是将委托设置为

URLSessionDataTask
,而不是
URLSession
。 Larme 代码的问题在于,他将委托设置为
URLSession
,因此函数
urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: Data)
将不会被调用。

完整代码演示:

class NetRequest: NSObject, URLSessionDataDelegate {

    func startRequest() {
        var urlRequest = URLRequest(url: "http://...")
        // Set up urlRequest...
        // ...
        
        let session = URLSession(configuration: .default)
        let dataTask = session.dataTask(with: urlRequest)
        dataTask.delegate = self
        dataTask.resume()
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: Data) {
        var resultString = String(data: didReceive, encoding: .utf8)
        print("didReceive: \(resultString)")
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("didCompleteWithError: \(error)")
    }

}


0
投票

iOS 可以使用现已弃用的 API

URLConnection
连接到 HTTP 流。该 API 在 iOS 9 中已弃用,但仍然可以使用(并且将在 iOS 16 中进行测试)。

首先您需要创建

URLRequest
并设置
NSURLConnection
:

let url = URL(string: "\(baseURL)/v2/path/\(keyID)")!

var urlRequest = URLRequest(url: url)
urlRequest.setValue("application/x-ndjson", forHTTPHeaderField: "Accept")

let connnection = NSURLConnection(request: urlRequest, delegate: self, startImmediately: true)
connnection?.start()

请注意,上面代码中 delegate 的参数是

Any
类型,这无助于确定要实现的协议。有两个 -
NSURLConnectionDelegate
NSURLConnectionDataDelegate

让我们接收数据:

public func connection(_ connection: NSURLConnection, didReceive data: Data) {
    let string = String(data: data, encoding: .utf8)
    Logger.shared.log(level: .debug, "didReceive data:\n\(string ?? "N/A")")
}

然后实现一个捕获错误的方法:

public func connection(_ connection: NSURLConnection, didFailWithError error: Error) {
    Logger.shared.log(level: .debug, "didFailWithError: \(error)")
}

如果您有自定义 SSL 固定,那么:

public func connection(_ connection: NSURLConnection, willSendRequestFor challenge: URLAuthenticationChallenge) {
    guard let certificate = certificate, let identity = identity else {
        Logger.shared.log(level: .info, "No credentials set. Using default handling. (certificate and/or identity are nil)")
        challenge.sender?.performDefaultHandling?(for: challenge)
        return
    }

    let credential = URLCredential(identity: identity, certificates: [certificate], persistence: .forSession)
    challenge.sender?.use(credential, for: challenge)
}

互联网上没有太多信息,因此希望它可以节省某人几天的尝试和错误。


0
投票

您现在(从 iOS 15.0 开始)可以使用非常简单的 API 来接收流式响应

URLSession

let (bytes, response) = try await URLSession.shared.bytes(for: request)

for try await line in bytes.lines {
  print(line)
}
© www.soinside.com 2019 - 2024. All rights reserved.