我们有一个移动应用程序,可通过各种双向流向服务器传输大量数据。有时需要关闭流(例如,当应用程序背景化时)。然后根据需要重新打开它们。有时,当发生这种情况时,出现问题:
我们的代码在下面进行了大量简化,但是这应该提供足够的细节来表明我们正在做什么。双向流由Switch
类管理:
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()
。我们会定期致电restartIfNeeded()
,例如。当使用流的屏幕进入视图时。
正如我上面提到的,偶尔发生的是当服务器将数据写入流时,我们的响应处理程序(rpcResponse
)停止被调用。流似乎是健康的(服务器接收我们写入的数据,protocall.state
既不是.notStarted也不是.finished)。但是甚至没有执行响应处理程序第一行的日志。
第一个问题:我们是正确管理流,还是我们停止和重新启动容易出错的流的方式?如果是这样,做这样的事情的正确方法是什么?
第二个问题:我们如何调试这个?我们可以想到的一切,我们可以查询状态告诉我们流已经启动并运行,但感觉就像objc gRPC库保留了很多隐藏在我们身上的机制。有没有办法看看服务器的响应是否做到达我们,但是无法触发我们的响应处理程序?
第三个问题:根据上面的代码,我们使用库提供的GRXBufferedPipe。它的文档建议不要在生产中使用它,因为它没有推回机制。根据我们的理解,编写器仅用于以同步,一次一个的方式向gRPC核心提供数据,并且由于服务器从我们这里接收数据很好,我们不认为这是一个问题。我们错了吗?作者是否还参与将从服务器接收的数据提供给我们的响应处理程序?即如果作者因超载而崩溃,那么从流中读取数据是否有问题,而不是写入?
更新:在问这个问题一年多后,我们终于在服务器端代码中发现了一个死锁错误,导致客户端出现这种情况。流似乎挂起,因为客户端发送的通信没有由服务器处理,反之亦然,但流实际上还活着。接受的答案为如何管理这些双向流提供了很好的建议,我认为这仍然很有价值(它帮助了我们很多!)。但问题实际上是由于编程错误。
此外,对于遇到此类问题的任何人,可能值得调查一下,当iOS更改其网络时,您是否正在体验this known issue通道被静默删除的情况。 This readme提供了使用Apple的CFStream API而不是TCP套接字作为该问题的可能修复程序的说明。
答案 0 :(得分:2)
第一个问题:我们是正确管理流,还是我们停止和重新启动容易出错的流的方式?如果是这样,做这样的事情的正确方法是什么?
通过查看代码我可以看出,start()
函数似乎是正确的。在stop()
函数中,您无需致电cancel()
的{{1}};呼叫将使用之前的self.protocall
完成。
self.writer.finishWithError(nil)
是一个有点凌乱的地方。首先,您不应该自己轮询/设置needsrestart()
的状态。该状态本身就会改变。其次,设置这些状态不会关闭您的流。它只会暂停一个作家,如果app在后台,暂停一个作家就像一个无操作。如果要关闭流,则应使用protocall
来终止此呼叫,并在以后需要时启动新呼叫。
第二个问题:我们如何调试这个?
一种方法是打开gRPC日志(GRPC_TRACE和GRPC_VERBOSITY)。另一种方法是在here设置断点,其中gRPC objc库从服务器接收gRPC消息。
第三个问题:作者是否也参与将从服务器收到的数据提供给我们的响应处理程序?
没有。如果您创建缓冲管道并将其作为呼叫请求提供,则它仅提供要发送到服务器的数据。接收路径由另一个编写器(实际上是您的finishWithError
对象)处理。
我不知道不鼓励在生产中使用protocall
的地方。此实用程序的已知缺点是,如果您暂停编写器但仍使用GRXBufferedPipe
向其写入数据,则最终会缓冲大量数据而无法刷新它们,这可能会导致内存问题。