Colusión: redes de dispositivos cercanos con MultipeerConnectivity en iOS
Publicado: 2022-03-11Tradicionalmente, conectar dispositivos para comunicaciones punto a punto ha sido un poco complicado. Una aplicación necesita descubrir qué hay a su alrededor, abrir conexiones en ambos lados y luego mantenerlas a medida que la infraestructura de red, las conexiones, las distancias, etc., todo cambia. Al darse cuenta de las dificultades inherentes a estas actividades, en iOS 7 y macOS 10.10, Apple introdujo su marco MultipeerConnectivity (en adelante, MPC), diseñado para permitir que las aplicaciones realicen estas tareas con un esfuerzo relativamente bajo.
MPC se ocupa de gran parte de la infraestructura necesaria subyacente aquí:
- Compatibilidad con múltiples interfaces de red (Bluetooth, WiFi y ethernet)
- Detección de dispositivos
- Seguridad mediante encriptación
- Paso de mensajes pequeños
- Transferencia de archivos
En este artículo, abordaremos principalmente la implementación de iOS, pero la mayoría, si no todo esto, es aplicable a macOS y tvOS.

Ciclo de vida de la sesión multipar:
-
MCNearbyServiceAdvertiser.startAdvertisingForPeers()
-
MCNearbyServiceBrowser.startBrowsingForPeers()
-
MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
-
MCNearbyServiceBrowser.invitePeer(...)
-
MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
- Llame a
invitationHandler
endidReceiveInvitation
-
Create the MCSession
-
MCSession.send(...)
-
MCSessionDelegate.session(_:didReceive:data:peerID)
-
MCSession.disconnect()
Consulte esta imagen de vez en cuando
Existen numerosos tutoriales y ejemplos de MultipeerConnectivity que pretenden guiar a los desarrolladores de iOS a través de la implementación de una aplicación basada en MPC. Sin embargo, en mi experiencia, por lo general están incompletos y tienden a pasar por alto algunos posibles obstáculos importantes con MPC. En este artículo, espero guiar al lector a través de una implementación rudimentaria de una aplicación de este tipo y señalar las áreas en las que me resultó fácil atascarme.
Conceptos y Clases
MPC se basa en un puñado de clases. Repasemos la lista de los más comunes y desarrollemos nuestra comprensión del marco.
-
MCSession
: una sesión administra todas las comunicaciones entre sus pares asociados. Puede enviar mensajes, archivos y transmisiones a través de una sesión, y su delegado será notificado cuando se reciba uno de estos de un compañero conectado. -
MCPeerID
: una identificación de pares le permite identificar dispositivos de pares individuales dentro de una sesión. Tiene un nombre asociado, pero tenga cuidado: las ID de pares con el mismo nombre no se consideran idénticas (consulte las Reglas básicas, a continuación). -
MCNearbyServiceAdvertiser
: un anunciante le permite transmitir el nombre de su servicio a dispositivos cercanos. Esto les permite conectarse contigo. -
MCNearbyServiceBrowser
: un navegador le permite buscar dispositivos medianteMCNearbyServiceAdvertiser
. El uso de estas dos clases juntas le permite descubrir dispositivos cercanos y crear sus conexiones punto a punto. -
MCBrowserViewController
: proporciona una interfaz de usuario muy básica para explorar los servicios de dispositivos cercanos (vendidos a travésMCNearbyServiceAdvertiser
). Si bien es adecuado para algunos casos de uso, no lo usaremos, ya que, según mi experiencia, uno de los mejores aspectos de MCP es su fluidez.
Reglas de juego
Hay un par de cosas a tener en cuenta al construir una red MPC:
- Los dispositivos se identifican mediante objetos MCPeerID. Estos son, superficialmente, cadenas envueltas y, de hecho, se pueden inicializar con nombres simples. Aunque se pueden crear dos MCPeerID con la misma cadena, no son idénticos. Por lo tanto, los MCPeerID nunca se deben copiar ni volver a crear; deben pasarse dentro de la aplicación. Si es necesario, se pueden almacenar utilizando un NSArchiver.
- Si bien falta documentación al respecto, MCSession se puede usar para comunicarse entre más de dos dispositivos. Sin embargo, en mi experiencia, la forma más estable de utilizar estos objetos es crear uno para cada compañero con el que interactúa su dispositivo.
- MPC no funcionará mientras su aplicación esté en segundo plano. Debe desconectar y eliminar todas sus MCSessions cuando su aplicación esté en segundo plano. No intente hacer más que operaciones mínimas en ninguna tarea en segundo plano.
Primeros pasos con MultipeerConnectivity
Antes de que podamos establecer nuestra red, debemos hacer un poco de limpieza y luego configurar las clases de anunciante y navegador para descubrir otros dispositivos con los que podamos comunicarnos. Vamos a crear un singleton que usaremos para contener algunas variables de estado (nuestro MCPeerID local y cualquier dispositivo conectado), luego crearemos MCNearbyServiceAdvertiser
y MCNearbyServiceBrowser
. Estos dos últimos objetos necesitan un tipo de servicio, que es solo una cadena que identifica su aplicación. Debe tener menos de 16 caracteres y debe ser lo más único posible (es decir, "MyApp-MyCo", no "Multipeer"). Podemos especificar un (pequeño) diccionario para nuestro anunciante que los navegadores pueden leer para brindar un poco más de información al buscar dispositivos cercanos (tal vez un tipo de juego o función de dispositivo).
Dado que MPC se basa en API proporcionadas por el sistema y se correlaciona con objetos del mundo real (otros dispositivos, así como la "red" compartida entre ellos), es una buena opción para el patrón singleton. Si bien se usan en exceso con frecuencia, los singletons son una buena opción para recursos compartidos como este.
Aquí está la definición de nuestro singleton:
class MPCManager: NSObject { var advertiser: MCNearbyServiceAdvertiser! var browser: MCNearbyServiceBrowser! static let instance = MPCManager() let localPeerID: MCPeerID let serviceType = "MPC-Testing" var devices: [Device] = [] override init() { if let data = UserDefaults.standard.data(forKey: "peerID"), let id = NSKeyedUnarchiver.unarchiveObject(with: data) as? MCPeerID { self.localPeerID = id } else { let peerID = MCPeerID(displayName: UIDevice.current.name) let data = try? NSKeyedArchiver.archivedData(withRootObject: peerID) UserDefaults.standard.set(data, forKey: "peerID") self.localPeerID = peerID } super.init() self.advertiser = MCNearbyServiceAdvertiser(peer: localPeerID, discoveryInfo: nil, serviceType: self.serviceType) self.advertiser.delegate = self self.browser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: self.serviceType) self.browser.delegate = self } }
Tenga en cuenta que estamos almacenando nuestro MCPeerID
en los valores predeterminados del usuario (a través de NSKeyedArchiver
) y reutilizándolo. Como se mencionó anteriormente, esto es importante, y el hecho de no almacenarlo en caché de alguna manera puede causar errores ocultos más adelante.
Aquí está nuestra clase de dispositivo, que usaremos para realizar un seguimiento de qué dispositivos se han descubierto y cuál es su estado:
class Device: NSObject { let peerID: MCPeerID var session: MCSession? var name: String var state = MCSessionState.notConnected init(peerID: MCPeerID) { self.name = peerID.displayName self.peerID = peerID super.init() } func invite() { browser.invitePeer(self.peerID, to: self.session!, withContext: nil, timeout: 10) } }
Ahora que hemos construido nuestras clases iniciales, es hora de dar un paso atrás y pensar en la interacción entre los navegadores y los anunciantes. En MPC, un dispositivo puede anunciar un servicio que ofrece y puede buscar un servicio que le interese en otros dispositivos. Dado que nos enfocamos en la comunicación de dispositivo a dispositivo utilizando solo nuestra aplicación, anunciaremos y buscaremos el mismo servicio.
En una configuración cliente/servidor tradicional, un dispositivo (el servidor) anunciaría sus servicios y el cliente los buscaría. Como somos igualitarios, no queremos tener que especificar roles para nuestros dispositivos; tendremos todos los dispositivos tanto para anunciar como para navegar.
Necesitamos agregar un método a nuestro MPCManager
para crear dispositivos a medida que se descubren y rastrearlos en nuestra matriz de dispositivos. Nuestro método tomará un MCPeerID
, buscará un dispositivo existente con esa ID y lo devolverá si lo encuentra. Si aún no tenemos un dispositivo existente, creamos uno nuevo y lo agregamos a nuestra matriz de dispositivos.

