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



有人有在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:

问题:Debugger显示此消息: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可以使用现在弃用的APIURLConnection连接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类型的,这无助于弄清楚要实现什么协议。有两个——NSURLConnectionDelegateNSURLConnectionDataDelegate

让我们接收数据:

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)
}

网上没有太多的信息,所以希望它能节省一些人几天的试错。

我今天也在寻找同样的解决方案。起初,我试图使用session.streamTask,但我不知道如何使用它。这是TCP的低级任务,但我想要的是http级别的解决方案。我也不想使用URLConnection,它已经被弃用了。

经过一番研究,我终于弄明白了:在URLSessionDataDelegate的文档中,https://developer.apple.com/documentation/foundation/urlsessiondatadelegate

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

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

关键是不要在dataTask()中设置一个完成处理程序块,并实现URLSessionDataDelegate的两个委托方法:

// 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)")
}
}

最新更新