Статьи

Создание игры с Bonjour — Настройка клиента и сервера

В первой части этой серии я писал об основах работы в сети. В этой статье мы начнем работать с Bonjour и библиотекой CocoaAsyncSocket, создав основу нашей игры.


В этой статье мы рассмотрим установление соединения между двумя устройствами, на которых запущен экземпляр игры, которую мы собираемся создать. Несмотря на то, что это может показаться тривиальным, для выполнения этой работы задействовано немало компонентов. Прежде чем мы запачкаем руки, позвольте мне шаг за шагом рассказать вам о процессе.

В предыдущей статье я писал о модели клиент-сервер. Прежде чем мы создадим саму игру, нам нужно внедрить модель клиент-сервер в приложение. Два устройства, на которых запущены разные экземпляры игры, не смогут волшебным образом найти друг друга в сети и запустить игру. Один экземпляр должен действовать как сервер и сообщать другим экземплярам в сети, что они могут присоединиться.

Обычный подход к решению этой проблемы — позволить игрокам принимать участие в игре или присоединяться к ней. Устройство игрока, на котором размещена игра, действует как сервер, а устройство игрока, присоединяющегося к игре, действует как клиент, подключающийся к серверу. Сервер предоставляет услугу, и он может сообщить об этом в сети, используя Bonjour. Устройство игрока, ищущего сеанс для присоединения, ищет в сети сервисы, также использующие Bonjour. Когда игрок присоединяется к игре, услуга разрешается, и клиентское устройство пытается установить соединение между двумя устройствами, чтобы игра могла начаться.

Если вас смущает модель клиент-сервер, то я рекомендую вернуться к первой части этой серии, в которой модель клиент-сервер описана более подробно.

Какую роль играет Bonjour в этой настройке? Bonjour отвечает за публикацию и поиск услуг в сети. Bonjour также используется для обнаружения принтеров или других устройств, подключенных к сети. Важно помнить, что Bonjour не несет ответственности за установление соединения между сервером и клиентом. Мы используем библиотеку CocoaAsyncSocket для выполнения этой задачи.

Библиотека CocoaAsyncSocket вступает в игру, когда нам приходится иметь дело с сокетами, портами и соединениями. Это также помогает нам отправлять данные с одного конца соединения на другой конец в обоих направлениях. Хотя Bonjour не несет ответственности за установление связи между двумя процессами, он предоставляет нам информацию, необходимую для установления связи. Как я упоминал ранее в этой серии, Bonjour и CocoaAsyncSocket — мощная комбинация, как вы увидите в этой статье.

Тестирование является ключевым аспектом разработки программного обеспечения, особенно когда речь идет о работе сети Чтобы протестировать сетевой компонент нашего приложения, вам необходимо запустить два его экземпляра. Вы можете сделать это, запустив один экземпляр на iOS Simulator и второй экземпляр на физическом устройстве. Один экземпляр будет служить сервером для размещения игры, тогда как другой экземпляр будет служить клиентом при поиске в сети игр для присоединения.


Откройте Xcode и создайте новый проект на основе шаблона приложения Single View (рисунок 1). Назовите проект « Четыре в ряд» , установите для « Устройства» значение « iPhone» и дважды проверьте, что ARC (автоматический подсчет ссылок) включен для проекта (рисунок 2). Мы не будем использовать раскадровки в этом уроке.

Создание игры с Bonjour - Клиент и Сервер - Настройка проекта
Настройка проекта
Создание игры с Bonjour - Клиент и Сервер - Настройка проекта
Настройка проекта

Добавить библиотеку CocoaAsyncSocket легко, если вы решите использовать CocoaPods, как я объяснил в предыдущем посте . Однако даже без CocoaPods добавление библиотеки CocoaAsyncSocket в ваш проект не является ракетостроением.

Загрузите последнюю версию библиотеки с GitHub и разверните архив. Найдите папку с именем GCD и перетащите и GCDAsyncSocket.h, и GCDAsyncSocket.m в свой проект Xcode. Обязательно скопируйте файлы в проект Xcode и добавьте их к цели « Четыре в ряд» (рисунок 3).

Создание игры с Bonjour - клиент и сервер - добавление библиотеки CocoaAsyncSocket
Добавление библиотеки CocoaAsyncSocket

