Ios 双向gRPC流有时在停止和启动后停止处理响应 简言之
我们有一个移动应用程序,它通过各种双向流向服务器和从服务器传输相当大的数据量。有时需要关闭流(例如,当应用程序处于后台时)。然后根据需要重新开放。有时发生这种情况时,会出现一些问题:Ios 双向gRPC流有时在停止和启动后停止处理响应 简言之,ios,swift,grpc,Ios,Swift,Grpc,我们有一个移动应用程序,它通过各种双向流向服务器和从服务器传输相当大的数据量。有时需要关闭流(例如,当应用程序处于后台时)。然后根据需要重新开放。有时发生这种情况时,会出现一些问题: 据我所知,流在设备端启动并运行(GRPCProtocall和相关GRXWriter的状态都是已启动或已暂停) 设备在流上发送数据(服务器接收数据) 服务器似乎很好地将数据发送回设备(服务器的流。发送调用成功返回) 在设备上,从未调用流上接收的数据的结果处理程序 更多细节 我们的代码在下面被大大简化了,但这应该能
- 据我所知,流在设备端启动并运行(GRPCProtocall和相关GRXWriter的状态都是已启动或已暂停)
- 设备在流上发送数据(服务器接收数据)
- 服务器似乎很好地将数据发送回设备(服务器的流。发送调用成功返回)
- 在设备上,从未调用流上接收的数据的结果处理程序
开关
类管理:
class Switch {
/** The protocall over which we send and receive data */
var protocall: GRPCProtoCall?
/** The writer object that writes data to the protocall. */
var writer: GRXBufferedPipe?
/** A static GRPCProtoService as per the .proto */
static let service = APPDataService(host: Settings.grpcHost)
/** A response handler. APPData is the datatype defined by the .proto. */
func rpcResponse(done: Bool, response: APPData?, error: Error?) {
NSLog("Response received")
// Handle response...
}
func start() {
// Create a (new) instance of the writer
// (A writer cannot be used on multiple protocalls)
self.writer = GRXBufferedPipe()
// Setup the protocall
self.protocall = Switch.service.rpcToStream(withRequestWriter: self.writer!, eventHandler: self.rpcRespose(done:response:error:))
// Start the stream
self.protocall.start()
}
func stop() {
// Stop the writer if it is started.
if self.writer.state == .started || self.writer.state == .paused {
self.writer.finishWithError(nil)
}
// Stop the proto call if it is started
if self.protocall?.state == .started || self.protocall?.state == .paused {
protocall?.cancel()
}
self.protocall = nil
}
private var needsRestart: Bool {
if let protocall = self.protocall {
if protocall.state == .notStarted || protocall.state == .finished {
// protocall exists, but isn't running.
return true
} else if writer.state == .notStarted || writer.state == .finished {
// writer isn't running
return true
} else {
// protocall and writer are running
return false
}
} else {
// protocall doesn't exist.
return true
}
}
func restartIfNeeded() {
guard self.needsRestart else { return }
self.stop()
self.start()
}
func write(data: APPData) {
self.writer.writeValue(data)
}
}
正如我所说,它被大大简化了,但它显示了我们如何启动、停止和重新启动流,以及我们如何检查流是否健康
当应用程序处于后台时,我们调用stop()
。当它被预先固定并且我们再次需要该流时,我们调用start()
。我们定期调用restartifneed()
,例如,当使用流的屏幕进入视图时
正如我前面提到的,当服务器向流写入数据时,偶尔会发生响应处理程序(rpresponse
)停止被调用的情况。流看起来是健康的(服务器接收我们写入它的数据,并且protocall.state
既不是.notStarted也不是.finished)。但即使响应处理程序第一行上的日志也不会执行
第一个问题:我们是否正确管理流,或者我们停止和重新启动流的方式是否容易出错?如果是的话,这样做的正确方式是什么
第二个问题:我们如何调试它?我们所能想到的所有我们可以查询到的状态都告诉我们流已经启动并运行,但感觉好像objc gRPC库对我们隐藏了很多机制。是否有一种方法可以查看来自服务器的响应是否能够到达我们,但无法触发我们的响应处理程序
第三个问题:根据上面的代码,我们使用库提供的GRXBufferedPipe。它的文档建议不要在生产中使用它,因为它没有一个后推机制。据我们了解,writer仅用于以同步、一次一个的方式将数据馈送到gRPC core,而且由于服务器可以很好地接收我们的数据,因此我们认为这不是问题。但我们错了吗?编写器是否也参与将从服务器接收的数据提供给我们的响应处理程序?也就是说,如果写入程序因过载而中断,是否会表现为从流中读取数据而不是写入数据时出现问题
更新:在提出这个问题一年多之后,我们终于在服务器端代码中发现了一个死锁bug,它导致了客户端的这种行为。这些流看起来是挂起的,因为客户端发送的通信没有被服务器处理,反之亦然,但这些流实际上是活动的,并且运行良好。公认的答案为如何管理这些双向流提供了很好的建议,我认为这仍然很有价值(它帮助了我们很多!)。但问题实际上是由于编程错误
此外,对于任何遇到此类问题的人来说,可能值得调查一下,当iOS更改其网络时,您是否遇到了频道被悄悄删除的情况。提供使用Apple的CFStream API而不是TCP套接字作为该问题的可能修复方法的说明
第一个问题:我们是否正确管理流,或者我们停止和重新启动流的方式是否容易出错?如果是的话,这样做的正确方式是什么
通过查看代码可以看出,start()
函数似乎是正确的。在stop()
函数中,您不需要调用self.protocall的cancel()
;调用将使用上一个self.writer.finishWithError(nil)
完成
needsrestart()
就是有点乱的地方。首先,您不应该自己轮询/设置protocall
的状态。这种状态是自己改变的。其次,设置这些状态不会关闭流。它只会暂停写入程序,如果应用程序在后台,暂停写入程序就像禁止操作一样。如果您想关闭流,您应该使用finishwhereror
终止此呼叫,并可能在需要时稍后启动新呼叫
第二个问题:我们如何调试它
一种方法是打开gRPC日志(gRPC_跟踪和gRPC_详细)。另一种方法是在gRPC objc库从服务器接收gRPC消息的位置设置断点
第三个问题:编写器是否也参与将从服务器接收到的数据提供给我们的响应处理程序
不可以。若您创建了一个缓冲管道,并将其作为调用的请求提供,那个么它只提供要发送到服务器的数据。接收路径由另一个编写器(实际上是您的protocall
对象)处理
我不认为在生产中不鼓励使用GRXBufferedPipe
。此实用程序的已知缺点是,如果暂停写入程序,但继续使用writeWithValue
向其写入数据,则最终会缓冲大量数据而无法刷新这些数据,这可能会导致内存问题。是否需要向self添加一些代理?比如service.delegate=self之类的?我不知道;看不到任何回调。初始化protocall时,响应处理程序将注册到protocall(self.protocall=Switch.service.rpcToStream(withRequestWriter:self