Multipeer Connectivity
As consumer web technologies and enterprises race towards cloud infrastructure, there is a curious and significant counter-movement towards connected devices.
In this age of mobile computing, the possibilities of collaboration, whether in work or play, have never been greater. In this age of privacy concerns and mass surveillance, the need for secure, ad hoc communications has never been more prescient. In this age of connected devices, the promise of mastery over the everyday objects of our lives has never been closer at hand.
The Multipeer Connectivity APIs, introduced in iOS 7, therefore may well be the most significant for the platform. It allows developers to completely reimagine how mobile apps are built, and to redefine what is possible. And we’re not just talking about successors to the lame bump-to-send-contact-information genre, either: multi-peer connectivity has implications on everything from collaborative editing and file sharing to multiplayer gaming and sensor aggregation.
Multipeer Connectivity is a framework that enables nearby devices to communicate over infrastructure Wi-Fi networks, peer-to-peer Wi-Fi, and Bluetooth personal area networks. Connected peers are able securely transmit messages, streams, or file resources to other devices without going through an intermediary web service.
Advertising & Discovering
The first step in communication is to make peers aware of one another. This is accomplished by advertising and discovering services.
Advertising makes a service known to other peers, while discovery is the inverse process of the client being made aware of services advertised by other peers. In many cases, clients both discover and advertise for the same service, which can lead to some initial confusion—especially to anyone rooted in the client-server paradigm.
Each service is identified by a type, which is a short text string of ASCII letters, numbers, and dashes, up to 15 characters in length. By convention, a service name should begin with the app name, followed by a dash and a unique descriptor for that service (think of it as simplified com.apple.*
-esque reverse-DNS notation):
static NSString * const XXService Type = @"xx-service";
Peers are uniquely identified by an MCPeer
object, which are initialized with a display name. This could be a user-specified nickname, or simply the current device name:
MCPeer ID *local Peer ID = [[MCPeer ID alloc] init With Display Name:[[UIDevice current Device] name]];
Peers can be also be advertised or discovered manually using
NSNet
or the Bonjour C APIs, but this is a rather advanced and specific concern. Additional information about manual peer management can be found in theService MCSession
documentation.
Advertising
Services are advertised by the MCNearby
, which is initialized with a local peer, service type, and any optional information to be communicated to peers that discover the service.
Discovery information is sent as Bonjour
TXT
records encoded according to RFC 6763.
MCNearby Service Advertiser *advertiser =
[[MCNearby Service Advertiser alloc] init With Peer:local Peer ID
discovery Info:nil
service Type:XXService Type];
advertiser.delegate = self;
[advertiser start Advertising Peer];
Events are handled by the advertiser’s delegate
, conforming to the MCNearby
protocol.
As an example implementation, consider a client that allows the user to choose whether to accept or reject incoming connection requests, with the option to reject and block any subsequent requests from that peer:
#pragma mark - MCNearby Service Advertiser Delegate
- (void)advertiser:(MCNearby Service Advertiser *)advertiser
did Receive Invitation From Peer:(MCPeer ID *)peer ID
with Context:(NSData *)context
invitation Handler:(void(^)(BOOL accept, MCSession *session))invitation Handler
{
if ([self.mutable Blocked Peers contains Object:peer ID]) {
invitation Handler(NO, nil);
return;
}
[[UIAction Sheet action Sheet With Title:[NSString string With Format:NSLocalized String(@"Received Invitation from %@", @"Received Invitation from {Peer}"), peer ID.display Name]
cancel Button Title:NSLocalized String(@"Reject", nil)
destructive Button Title:NSLocalized String(@"Block", nil)
other Button Titles:@[NSLocalized String(@"Accept", nil)]
block:^(UIAction Sheet *action Sheet, NSInteger button Index)
{
BOOL accepted Invitation = (button Index == [action Sheet first Other Button Index]);
if (button Index == [action Sheet destructive Button Index]) {
[self.mutable Blocked Peers add Object:peer ID];
}
MCSession *session = [[MCSession alloc] init With Peer:local Peer ID
security Identity:nil
encryption Preference:MCEncryption None];
session.delegate = self;
invitation Handler(accepted Invitation, (accepted Invitation ? session : nil));
}] show In View:self.view];
}
For sake of simplicity, this example contrives a block-based initializer for
UIAction
, which allows for theSheet invitation
to be passed directly into the action sheet responder in order to avoid the messy business of creating and managing a custom delegate object. This method can be implemented in a category, or adapted from any of the implementations available on CocoaPodsHandler
Creating a Session
As in the example above, sessions are created by advertisers, and passed to peers when accepting an invitation to connect. An MCSession
object is initialized with the local peer identifier, as well as security
and encryption
parameters.
MCSession *session = [[MCSession alloc] init With Peer:local Peer ID
security Identity:nil
encryption Preference:MCEncryption None];
session.delegate = self;
security
is an optional parameter that allows peers to securely identify peers by X.509 certificates. When specified, the first object should be an Sec
identifying the client, followed by one or more Sec
objects than can be used to verify the local peer’s identity.
The encryption
parameter specifies whether to encrypt communication between peers. Three possible values are provided by the MCEncryption
enum:
-
MCEncryption
: The session prefers to use encryption, but will accept unencrypted connections.Optional -
MCEncryption
: The session requires encryption.Required -
MCEncryption
: The session should not be encrypted.None
Enabling encryption can significantly reduce transfer rates, so unless your application specifically deals with user-sensitive information,
MCEncryption
is recommended.None
The MCSession
protocol will be covered in the section on sending and receiving information.
Discovering
Clients can discover advertised services using MCNearby
, which is initialized with the local peer identifier and the service type, much like for MCNearby
.
MCNearby Service Browser *browser = [[MCNearby Service Browser alloc] init With Peer:local Peer ID service Type:XXService Type];
browser.delegate = self;
There may be many peers advertising a particular service, so as a convenience to the user (and the developer), the MCBrowser
offers a built-in, standard way to present and connect to advertising peers:
MCBrowser View Controller *browser View Controller =
[[MCBrowser View Controller alloc] init With Browser:browser
session:session];
browser View Controller.delegate = self;
[self present View Controller:browser View Controller
animated:YES
completion:
^{
[browser start Browsing For Peers];
}];
When a browser has finished connecting to peers, it calls -browser
on its delegate, to notify the presenting view controller that it should update its UI to accommodate the newly-connected clients.
Sending & Receiving Information
Once peers are connected to one another, information can be sent between them. The Multipeer Connectivity framework distinguishes between three different forms of data transfer:
- Messages are information with well-defined boundaries, such as short text or small serialized objects.
- Streams are open channels of information used to continuously transfer data like audio, video, or real-time sensor events.
- Resources are files like images, movies, or documents.
Messages
Messages are sent with -send
:
NSString *message = @"Hello, World!";
NSData *data = [message data Using Encoding:NSUTF8String Encoding];
NSError *error = nil;
if (![self.session send Data:data
to Peers:peers
with Mode:MCSession Send Data Reliable
error:&error]) {
NSLog(@"[Error] %@", error);
}
Messages are received through the MCSession
method -session
. Here’s how one would decode the message sent in the previous code example:
#pragma mark - MCSession Delegate
- (void)session:(MCSession *)session
did Receive Data:(NSData *)data
from Peer:(MCPeer ID *)peer ID
{
NSString *message =
[[NSString alloc] init With Data:data
encoding:NSUTF8String Encoding];
NSLog(@"%@", message);
}
Another approach would be to send NSKeyed
-encoded objects:
id <NSSecure Coding> object = …;
NSData *data = [NSKeyed Archiver archived Data With Root Object:object];
NSError *error = nil;
if (![self.session send Data:data
to Peers:peers
with Mode:MCSession Send Data Reliable
error:&error]) {
NSLog(@"[Error] %@", error);
}
#pragma mark - MCSession Delegate
- (void)session:(MCSession *)session
did Receive Data:(NSData *)data
from Peer:(MCPeer ID *)peer ID
{
NSKeyed Unarchiver *unarchiver = [[NSKeyed Unarchiver alloc] init For Reading With Data:data];
unarchiver.requires Secure Coding = YES;
id object = [unarchiver decode Object];
[unarchiver finish Decoding];
NSLog(@"%@", object);
}
In order to guard against object substitution attacks, it is important to set
requires
toSecure Coding YES
, such that an exception is thrown if the root object class does not conform to<NSSecure
. For more information, see the [NSHipster article on NSSecureCoding.Coding>
Streams
Streams are created with -start
:
NSOutput Stream *output Stream =
[session start Stream With Name:name
to Peer:peer];
stream.delegate = self;
[stream schedule In Run Loop:[NSRun Loop main Run Loop]
for Mode:NSDefault Run Loop Mode];
[stream open];
…
Streams are received by the MCSession
with -session:did
:
#pragma mark - MCSession Delegate
- (void)session:(MCSession *)session
did Receive Stream:(NSInput Stream *)stream
with Name:(NSString *)stream Name
from Peer:(MCPeer ID *)peer ID
{
stream.delegate = self;
[stream schedule In Run Loop:[NSRun Loop main Run Loop]
for Mode:NSDefault Run Loop Mode];
[stream open];
}
Both the input and output streams must be scheduled and opened before they can be used. Once that’s done, streams can be read from and written to just like any other bound pair.
Resources
Resources are sent with send
:
NSURL *file URL = [NSURL file URLWith Path:@"path/to/resource"];
NSProgress *progress =
[self.session send Resource At URL:file URL
with Name:[file URL last Path Component]
to Peer:peer
with Completion Handler:^(NSError *error)
{
NSLog(@"[Error] %@", error);
}];
The returned NSProgress
object can be Key-Value Observed to monitor progress of the file transfer, as well as provide a cancellation handler, through the -cancel
method.
Receiving resources happens across two methods in MCSession
: -session:did
& -session:did
:
#pragma mark - MCSession Delegate
- (void)session:(MCSession *)session
did Start Receiving Resource With Name:(NSString *)resource Name
from Peer:(MCPeer ID *)peer ID
with Progress:(NSProgress *)progress
{
…
}
- (void)session:(MCSession *)session
did Finish Receiving Resource With Name:(NSString *)resource Name
from Peer:(MCPeer ID *)peer ID
at URL:(NSURL *)local URL
with Error:(NSError *)error
{
NSURL *destination URL = [NSURL file URLWith Path:@"/path/to/destination"];
NSError *error = nil;
if (![[NSFile Manager default Manager] move Item At URL:local URL
to URL:destination URL
error:&error]) {
NSLog(@"[Error] %@", error);
}
}
Again, the NSProgress
parameter in -session:did
allows the receiving peer to monitor the file transfer progress. In -session:did
, it is the responsibility of the delegate to move the file at the temporary local
to a permanent location.
Multipeer Connectivity is a ground-breaking API, whose value is only just starting to be fully understood. Although full support for features like AirDrop are currently limited to latest-gen devices, you should expect to see this kind of functionality become expected behavior.
As you look forward to the possibilities of the new year ahead, get your head out of the cloud, and start to consider the incredible possibilities around you.