В первой части этой серии я писал об основах работы в сети. В этой статье мы начнем работать с Bonjour и библиотекой CocoaAsyncSocket, создав основу нашей игры.
обзор
В этой статье мы рассмотрим установление соединения между двумя устройствами, на которых запущен экземпляр игры, которую мы собираемся создать. Несмотря на то, что это может показаться тривиальным, для выполнения этой работы задействовано немало компонентов. Прежде чем мы запачкаем руки, позвольте мне шаг за шагом рассказать вам о процессе.
В предыдущей статье я писал о модели клиент-сервер. Прежде чем мы создадим саму игру, нам нужно внедрить модель клиент-сервер в приложение. Два устройства, на которых запущены разные экземпляры игры, не смогут волшебным образом найти друг друга в сети и запустить игру. Один экземпляр должен действовать как сервер и сообщать другим экземплярам в сети, что они могут присоединиться.
Обычный подход к решению этой проблемы — позволить игрокам принимать участие в игре или присоединяться к ней. Устройство игрока, на котором размещена игра, действует как сервер, а устройство игрока, присоединяющегося к игре, действует как клиент, подключающийся к серверу. Сервер предоставляет услугу, и он может сообщить об этом в сети, используя Bonjour. Устройство игрока, ищущего сеанс для присоединения, ищет в сети сервисы, также использующие Bonjour. Когда игрок присоединяется к игре, услуга разрешается, и клиентское устройство пытается установить соединение между двумя устройствами, чтобы игра могла начаться.
Если вас смущает модель клиент-сервер, то я рекомендую вернуться к первой части этой серии, в которой модель клиент-сервер описана более подробно.
Bonjour
Какую роль играет Bonjour в этой настройке? Bonjour отвечает за публикацию и поиск услуг в сети. Bonjour также используется для обнаружения принтеров или других устройств, подключенных к сети. Важно помнить, что Bonjour не несет ответственности за установление соединения между сервером и клиентом. Мы используем библиотеку CocoaAsyncSocket для выполнения этой задачи.
CocoaAsyncSocket
Библиотека CocoaAsyncSocket вступает в игру, когда нам приходится иметь дело с сокетами, портами и соединениями. Это также помогает нам отправлять данные с одного конца соединения на другой конец в обоих направлениях. Хотя Bonjour не несет ответственности за установление связи между двумя процессами, он предоставляет нам информацию, необходимую для установления связи. Как я упоминал ранее в этой серии, Bonjour и CocoaAsyncSocket — мощная комбинация, как вы увидите в этой статье.
тестирование
Тестирование является ключевым аспектом разработки программного обеспечения, особенно когда речь идет о работе сети Чтобы протестировать сетевой компонент нашего приложения, вам необходимо запустить два его экземпляра. Вы можете сделать это, запустив один экземпляр на iOS Simulator и второй экземпляр на физическом устройстве. Один экземпляр будет служить сервером для размещения игры, тогда как другой экземпляр будет служить клиентом при поиске в сети игр для присоединения.
1 Настройка проекта
Откройте Xcode и создайте новый проект на основе шаблона приложения Single View (рисунок 1). Назовите проект « Четыре в ряд» , установите для « Устройства» значение « iPhone» и дважды проверьте, что ARC (автоматический подсчет ссылок) включен для проекта (рисунок 2). Мы не будем использовать раскадровки в этом уроке.
2 Добавление CocoaAsyncSocket
Добавить библиотеку CocoaAsyncSocket легко, если вы решите использовать CocoaPods, как я объяснил в предыдущем посте . Однако даже без CocoaPods добавление библиотеки CocoaAsyncSocket в ваш проект не является ракетостроением.
Шаг 1
Загрузите последнюю версию библиотеки с GitHub и разверните архив. Найдите папку с именем GCD и перетащите и GCDAsyncSocket.h, и GCDAsyncSocket.m в свой проект Xcode. Обязательно скопируйте файлы в проект Xcode и добавьте их к цели « Четыре в ряд» (рисунок 3).
Шаг 2
Библиотека CocoaAsyncSocket зависит от фреймворков CFNetwork и Security , что означает, что нам нужно связать наш проект с обеими фреймворками. Откройте свой проект в Навигаторе проектов слева, выберите цель « Четыре в ряд» из списка целей, выберите вкладку « Фазы сборки » вверху и откройте панель « Связать двоичные файлы с библиотеками» . Нажмите кнопку «плюс» и свяжите ваш проект XCode с платформами CFNetwork и Security (рисунок 4)
Шаг 3
Прежде чем продолжить, добавьте оператор импорта в предварительно скомпилированный заголовочный файл проекта, чтобы импортировать заголовочный файл, который мы только что добавили в наш проект Xcode. Это гарантирует, что мы можем использовать класс GCDAsyncSocket
в нашем проекте.
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <Availability.h>
#ifndef __IPHONE_4_0
#warning «This project uses features only available in iOS SDK 4.0 and later.»
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import «GCDAsyncSocket.h»
#endif
|
Возможно, вы заметили, что мы не добавили GCDAsyncUdpSocket.h и GCDAsyncUdpSocket.m в наш проект. Как показывают имена файлов, эти файлы объявляют и реализуют класс GCDAsyncUdpSocket
, который отвечает за работу с протоколом UDP. Хотя в этой серии мы будем работать только с протоколом TCP, имейте в виду, что библиотека CocoaAsyncSocket также поддерживает протокол UDP.
3 Создание Host Game View Controller
Шаг 1
Каждый раз, когда пользователь запускает наше приложение, у него есть два варианта на выбор: (1) хостинг игры или (2) присоединение к игре, которую ведет другой игрок. Интерфейс довольно прост, как вы можете себе представить. Откройте MTViewController.xib , добавьте две кнопки в представление контроллера представления и присвойте каждой кнопке соответствующий заголовок (рисунок 5).
Шаг 2
Добавьте два действия, hostGame:
и joinGame:
в файл реализации контроллера представления и соедините каждое действие с соответствующей кнопкой в MTViewController.xib .
1
2
3
|
— (IBAction)hostGame:(id)sender {
}
|
1
2
3
|
— (IBAction)joinGame:(id)sender {
}
|
Шаг 3
Когда пользователь нажимает кнопку под названием « Хост игры» , он предоставляет пользователю модальное представление. Под капотом приложение будет публиковать сервис с использованием Bonjour и библиотеки CocoaAsyncSocket. Когда другой игрок присоединяется к игре, пользователь, принимающий игру, будет уведомлен, и игра может начаться. Создайте подкласс UIViewController
и назовите его MTHostGameViewController
(рисунок 6). Скажите Xcode также создать XIB-файл для нового класса контроллера представления (рисунок 6).
Шаг 4
Добавьте оператор импорта для нового контроллера представления в MTViewController.m и реализуйте действие hostGame:
как показано ниже. Когда пользователь нажимает верхнюю кнопку, создается экземпляр класса MTHostGameViewController
, устанавливается в качестве корневого контроллера представления контроллера навигации и представляется модально.
1
|
#import «MTHostGameViewController.h»
|
01
02
03
04
05
06
07
08
09
10
|
— (IBAction)hostGame:(id)sender {
// Initialize Host Game View Controller
MTHostGameViewController *vc = [[MTHostGameViewController alloc] initWithNibName:@»MTHostGameViewController» bundle:[NSBundle mainBundle]];
// Initialize Navigation Controller
UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:vc];
// Present Navigation Controller
[self presentViewController:nc animated:YES completion:nil];
}
|
Шаг 5
Откройте MTHostGameViewController.m и viewDidLoad
метод viewDidLoad
как показано ниже. Все, что мы делаем, это вызываем setupView
, вспомогательный метод. В методе setupView
мы добавляем кнопку на панель навигации, чтобы позволить пользователю отменить хостинг игры и закрыть контроллер представления. Как показывает реализация действия cancel:
отмена хостинга игры — это то, что мы будем реализовывать позже в этом руководстве. Создайте и запустите приложение в первый раз, чтобы увидеть, все ли работает как положено.
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
// Setup View
[self setupView];
}
|
1
2
3
4
|
— (void)setupView {
// Create Cancel Button
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)];
}
|
1
2
3
4
5
6
7
|
— (void)cancel:(id)sender {
// Cancel Hosting Game
// TODO
// Dismiss View Controller
[self dismissViewControllerAnimated:YES completion:nil];
}
|
4 Издательство Сервиса
Когда пользователь открывает главный контроллер вида игры, контроллер представления должен автоматически опубликовать сервис, который могут обрабатывать другие экземпляры приложения в сети. Класс, который мы будем использовать для этой цели, — NSNetService
. Как говорится в документации NSNetService
, экземпляр класса NSNetService
представляет сетевой сервис. Помните, что сервис не ограничен процессом или приложением. Принтеры или другие устройства, подключенные к сети, также могут сообщать о своих услугах с помощью Bonjour. Именно эта универсальность делает Bonjour отличным выбором для потребителей.
Вместо того, чтобы заваливать вас большим количеством теории, позвольте мне показать вам, что нужно для публикации службы с использованием Bonjour и библиотеки CocoaAsyncSocket. viewDidLoad
метод viewDidLoad
как показано ниже.
1
2
3
4
5
6
7
8
9
|
— (void)viewDidLoad {
[super viewDidLoad];
// Setup View
[self setupView];
// Start Broadcast
[self startBroadcast];
}
|
Прежде чем мы взглянем на метод startBroadcast
, нам нужно немного поработать. Добавьте расширение класса вверху файла реализации контроллера вида игры хоста и объявите два частных свойства, service
, типа NSNetService
и socket
, типа GCDAsyncSocket
. В расширении класса мы также NSNetServiceDelegate
контроллер вида игры хоста к протоколам NSNetServiceDelegate
и GCDAsyncSocketDelegate
.
1
2
3
4
5
6
|
@interface MTHostGameViewController () <NSNetServiceDelegate, GCDAsyncSocketDelegate>
@property (strong, nonatomic) NSNetService *service;
@property (strong, nonatomic) GCDAsyncSocket *socket;
@end
|
Свойство service
представляет сетевой сервис, который мы будем публиковать с использованием Bonjour. Свойство socket
имеет тип GCDAsyncSocket
и предоставляет интерфейс для взаимодействия с сокетом, который мы будем использовать для прослушивания входящих соединений. Имея расширение класса, давайте посмотрим на реализацию метода startBroadcast
.
Мы инициализируем экземпляр класса GCDAsyncSocket
и передаем контроллер представления в качестве делегата сокета. Вторым аргументом инициализатора является очередь GCD (Grand Central Dispatch), в этом примере очередь отправки основного потока приложения. Библиотека CocoaAsyncSocket имеет архитектуру с очередями, что делает ее чрезвычайно гибкой и мощной. Хотя интеграция с GCD является важным дополнением к библиотеке CocoaAsyncSocket, я не буду освещать эту интеграцию в этой серии.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (void)startBroadcast {
// Initialize GCDAsyncSocket
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
// Start Listening for Incoming Connections
NSError *error = nil;
if ([self.socket acceptOnPort:0 error:&error]) {
// Initialize Service
self.service = [[NSNetService alloc] initWithDomain:@»local.»
// Configure Service
[self.service setDelegate:self];
// Publish Service
[self.service publish];
} else {
NSLog(@»Unable to create socket. Error %@ with user info %@.», error, [error userInfo]);
}
}
|
Второй шаг — сообщить сокету о принятии входящих соединений, отправив ему сообщение acceptOnPort:error:
Мы передаем 0
в качестве номера порта, что означает, что операционная система должна предоставить нам доступный порт (номер). Как правило, это самое безопасное решение, так как мы не всегда знаем, используется ли конкретный порт или нет. Позволяя системе выбрать порт от нашего имени, мы можем быть уверены, что порт (номер), который мы получаем, доступен. Если вызов успешен, то есть, возвращая YES
и не выдавая ошибку, мы можем инициализировать сетевой сервис.
Порядок, в котором все это происходит, важен. Сетевой сервис, который мы инициализируем, должен знать номер порта для прослушивания входящих соединений. Чтобы инициализировать сетевой сервис, мы передаем (1) домен , который всегда является локальным. для локального домена: (2) тип сетевого сервиса, который представляет собой строку, которая однозначно идентифицирует сетевой сервис (не экземпляр нашего приложения), (3) имя, по которому сетевой сервис идентифицирован в сети, и ( 4) порт, на котором опубликован сетевой сервис.
Тип, который мы передаем в качестве второго аргумента initWithDomain:type:name:port:
необходимо указать как тип службы, так и протокол транспортного уровня (в данном примере TCP). Подчеркивание префикса типа сервиса и протокола транспортного уровня также важно. Подробности можно найти в документации .
Когда сетевой сервис готов к использованию, мы устанавливаем делегата сервиса и публикуем его. Публикация сервиса просто означает рекламу сетевого сервиса в сети, чтобы клиенты могли его обнаружить.
Если бы мы настроили все это с использованием CFNetwork вместо библиотеки CocoaAsyncSocket , нам пришлось бы написать довольно сложный C-код. Как видите, библиотека CocoaAsyncSocket значительно упрощает этот процесс, предоставляя удобный Objective-C API.
5 Отвечая на события
Нам не нужно реализовывать каждый метод протокола NSNetServiceDelegate
. Наиболее важные из них перечислены ниже. В этих методах делегатов мы просто записываем сообщение на консоль, чтобы убедиться, что мы можем отслеживать происходящее.
1
2
3
|
— (void)netServiceDidPublish:(NSNetService *)service {
NSLog(@»Bonjour Service Published: domain(%@) type(%@) name(%@) port(%i)», [service domain], [service type], [service name], (int)[service port]);
}
|
1
2
3
|
— (void)netService:(NSNetService *)service didNotPublish:(NSDictionary *)errorDict {
NSLog(@»Failed to Publish Service: domain(%@) type(%@) name(%@) — %@», [service domain], [service type], [service name], errorDict);
}
|
Протокол GCDAsyncSocketDelegate
имеет довольно много методов делегатов, как вы можете видеть в документации . Чтобы не перегружать вас, мы будем реализовывать по одному методу за раз. Первый интересующий нас метод — это socket:didAcceptNewSocket:
который вызывается, когда прослушивающий сокет (сокет сервера) принимает соединение (соединение клиента). Поскольку в нашем приложении одновременно разрешено только одно соединение, мы отбрасываем старый (слушающий) сокет и сохраняем ссылку на новый сокет в свойстве socket
. Чтобы иметь возможность использовать входящее клиентское соединение, важно сохранить ссылку на новый сокет, который передается нам в этом методе делегата.
Как указано в документации, очередь делегатов и делегатов нового сокета совпадают с очередью делегатов и делегатов старого сокета. Многие люди часто забывают, что нам нужно указать новому сокету, чтобы он начал читать данные, и установить время ожидания -1
(без времени ожидания). За кулисами библиотека CocoaAsyncSocket создает для нас поток чтения и записи, но мы должны сказать сокету контролировать поток чтения для входящих данных.
1
2
3
4
5
6
7
8
9
|
— (void)socket:(GCDAsyncSocket *)socket didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
NSLog(@»Accepted New Socket from %@:%hu», [newSocket connectedHost], [newSocket connectedPort]);
// Socket
[self setSocket:newSocket];
// Read Data from Socket
[newSocket readDataToLength:sizeof(uint64_t) withTimeout:-1.0 tag:0];
}
|
Второй метод делегата, который мы можем реализовать в настоящее время, — это метод, который вызывается при разрыве соединения. Все, что мы делаем в этом методе, это убираем вещи.
1
2
3
4
5
6
7
8
|
— (void)socketDidDisconnect:(GCDAsyncSocket *)socket withError:(NSError *)error {
NSLog(@»%s», __PRETTY_FUNCTION__);
if (self.socket == socket) {
[self.socket setDelegate:nil];
[self setSocket:nil];
}
}
|
Поскольку время от времени работа в сети может быть немного беспорядочной, вы могли заметить, что я вставил несколько операторов журнала здесь и там. Ведение журнала — ваш лучший друг при создании приложений, связанных с сетью. Не стесняйтесь добавить несколько записей журнала, если вы не уверены, что происходит под капотом.
Создайте и запустите приложение в iOS Simulator (или на физическом устройстве) и нажмите верхнюю кнопку, чтобы разместить игру. Если все прошло хорошо, вы должны увидеть сообщение в консоли XCode, которое указывает, что сетевая служба была успешно опубликована. Насколько это было легко? На следующем шаге мы создадим второй кусок головоломки, обнаружив и подключившись к сетевому сервису.
1
|
2013-04-10 11:44:03.286 Four in a Row[3771:c07] Bonjour Service Published: domain(local.) type(_fourinarow._tcp.) name(Puma) port(51803)
|
Если вы запускаете приложение в iOS Simulator и у вас включен брандмауэр на Mac, вы должны увидеть предупреждение операционной системы, запрашивающее ваше разрешение на входящие соединения для приложения, запущенного в iOS Simulator (рисунок 7) , Чтобы убедиться, что все работает, важно принять входящие соединения для нашего приложения.
6 Обнаружение Услуг
Для второй части головоломки нам нужно создать еще один класс контроллера представления, точнее подкласс UITableViewController
(рисунок 8). Назовите новый класс MTJoinGameViewController
. Нет необходимости создавать файл XIB для нового класса.
Следующие шаги похожи на то, что мы делали ранее. Реализация действия joinGame:
практически идентична hostGame:
Не забудьте добавить оператор импорта для класса MTJoinGameViewController
.
1
|
#import «MTJoinGameViewController.h»
|
01
02
03
04
05
06
07
08
09
10
|
— (IBAction)joinGame:(id)sender {
// Initialize Join Game View Controller
MTJoinGameViewController *vc = [[MTJoinGameViewController alloc] initWithStyle:UITableViewStylePlain];
// Initialize Navigation Controller
UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:vc];
// Present Navigation Controller
[self presentViewController:nc animated:YES completion:nil];
}
|
Как и в классе MTHostGameViewController
, мы вызываем setupView
в методе viewDidLoad
контроллера представления.
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
// Setup View
[self setupView];
}
|
1
2
3
4
|
— (void)setupView {
// Create Cancel Button
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)];
}
|
1
2
3
4
5
6
7
|
— (void)cancel:(id)sender {
// Stop Browsing Services
[self stopBrowsing];
// Dismiss View Controller
[self dismissViewControllerAnimated:YES completion:nil];
}
|
Единственное отличие состоит в том, что мы прекращаем поиск сервисов в действии cancel:
до закрытия контроллера представления. Мы реализуем методы stopBrowsing
в stopBrowsing
время.
Услуги просмотра
Для поиска сервисов в локальной сети мы используем класс NSNetServiceBrowser
. Прежде чем NSNetServiceBrowser
класс NSNetServiceBrowser
, нам нужно создать несколько закрытых свойств. Добавьте расширение класса в класс MTJoinGameViewController
и объявите три свойства, как показано ниже. Первое свойство, тип socket
GCDAsyncSocket
, будет хранить ссылку на сокет, который будет создан при успешном GCDAsyncSocket
сетевой службы. Свойство services
( NSMutableArray
) будет хранить все службы, которые браузер служб обнаруживает в сети. Каждый раз, когда браузер службы находит новый сервис, он уведомляет нас, и мы можем добавить его в этот изменяемый массив. Этот массив также будет служить источником данных табличного представления контроллера представления. Третье свойство, serviceBrowser
, относится к типу NSNetServiceBrowser
и будет выполнять поиск в сети сетевых служб, которые представляют для нас интерес. Также обратите внимание, что MTJoinGameViewController
соответствует трем протоколам. Это станет ясно, когда мы реализуем методы каждого из этих протоколов.
1
2
3
4
5
6
7
|
@interface MTJoinGameViewController () <NSNetServiceDelegate, NSNetServiceBrowserDelegate, GCDAsyncSocketDelegate>
@property (strong, nonatomic) GCDAsyncSocket *socket;
@property (strong, nonatomic) NSMutableArray *services;
@property (strong, nonatomic) NSNetServiceBrowser *serviceBrowser;
@end
|
viewDidLoad
метод viewDidLoad
контроллера представления, как показано ниже. Мы вызываем другой вспомогательный метод, в котором мы инициализируем браузер службы, чтобы приложение могло начать поиск в сети интересующих нас служб. Давайте рассмотрим метод startBrowsing
.
1
2
3
4
5
6
7
8
9
|
— (void)viewDidLoad {
[super viewDidLoad];
// Setup View
[self setupView];
// Start Browsing
[self startBrowsing];
}
|
В методе startBrowsing
мы подготавливаем источник данных табличного представления — изменяемый массив, в котором хранятся службы, обнаруженные в сети. Мы также инициализируем экземпляр класса NSNetServiceBrowser
, устанавливаем его делегат и говорим ему начать поиск сервисов. Ключевым моментом является то, что тип сетевого сервиса, который вы передаете в качестве первого аргумента searchForServicesOfType:inDomain:
идентичен типу, который мы передали в классе MTHostGameViewController
. Это хорошая идея, чтобы сделать это константой, чтобы предотвратить любые опечатки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
— (void)startBrowsing {
if (self.services) {
[self.services removeAllObjects];
} else {
self.services = [[NSMutableArray alloc] init];
}
// Initialize Service Browser
self.serviceBrowser = [[NSNetServiceBrowser alloc] init];
// Configure Service Browser
[self.serviceBrowser setDelegate:self];
[self.serviceBrowser searchForServicesOfType:@»_fourinarow._tcp.»
}
|
При вызове searchForServicesOfType:inDomain:
браузер служб начинает поиск в сети служб указанного типа. Каждый раз, когда браузер службы находит интересующую службу, он уведомляет своего делегата, отправляя ему сообщение netServiceBrowser:didFindService:moreComing:
Обнаруженная сетевая служба ( NSNetService
) передается как второй параметр, и мы добавляем этот объект в наш массив служб. Последний параметр этого метода делегата, moreComing
, сообщает нам, можем ли мы ожидать больше сервисов. Этот флаг полезен, если вы не хотите преждевременно обновлять пользовательский интерфейс вашего приложения, например обновлять табличное представление. Если для флага moreComing
установлено значение NO
, мы сортируем массив служб и обновляем представление таблицы.
01
02
03
04
05
06
07
08
09
10
11
12
|
— (void)netServiceBrowser:(NSNetServiceBrowser *)serviceBrowser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing {
// Update Services
[self.services addObject:service];
if(!moreComing) {
// Sort Services
[self.services sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@»name» ascending:YES]]];
// Update Table View
[self.tableView reloadData];
}
}
|
Также возможно, что ранее обнаруженная служба по какой-то причине остановилась и больше не доступна. Когда это происходит, netServiceBrowser:didRemoveService:moreComing:
метод netServiceBrowser:didRemoveService:moreComing:
делегат. Он работает во многом так же, как и предыдущий метод делегата. Вместо добавления сетевого сервиса, который передается в качестве второго аргумента, мы удаляем его из массива сервисов и соответственно обновляем табличное представление.
1
2
3
4
5
6
7
8
9
|
— (void)netServiceBrowser:(NSNetServiceBrowser *)serviceBrowser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing {
// Update Services
[self.services removeObject:service];
if(!moreComing) {
// Update Table View
[self.tableView reloadData];
}
}
|
Два других метода делегата представляют для нас интерес. Когда браузер службы прекращает поиск или не может начать поиск, netServiceBrowserDidStopSearch:
и netServiceBrowser:didNotSearch:
делегат, соответственно. Все, что мы делаем в этих методах, это stopBrowsing
то, что мы начали в вспомогательном методе stopBrowsing
как показано ниже.
1
2
3
|
— (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)serviceBrowser {
[self stopBrowsing];
}
|
1
2
3
|
— (void)netServiceBrowser:(NSNetServiceBrowser *)aBrowser didNotSearch:(NSDictionary *)userInfo {
[self stopBrowsing];
}
|
Реализация stopBrowsing
не сложно. Все, что мы делаем, — инструктируем сервисный браузер, чтобы он прекратил просмотр и все очистил.
1
2
3
4
5
6
7
|
— (void)stopBrowsing {
if (self.serviceBrowser) {
[self.serviceBrowser stop];
[self.serviceBrowser setDelegate:nil];
[self setServiceBrowser:nil];
}
}
|
Прежде чем мы продолжим наше путешествие, давайте посмотрим на все это в действии. Однако сначала необходимо реализовать протокол источника данных табличного представления, как показано ниже, чтобы убедиться, что табличное представление заполнено службами, которые браузер служб находит в сети. Как вы можете видеть в tableView:cellForRowAtIndexPath:
метод, мы отображаем свойство name
сетевой службы, которое является именем, которое мы передали в MTHostGameViewController
когда мы инициализировали сетевую службу. Поскольку мы не передали имя, оно автоматически использует имя устройства.
1
2
3
|
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.services ?
}
|
1
2
3
|
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.services count];
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ServiceCell];
if (!cell) {
// Initialize Table View Cell
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ServiceCell];
}
// Fetch Service
NSNetService *service = [self.services objectAtIndex:[indexPath row]];
// Configure Cell
[cell.textLabel setText:[service name]];
return cell;
}
|
Не забудьте статически объявить идентификатор повторного использования ячейки, который мы используем в tableView:cellForRowAtIndexPath:
table view data source.
1
|
static NSString *ServiceCell = @»ServiceCell»;
|
Чтобы проверить, что мы создали, вам нужно запустить два экземпляра приложения. В одном случае вы нажимаете кнопку « Развернуть игру» , а в другом — кнопку « Присоединиться к игре» . В последнем случае вы должны увидеть список со всеми экземплярами, которые публикуют интересующий нас сервис (рисунок 9). Bonjour делает поиск сетевых услуг очень простым, как вы можете видеть.
7 Создание связи
Последним этапом процесса является установление соединения между обоими устройствами, когда игрок присоединяется к услуге (игре), нажав на службу в списке обнаруженных служб. Позвольте мне показать вам, как это работает на практике.
Когда пользователь нажимает на строку в табличном представлении, мы извлекаем соответствующую сетевую службу из массива служб и пытаемся разрешить службу. Это происходит в tableView:didSelectRowAtIndexPath:
метод делегата табличного представления. resolveWithTimeout:
метод принимает один аргумент, максимальное количество секунд для разрешения службы.
01
02
03
04
05
06
07
08
09
10
|
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// Fetch Service
NSNetService *service = [self.services objectAtIndex:[indexPath row]];
// Resolve Service
[service setDelegate:self];
[service resolveWithTimeout:30.0];
}
|
Произойдет одно из двух: успех или неудача. Если разрешение службы не удалось, netService:didNotResolve:
метод netService:didNotResolve:
делегат. Все, что мы делаем в этом методе, это убираем все.
1
2
3
|
— (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorDict {
[service setDelegate:nil];
}
|
Если служба разрешается успешно, мы пытаемся создать соединение, вызывая connectWithService:
и передавая службу в качестве аргумента. Помните, что Bonjour не несет ответственности за создание соединения. Bonjour предоставляет нам только информацию, необходимую для установления связи. Разрешение службы — это не то же самое, что создание соединения. Что нужно сделать, чтобы установить соединение, можно узнать из реализации connectWithService:
1
2
3
4
5
6
7
8
|
— (void)netServiceDidResolveAddress:(NSNetService *)service {
// Connect With Service
if ([self connectWithService:service]) {
NSLog(@»Did Connect with Service: domain(%@) type(%@) name(%@) port(%i)», [service domain], [service type], [service name], (int)[service port]);
} else {
NSLog(@»Unable to Connect with Service: domain(%@) type(%@) name(%@) port(%i)», [service domain], [service type], [service name], (int)[service port]);
}
}
|
Сначала мы создаем вспомогательную переменную _isConnected
и изменяемую копию addresses
сетевого сервиса. Вы можете быть удивлены тем, что служба может иметь несколько адресов, но это действительно может иметь место в некоторых ситуациях. Каждый элемент массива адресов является экземпляром NSData
содержащим структуру sockaddr
. Если свойство socket
еще не было инициализировано (соединение не активно), мы создаем новый сокет, как мы это делали в классе MTHostGameViewcontroller
. Затем мы перебираем массив адресов и пытаемся подключиться к одному из адресов в массиве адресов. Если это успешно, мы устанавливаем _isConnected
в YES
. Это означает, что соединение было успешно установлено.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
— (BOOL)connectWithService:(NSNetService *)service {
BOOL _isConnected = NO;
// Copy Service Addresses
NSArray *addresses = [[service addresses] mutableCopy];
if (!self.socket || ![self.socket isConnected]) {
// Initialize Socket
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
// Connect
while (!_isConnected && [addresses count]) {
NSData *address = [addresses objectAtIndex:0];
NSError *error = nil;
if ([self.socket connectToAddress:address error:&error]) {
_isConnected = YES;
} else if (error) {
NSLog(@»Unable to connect to address. Error %@ with user info %@.», error, [error userInfo]);
}
}
} else {
_isConnected = [self.socket isConnected];
}
return _isConnected;
}
|
Когда сокет успешно установил соединение, socket:didConnectToHost:
метод socket:didConnectToHost:
делегат протокола GCDAsyncSocketDelegate
. Помимо записи некоторой информации на консоль, мы сообщаем сокету начать мониторинг потока чтения для входящих данных. Я расскажу о чтении и письме более подробно в следующей статье этой серии.
1
2
3
4
5
6
|
— (void)socket:(GCDAsyncSocket *)socket didConnectToHost:(NSString *)host port:(UInt16)port {
NSLog(@»Socket Did Connect to Host: %@ Port: %hu», host, port);
// Start Reading
[socket readDataToLength:sizeof(uint64_t) withTimeout:-1.0 tag:0];
}
|
Это также хорошее время для реализации другого метода протокола GCDAsyncSocketDelegate
. socketDidDisconnect:withError:
метод вызывается при разрыве соединения. Все, что мы делаем, это регистрируем некоторую информацию о сокете на консоли и очищаем все.
1
2
3
4
5
6
|
— (void)socketDidDisconnect:(GCDAsyncSocket *)socket withError:(NSError *)error {
NSLog(@»Socket Did Disconnect with Error %@ with User Info %@.», error, [error userInfo]);
[socket setDelegate:nil];
[self setSocket:nil];
}
|
Запустите два экземпляра приложения, чтобы убедиться, что все работает. Важно, чтобы на устройстве, на котором размещена игра, socket:didAcceptNewSocket:
метод socket:didAcceptNewSocket:
делегат, а на устройстве, присоединяющемся к игре, socket:didConnectToHost:
метод socket:didConnectToHost:
делегат. Если вызываются оба метода делегата, мы знаем, что мы успешно создали соединение между обоими устройствами.
Незаконченные дела
Есть еще несколько слабых сторон, о которых мы должны позаботиться, но это то, что мы сделаем в следующей части этой серии. Надеюсь, вы согласитесь, что начать работу с Bonjour и библиотекой CocoaAsyncSocket не так уж и сложно. Я хочу подчеркнуть, что библиотека CocoaAsyncSocket невероятно упрощает работу с сокетами и потоками, предоставляя Objective-C API и следя за тем, чтобы нам не приходилось самим использовать инфраструктуру CFNetwork.
Вывод
В этом уроке мы заложили основу нашей игры с точки зрения сетевого взаимодействия. Конечно, нам еще многое предстоит сделать, так как мы даже не начали реализовывать саму игру. Это то, что мы сделаем в следующей части этой серии. Будьте на связи.