将 NWConnection 用于长时间运行的 TCP 套接字的正确方法
Posted
技术标签:
【中文标题】将 NWConnection 用于长时间运行的 TCP 套接字的正确方法【英文标题】:Correct way to use NWConnection for long-running TCP socket 【发布时间】:2019-03-04 04:12:10 【问题描述】:我整天都在与 NWConnection 斗争,以便在长时间运行的 TCP 套接字上接收数据。由于缺乏文档,在对自己造成以下错误后,我终于让它工作了:
-
数据不完整(由于只调用receive一次)
无序获取 TCP 数据(由于从计时器“轮询”接收...导致多个同时关闭等待获取数据)。
遭受无限循环(由于在接收后重新启动接收而没有检查“isComplete”布尔值——一旦套接字从另一端终止,这是....糟糕......非常糟糕)。
我所学的总结:
-
一旦您处于 .ready 状态,您就可以调用 receive...一次且仅一次
收到一些数据后,您可以再次调用 receive...但前提是您仍处于 .ready 状态并且 isComplete 为 false。
这是我的代码。我认为这是对的。但如果有错误请告诉我:
queue = DispatchQueue(label: "hostname", attributes: .concurrent)
let serverEndpoint = NWEndpoint.Host(hostname)
guard let portEndpoint = NWEndpoint.Port(rawValue: port) else return nil
connection = NWConnection(host: serverEndpoint, port: portEndpoint, using: .tcp)
connection.stateUpdateHandler = [weak self] (newState) in
switch newState
case .ready:
debugPrint("TcpReader.ready to send")
self?.receive()
case .failed(let error):
debugPrint("TcpReader.client failed with error \(error)")
case .setup:
debugPrint("TcpReader.setup")
case .waiting(_):
debugPrint("TcpReader.waiting")
case .preparing:
debugPrint("TcpReader.preparing")
case .cancelled:
debugPrint("TcpReader.cancelled")
func receive()
connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) (content, context, isComplete, error) in
debugPrint("\(Date()) TcpReader: got a message \(String(describing: content?.count)) bytes")
if let content = content
self.delegate.gotData(data: content, from: self.hostname, port: self.port)
if self.connection.state == .ready && isComplete == false
self.receive()
【问题讨论】:
我希望今天早上能找到这篇文章。我正在努力解决如果我使用 connection.send 发送多个数据位然后连接接收将数据组合在一起的问题。我应该将其视为仅在网络上发生的事情,还是应该限制我的发送,还是应该以不同的方式发送? 我无法回答这个问题,但这听起来是一个很好的问题,尤其是在您包含代码的情况下。我希望看到更多 NWconnection 代码示例。 所以,这表明我有点以错误的方式使用它(AFAIK)。我把连接看作是你打开的管道,然后不断地把东西放进去。当我将其视为 NWConnection 是用于发送单个事物并在该单个事物之后关闭时,一切都开始正常工作。 如果要多次连接可以处理newConnectionHandler
,在服务器上重启NWListener和NWConnection。
不需要定时器。您应该处理NWConnection.receiveMessage
来获取消息并调用receiveNextMessage() 来获取下一个。
【参考方案1】:
我认为您可以多次使用短时间连接。例如,客户端连接到主机并要求主机做某事,然后告诉主机关闭连接。主机切换到等待模式以准备新的连接。见下图。
当客户端在特定时间内没有向主机发送关闭连接或应答事件时,您应该有连接计时器来关闭打开的连接。
【讨论】:
【参考方案2】:在一个长时间运行的 TCP 套接字上,你应该实现自定义的心跳来监控连接状态是活动的还是断开的。
心跳可以作为消息或加密数据发送,通常根据服务器规范文件来实现。
下面作为示例概念代码来解释流程以供参考(没有网络数据包内容处理程序)。
我不能保证这种方法是常见且正确的,但这对我的项目有效。
import Network
class NetworkService
lazy var heartbeatTimeoutTask: DispatchWorkItem =
return DispatchWorkItem self.handleHeartbeatTimeOut()
()
lazy var connection: NWConnection =
// Create the connection
let connection = NWConnection(host: "x.x.x.x", port: 1234, using: self.parames)
connection.stateUpdateHandler = self.listenStateUpdate(to:)
return connection
()
lazy var parames: NWParameters =
let parames = NWParameters(tls: nil, tcp: self.tcpOptions)
if let isOption = parames.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options
isOption.version = .v4
parames.preferNoProxies = true
parames.expiredDNSBehavior = .allow
parames.multipathServiceType = .interactive
parames.serviceClass = .background
return parames
()
lazy var tcpOptions: NWProtocolTCP.Options =
let options = NWProtocolTCP.Options()
options.enableFastOpen = true // Enable TCP Fast Open (TFO)
options.connectionTimeout = 5 // connection timed out
return options
()
let queue = DispatchQueue(label: "hostname", attributes: .concurrent)
private func listenStateUpdate(to state: NWConnection.State)
// Set the state update handler
switch state
case .setup:
// init state
debugPrint("The connection has been initialized but not started.")
case .waiting(let error):
debugPrint("The connection is waiting for a network path change with: \(error)")
self.disconnect()
case .preparing:
debugPrint("The connection in the process of being established.")
case .ready:
// Handle connection established
// this means that the handshake is finished
debugPrint("The connection is established, and ready to send and receive data.")
self.receiveData()
self.sendHeartbeat()
case .failed(let error):
debugPrint("The connection has disconnected or encountered an: \(error)")
self.disconnect()
case .cancelled:
debugPrint("The connection has been canceled.")
default:
break
// MARK: - Socket I/O
func connect()
// Start the connection
self.connection.start(queue: self.queue)
func disconnect()
// Stop the connection
self.connection.stateUpdateHandler = nil
self.connection.cancel()
private func sendPacket()
var packet: Data? // do something for heartbeat packet
self.connection.send(content: packet, completion: .contentProcessed( (error) in
if let err = error
// Handle error in sending
debugPrint("encounter an error with: \(err) after send Packet")
else
// Send has been processed
))
private func receiveData()
self.connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) [weak self] (data, context, isComplete, error) in
guard let weakSelf = self else return
if weakSelf.connection.state == .ready && isComplete == false, var data = data, !data.isEmpty
// do something for detect heart packet
weakSelf.parseHeartBeat(&data)
// MARK: - Heartbeat
private func sendHeartbeat()
// sendHeartbeatPacket
self.sendPacket()
// trigger timeout mission if the server no response corresponding packet within 5 second
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5.0, execute: self.heartbeatTimeoutTask)
private func handleHeartbeatTimeOut()
// this's sample time out mission, you can customize this chunk
self.heartbeatTimeoutTask.cancel()
self.disconnect()
private func parseHeartBeat(_ heartbeatData: inout Data)
// do something for parse heartbeat
// cancel heartbeat timeout after parse packet success
self.heartbeatTimeoutTask.cancel()
// send heartbeat for monitor server after computing 15 second
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 15.0)
self.sendHeartbeat()
【讨论】:
以上是关于将 NWConnection 用于长时间运行的 TCP 套接字的正确方法的主要内容,如果未能解决你的问题,请参考以下文章