在我们的应用程序中,我们通过服务器发送的事件请求信息流。为此,我们使用小库IKEventSource,它在引擎盖下使用Foundation.URLSession
。
此信息以小JSON包的形式发送,例如:{"type":"update","data":{"id":"1234","name":"someName","someOtherField":33,"size":"20","someAddress":"Awesome Street 111"}}
现在这对我们来说很好,但我们注意到有时我们会得到以下错误:Error Domain=kCFErrorDomainCFNetwork Code=303 "(null)" UserInfo={NSErrorPeerAddressKey=<CFData 0x610000a84510 [0x103ccbe40]>{length = 16, capacity = 16, bytes = 0x100201bbb9131fbd0000000000000000}, _kCFStreamErrorCodeKey=-2201, _kCFStreamErrorDomainKey=4}
据我所知,这是一个kCFErrorHTTPParseFailure
它试图解析的字符串似乎只是JSON包的一个片段,如:{"type":"update","data":{"id":"1234","name":"so
我们目前的理解是,URLSession正在缓冲数据,有时它会填满,最后一部分将被切断。我们可以使用curl http://our.service.com
在终端中重现这一点,并查看使用curl -N http://our.service.com
的工作示例
有人知道如何将此选项添加到URLSession
,URLSessionConfiguration
或URLSessionTask
。或者有人有其他解释或解决方案(可能是服务器端)有了这个错误,我们的用户有时会错过数据更新,我们认为这是一些其他无法解释的反馈的原因。
顺便说一句,我们有同样的问题,在我们相应的Android应用程序
2条答案
按热度按时间xzv2uavs1#
我也有同样的错误,没有使用任何库,只是使用我自己的
URLSession
实现。我通过添加HTTP头
Accept: text/event-stream
解决了这个问题。jhkqcmku2#
几年后,现在有了Swift并发,也许有人会发现这很有帮助。
看来原来关于缓冲区填充的问题是一个问题。我发现,通过从连接到SSE端点的服务请求返回
AsyncThrowingStream
,您可以通过AsyncThrowingStream
的bufferingPolicy
控制缓冲区。苹果电脑:AsyncThrowingStream.init(:bufferingPolicy::)
下面的代码是为了可读性。你当然可以使用泛型,使一些东西更可重用,但这得到了重点。
假设你有一个端点,它接受了一个带有主体的POST请求,然后用一个事件流进行响应。正如@Guillaume所提到的,您希望返回
text/event-stream
,而不是典型的JSON。请求体中的JSON可能看起来像这样:
响应可能会将事件以文本的形式返回,如下所示:
您需要了解从服务器返回的事件的格式,以便正确解码。它应该有一个JSON字符串在那里的某个地方,因为你将不得不解码的东西,但它可能没有前缀
data:
。我们将继续假设它是针对这个例子的。下面是发出此请求的示例代码:
1.我们需要保留一个非结构化的任务,这样我们可以在需要的时候取消它。这在下面是有意义的。
1.建立我们的URL请求。没什么特别的
1.调用这个
bytes(for:delegate:)
方法。这个东西很酷,因为它返回(URLSession.AsyncBytes, URLResponse)
。AsyncBytes
部分是关键。Apple Docs1.用定义的
bufferingPolicy
构造AsyncThrowingStream
。在这里,我选择将最新的3个事件保存在缓冲区中。做对你有用或有意义的事情。有一个更简单的AsyncThrowingStream(unfolding:)
,我们可以使用,但看到GOTCHA在通过这些步骤后,在底部的更多信息。1.当连续性终止时(即由于取消),我们应该
cancel()
和零出我们的streamTask
。如果你还在想“什么流任务?“这是第六步。1.这是
streamTask
。我们创建并保留对非结构化Task
的引用来完成我们的工作。通过将它存储在第1步的streamTask
属性中,我们可以在第5步中取消它。1.迭代我们从URLSession调用中得到的
asyncBytes.lines
。看看我们上面的示例响应中的每个data: {...}
事件是如何在一个新行上开始的?这将覆盖每一行。1.检查
line
的内容(记住它只是文本,不是JSON)是否以data:
开头,然后将这些字符剥离并将其转换为Data
。如果不是,我们就有不认识的东西了。继续等待下一个事件。1.从上一步中取出
Data
并对其进行解码。如果成功,则yield
解码的Response
。如果没有,抛出错误,我们就完成了。(您也可以选择不完成,并继续尝试解码流中的下一个事件)。1.假设我们没有抛出上面的任何东西,一旦我们完成了流中的所有事件,我们将
.finish()
。[*] The Gotcha:**
你们中的一些人可能会想:“你在一个令人不安的背景下!为什么要将
AsyncThrowingStream
与同步build
闭包一起使用,后者只需要一个continuation
来 Package 一个非结构化的Task?你破坏了自动取消系统!为什么不返回AsyncThrowingStream(unfolding:)
?”如果你之前没有这么想,或者现在在想“等等,什么?”,在这里:我一开始就试着这么做。将
// 4.
以下的所有内容替换为以下内容,您将获得:天哪,这可好多了。不需要坚持一些非结构化的任务。你会自动取消。可以只返回/抛出,而不是弄乱
continuation
的东西。只是代码更少。然而,我发现这只是 * 有时 * 中途停止(通常发生在较长的响应中)。我可以在Proxyman中看到服务器仍然在发送事件,但是当我将
print(line)
放在for try await line in asyncBytes.lines {}
的顶部时,它只是停止了中途打印。甚至docs也说“默认情况下,缓冲区限制是Int.max,这意味着它是无界的。”所以要么这不是真的,要么有一些非缓冲区相关的问题发生,而使用continuation
的方法不会遇到。如果您使用
AsyncThrowingStream(unfolding:)
方法,请确保进行大量测试。我很想听听是否有人有任何想法,为什么基于
continuation
的方法是唯一有效的方法。