Библиотека CocoaAsyncSocket зависит от фреймворков CFNetwork и Security , что означает, что нам нужно связать наш проект с обеими фреймворками. Откройте свой проект в Навигаторе проектов слева, выберите цель « Четыре в ряд» из списка целей, выберите вкладку « Фазы сборки » вверху и откройте панель « Связать двоичные файлы с библиотеками» . Нажмите кнопку «плюс» и свяжите ваш проект XCode с платформами CFNetwork и Security (рисунок 4)

Создание игры с Bonjour - клиент и сервер - связь проекта с сетями CFNetwork и безопасности
Связь проекта с сетью CFN и системами безопасности

Прежде чем продолжить, добавьте оператор импорта в предварительно скомпилированный заголовочный файл проекта, чтобы импортировать заголовочный файл, который мы только что добавили в наш проект 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.


Каждый раз, когда пользователь запускает наше приложение, у него есть два варианта на выбор: (1) хостинг игры или (2) присоединение к игре, которую ведет другой игрок. Интерфейс довольно прост, как вы можете себе представить. Откройте MTViewController.xib , добавьте две кнопки в представление контроллера представления и присвойте каждой кнопке соответствующий заголовок (рисунок 5).

Создание игры с Bonjour - Клиент и сервер - Создание пользовательского интерфейса
Создание пользовательского интерфейса

Добавьте два действия, hostGame: и joinGame: в файл реализации контроллера представления и соедините каждое действие с соответствующей кнопкой в MTViewController.xib .

1
2
3
— (IBAction)hostGame:(id)sender {
 
}
1
2
3
— (IBAction)joinGame:(id)sender {
 
}

Когда пользователь нажимает кнопку под названием « Хост игры» , он предоставляет пользователю модальное представление. Под капотом приложение будет публиковать сервис с использованием Bonjour и библиотеки CocoaAsyncSocket. Когда другой игрок присоединяется к игре, пользователь, принимающий игру, будет уведомлен, и игра может начаться. Создайте подкласс UIViewController и назовите его MTHostGameViewController (рисунок 6). Скажите Xcode также создать XIB-файл для нового класса контроллера представления (рисунок 6).

Создание игры с Bonjour - Клиент и сервер - Создание хост-контроллера Game View
Создание Host Game View Controller

Добавьте оператор импорта для нового контроллера представления в 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];
}

Откройте 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];
}

Когда пользователь открывает главный контроллер вида игры, контроллер представления должен автоматически опубликовать сервис, который могут обрабатывать другие экземпляры приложения в сети. Класс, который мы будем использовать для этой цели, — 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.


Нам не нужно реализовывать каждый метод протокола 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) , Чтобы убедиться, что все работает, важно принять входящие соединения для нашего приложения.

Создание игры с Bonjour - клиент и сервер - указание брандмауэру разрешить входящие соединения
Указание брандмауэру разрешить входящие соединения

Для второй части головоломки нам нужно создать еще один класс контроллера представления, точнее подкласс UITableViewController (рисунок 8). Назовите новый класс MTJoinGameViewController . Нет необходимости создавать файл XIB для нового класса.

Создание игры с Bonjour - клиент и сервер - создание контроллера присоединения к игре
Создание Присоединенного Контроллера Представления Игры

Следующие шаги похожи на то, что мы делали ранее. Реализация действия 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 делает поиск сетевых услуг очень простым, как вы можете видеть.

Создание игры с Bonjour - клиент и сервер - тестирование приложения
Тестирование приложения

Последним этапом процесса является установление соединения между обоими устройствами, когда игрок присоединяется к услуге (игре), нажав на службу в списке обнаруженных служб. Позвольте мне показать вам, как это работает на практике.

Когда пользователь нажимает на строку в табличном представлении, мы извлекаем соответствующую сетевую службу из массива служб и пытаемся разрешить службу. Это происходит в 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.


В этом уроке мы заложили основу нашей игры с точки зрения сетевого взаимодействия. Конечно, нам еще многое предстоит сделать, так как мы даже не начали реализовывать саму игру. Это то, что мы сделаем в следующей части этой серии. Будьте на связи.