func device(for id: MCPeerID) -> Device { for device in self.devices { if device.peerID == id { return device } } let device = Device(peerID: id) self.devices.append(device) return device }
Una vez que un dispositivo ha comenzado a anunciarse, otro dispositivo de navegación puede intentar conectarse a él. Tendremos que agregar métodos de delegado a nuestra clase MPCSession
para manejar las llamadas de delegado entrantes de nuestro anunciante en este caso:
extension MPCManager: MCNearbyServiceAdvertiserDelegate { func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { let device = MPCManager.instance.device(for: peerID) device.connect() invitationHandler(true, device.session) } }
…un método en nuestro Dispositivo para crear la MCSession:
func connect() { if self.session != nil { return } self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required) self.session?.delegate = self }
…y finalmente un método para activar la invitación cuando nuestro navegador descubre un anunciante:
extension MPCManager: MCNearbyServiceBrowserDelegate { func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { let device = MPCManager.instance.device(for: peerID) device.invite(with: self.browser) }
En este momento, estamos ignorando el argumento withDiscoveryInfo
; Podríamos usar esto para filtrar dispositivos particulares según lo que hayan puesto a disposición (este es el mismo diccionario que proporcionamos en el argumento discoveryInfo
para MCNearbyServiceAdvertiser
, arriba).
Conexión de dispositivos
Ahora que nos hemos ocupado de todas nuestras tareas domésticas, podemos comenzar el negocio real de conectar dispositivos.
En el método init de nuestra MPCSession, configuramos tanto nuestro anunciante como nuestro delegado. Cuando estemos listos para comenzar a conectarnos, necesitaremos iniciar ambos. Esto se puede hacer en el método didFinishLaunching del delegado de la aplicación, o cuando sea apropiado. Aquí está el método start()
que agregaremos a nuestra clase:
func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() }
Estas llamadas significarán que su aplicación comenzará a transmitir su presencia a través de WiFi. Tenga en cuenta que no necesita estar conectado a una red WiFi para que esto funcione (pero sí debe tenerlo encendido).
Cuando un dispositivo responde a una invitación e inicia su MCSession, comenzará a recibir devoluciones de llamada de delegados de la sesión. Agregaremos controladores para ellos a nuestro objeto de dispositivo; la mayoría de ellos los ignoraremos por el momento:
extension Device: MCSessionDelegate { public func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { self.state = state NotificationCenter.default.post(name: Multipeer.Notifications.deviceDidChangeState, object: self) } public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { } public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { } public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { } public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { } }
Por el momento, nos preocupa principalmente la devolución de llamada de la session(_:peer:didChangeState:)
. Esto se llamará cada vez que un dispositivo pase a un nuevo estado ( notConnected
, connecting
y connected
). Querremos realizar un seguimiento de esto para poder crear una lista de todos los dispositivos conectados:
extension MPCManager { var connectedDevices: [Device] { return self.devices.filter { $0.state == .connected } } }
Enviando mensajes
Ahora que tenemos todos nuestros dispositivos conectados, es hora de comenzar a enviar mensajes de ida y vuelta. MPC ofrece tres opciones en este sentido:
- Podemos enviar un bloque de bytes (un objeto de datos)
- Podemos enviar un archivo
- Podemos abrir un stream al otro dispositivo
En aras de la simplicidad, solo veremos la primera de estas opciones. Enviaremos mensajes simples de un lado a otro, y no nos preocuparemos demasiado por las complejidades de los tipos de mensajes, el formato, etc. Usaremos una estructura codificable para encapsular nuestro mensaje, que se verá así:
struct Message: Codable { let body: String }
También agregaremos una extensión al dispositivo para enviar uno de estos:
extension Device { func send(text: String) throws { let message = Message(body: text) let payload = try JSONEncoder().encode(message) try self.session?.send(payload, toPeers: [self.peerID], with: .reliable) } } ~~~swift Finally, we'll need to modify our `Device.session(_:didReceive:fromPeer)` code to receive the message, parse it, and notify any interested objects about it:
static let messageReceivedNotification = Notification.Name(“DeviceDidReceiveMessage”) public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { if let message = try? JSONDecoder().decode(Message.self, from: data) { NotificationCenter.default.post(name: Device.messageReceivedNotification, object: message, userInfo: [“from”: self]) } }
## Disconnections Now that we've got a connection created between multiple devices, we have to be able to both disconnect on demand and also handle system interruptions. One of the undocumented weaknesses of MPC is that it doesn't function in the background. We need to observe the `UIApplication.didEnterBackgroundNotification` notification, and make sure that we shut down all our sessions. Failure to do this will lead to undefined states in the sessions and devices and can cause lots of confusing, hard-to-track-down errors. There is a temptation to use a background task to keep your sessions around, in case the user jumps back into your app. However, this is a bad idea, as MPC will usually fail within the first second of being backgrounded. When your app returns to the foreground, you can rely on MPC's delegate methods to rebuild your connections. In our MPCSession's `start()` method, we'll want to observe this notification and add code to handle it and shut down all our sessions. ~~~swift func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil) } @objc func enteredBackground() { for device in self.devices { device.disconnect() } } func disconnect() { self.session?.disconnect() self.session = nil }
Conclusiones
Este artículo cubre la arquitectura requerida para desarrollar los componentes de red de una aplicación basada en MultipeerConnectivity. El código fuente completo (disponible en Github) ofrece un contenedor de interfaz de usuario mínimo que le permite ver los dispositivos conectados y enviar mensajes entre ellos.
MPC ofrece una conectividad casi perfecta entre dispositivos cercanos sin necesidad de preocuparse por redes WiFi, Bluetooth o gimnasia compleja de cliente/servidor. Ser capaz de emparejar rápidamente algunos teléfonos para una sesión de juego corta, o conectar dos dispositivos para compartir, se hace al estilo típico de Apple.
El código fuente de este proyecto está disponible en Github en https://github.com/bengottlieb/MultipeerExample.
¿Estás diseñando un iOS que use AFNetworking? El patrón de diseño Modelo-Vista-Controlador (MVC) es excelente para una base de código mantenible, pero a veces necesita una sola clase para manejar su red debido a preocupaciones como el código DRY, el registro de red centralizado y, especialmente, la limitación de velocidad. Lea todo sobre el manejo de esto con una clase Singleton en redes desacopladas y centralizadas de iOS: tutorial AFNetworking con una clase Singleton