#swift #networking
#swift #сеть
Вопрос:
Я весь день боролся с NWConnection за получение данных по длительно работающему TCP-сокету. Я, наконец, заработал, вызвав у себя следующие ошибки из-за отсутствия документации:
- Неполные данные (из-за только вызова receive один раз)
- Вывод данных TCP из строя (из-за «опроса», получаемого от таймера … что приводит к нескольким одновременным закрытиям, ожидающим получения данных).
- Страдающий бесконечными циклами (из-за перезапуска receive после получения без проверки «IsComplete» Bool- как только сокет завершается с другого конца, это …. плохо … очень плохо).
Краткое изложение того, что я узнал:
- Как только вы перейдете в состояние .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 amp;amp; isComplete == false {
self.receive()
}
}
}
Комментарии:
1. Жаль, что я не нашел этот пост сегодня утром. Я борюсь с проблемой, связанной с отправкой нескольких битов данных с использованием connection. отправка, затем получение соединения, позволяет объединить данные вместе. Должен ли я обрабатывать это как нечто, что просто происходит в сети, или я должен регулировать свои отправки, или я должен отправлять другим способом?
2. Я не могу ответить на этот вопрос, но это звучит как отличный вопрос, особенно если вы включаете свой код. Я хотел бы увидеть больше примеров кода NWconnection.
3. Таким образом, получается, что я вроде как использовал это неправильно (AFAIK). Я рассматривал соединение как канал, который вы открываете, а затем постоянно добавляете что-то в него. Когда я вместо этого посмотрел на это как на NWConnection, который используется для отправки одной вещи, а затем закрывается после этой единственной вещи, все начало работать правильно.
4. Если вы хотите подключаться много раз, вы можете обработать
newConnectionHandler
и перезапустить NWListener и NWConnection на сервере.5. Таймер не нужен. Вы должны обработать
NWConnection.receiveMessage
для получения сообщений и вызвать receiveNextMessage(), чтобы получить следующий.
Ответ №1:
Я думаю, вы можете использовать соединение на короткое время много раз. Например, клиент подключается к хосту и просит хост что-то сделать, а затем сообщает хосту отключить соединение. Хост переключается в режим ожидания, чтобы подготовить новое соединение. Смотрите схему ниже.
У вас должен быть установлен таймер подключения для отключения открытого соединения, когда клиент не отправляет событие close connection или answer хосту в течение определенного времени.
Комментарии:
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 amp;amp; isComplete == false, var data = data, !data.isEmpty {
// do something for detect heart packet
weakSelf.parseHeartBeat(amp;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()
}
}
}
Комментарии:
1. есть ли причина, по которой только один пользователь может получать сообщения с помощью этого подхода?
2. Я не уверен, к чему вы клоните, спецификация пакета приема отличается особым дизайном серверной части и методом шифрования, но обычным способом было сначала вычислить длину байтов заголовка пакета и расшифровать, затем продолжить расшифровку и проанализировать тело пакета, а затем в конечном итоге получить объект ответа, такой как необработанные данные или сообщение вашего значения.