Правильный способ использования NWConnection для продолжительного TCP-сокета

#swift #networking

#swift #сеть

Вопрос:

Я весь день боролся с NWConnection за получение данных по длительно работающему TCP-сокету. Я, наконец, заработал, вызвав у себя следующие ошибки из-за отсутствия документации:

  1. Неполные данные (из-за только вызова receive один раз)
  2. Вывод данных TCP из строя (из-за «опроса», получаемого от таймера … что приводит к нескольким одновременным закрытиям, ожидающим получения данных).
  3. Страдающий бесконечными циклами (из-за перезапуска receive после получения без проверки «IsComplete» Bool- как только сокет завершается с другого конца, это …. плохо … очень плохо).

Краткое изложение того, что я узнал:

  1. Как только вы перейдете в состояние .ready, вы можете вызвать receive … один и только один раз
  2. Как только вы получите некоторые данные, вы можете снова вызвать 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. Я не уверен, к чему вы клоните, спецификация пакета приема отличается особым дизайном серверной части и методом шифрования, но обычным способом было сначала вычислить длину байтов заголовка пакета и расшифровать, затем продолжить расшифровку и проанализировать тело пакета, а затем в конечном итоге получить объект ответа, такой как необработанные данные или сообщение вашего значения.