The dark corners of the Multipeer Connectivity Framework

Introduction

During my work on the ThaliProject I had an outstanding chance to explore the strength and and at the same time weakness of Apple’s multipeer connectivity framework. Thali is a cross platform Cordova plugin for peer to peer communication. My job was to enable this framework to run peer to peer on iOS. Unfortunately, Apple’s documentation left something to be desired. During my time on this project I had to dig down into the guts of the MPCF to see how it behaves in some undocumented scenarios. All cases listed below are correct as of for iOS9, although perhaps Apple have changed something in iOS10.

Discovery and Session phases

Data exchange in MPCF consists of 2 parts: the Discovery phase where devices find each other and Session phase when devices establish connections between each other. The basic flows are explained clearly in the documentation.

MCPeerID

In working with the MPCF I thought that dealing with MCPeerIDs should be easy. Look at the raw MCPeerID object, its interface just contains a displayName: String property:

open class MCPeerID : NSObject, NSCopying, NSSecureCoding {

public init(displayName myDisplayName: String)

open var displayName: String { get }
}

Managing multiple MCSessions

In the simplest scenario one device is always an Advertiser and another is always a Browser. But in the Thali context we have to enable devices to be both an Advertiser and Browser at the same time. The reason is that in Thali it is quite common for one device to want to send something to to another device (advertise) and simultaneously listen for changes from other devices (browse). In the WWDC video about MPCF, Apple strongly recommends using only one MCPeerID for all your multipeer connectivity needs. This means if you started advertising with one instance of MCPeerID you should use the same object for future both advertising and browsing. Relying on this information Thali used the same instance of MCPeerID for MCNearbyServiceAdvertiser and for MCNearbyServiceBrowser. But this approach caused a serious problem:

  • Everything looks fine — MCNearbyServiceBrowserDelegate on each phone discovers other device — A found B and B found A
  • Then these 2 devices try to invite each other to connect. The related MCSession objects move to the .Connecting state.
  • But after a second all the MCSession objects on both sides move to the .NotConnected state.

Zombie Advertisers

The next chapter of our story with the MPCF started when we ran automated tests on real devices to make sure that our native code worked as expected. We have several discovery tests — each one executes calls to start advertising and browsing in setup and to stop advertising and browsing in teardown. Everything looked clean. But our tests were failing randomly. Well…

  • announcements from the Advertiser are stored on the Browser’s side in a queue, but this queue isn’t cleared out on stopListening call.
  1. Create and start Browser B1;
  2. B1 received foundPeer events for A1, A2, ..., AN;
  3. Call stopAdvertising for A1, A2, ..., AN;
  4. Call stopBrowsing for B1 and asynchronously release B1;
  5. Before actually being destroyed B1 has time to receive lostPeer for M Advertisers N, where M<=N;
  6. Create and start Browser B2;
  7. B2 receives N-M foundPeer announcements from Advertisers from the first step and right after that B2 receives another N-M announcements with lostPeer.

MPCF under pressure

We also wrote test project to see how the MPCF behaves when it is maintaining active sessions and in parallel some peer wants to create a new session or join an existing one. All the code is available on github. In README file you can see detailed description for each test. These tests uncover some interesting facts. Depending on what network we use we have limited channel bandwidth (bigger for WiFi and smaller for Bluetooth). This channel is used to transfer data between peers. And if this channel isn’t full we can create new MCSessions or join existing ones. But if we have filled the channel with data then our attempts to create new sessions or join existing ones will fail randomly.

Software engineer, teacher-volunteer, long distance runner-amateur. Born in Smolichy, living in Minsk

Software engineer, teacher-volunteer, long distance runner-amateur. Born in Smolichy, living in Minsk