Статьи

Создание игры с Bonjour: отправка данных

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


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

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

Это замечательно, но как получатель узнает, какой длины заголовок? Каждое поле заголовка HTTP заканчивается CRLF (возвращение C, L ine Feed), а сам заголовок HTTP также заканчивается CRLF . Это означает, что заголовок каждого HTTP-запроса и ответа заканчивается двумя CRLF . Когда получатель читает входящие данные из потока чтения, он должен только искать этот шаблон (два последовательных CRLF ) в потоке чтения. Тем самым получатель может идентифицировать и извлечь заголовок HTTP-запроса или ответа. С извлеченным заголовком извлечь тело HTTP-запроса или ответа довольно просто.

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


Важно понимать, что вышеуказанные стратегии адаптированы к протоколу TCP и работают только из-за того, как работает протокол TCP. Протокол TCP делает все возможное, чтобы каждый пакет достиг пункта назначения в том порядке, в котором он был отправлен; Таким образом, изложенные мною стратегии работают очень хорошо.

Несмотря на то, что мы можем отправлять данные любого типа через TCP-соединение, рекомендуется предоставить пользовательскую структуру для хранения данных, которые мы хотели бы отправить. Мы можем сделать это, создав собственный класс пакета. Преимущество этого подхода становится очевидным, как только мы начнем использовать класс пакета. Идея проста, хотя. Класс представляет собой класс Objective-C, который содержит данные; тело, если хотите. Он также включает в себя некоторую дополнительную информацию о пакете, называемую заголовком. Основное отличие от протокола HTTP заключается в том, что заголовок и тело не разделены строго. Класс пакета также должен соответствовать протоколу NSCoding , что означает, что экземпляры класса могут быть закодированы и декодированы. Это важно, если мы хотим отправлять экземпляры класса пакета через TCP-соединение.

Создайте новый класс Objective C, сделайте его подклассом NSObject и назовите его MTPacket (рисунок 1). Для создаваемой игры класс пакетов может быть довольно простым. Класс имеет три свойства: type , action и data . Свойство type используется для определения цели пакета, в то время как свойство action содержит намерение пакета. Свойство data используется для хранения фактического содержимого или загрузки пакета. Все станет яснее, как только мы начнем использовать класс пакетов в нашей игре.

Создание игры с Bonjour - Отправка данных - Создание класса пакета
Создание класса пакета

Найдите минутку, чтобы проверить интерфейс класса MTPacket показанный ниже. Как я уже упоминал, важно, чтобы экземпляры класса могли быть закодированы и декодированы в соответствии с протоколом NSCoding . Чтобы соответствовать протоколу NSCoding , нам нужно реализовать только два (обязательных) метода encodeWithCoder: и initWithCoder:

Еще одна важная деталь: свойства type и action имеют тип MTPacketType и MTPacketAction соответственно. Вы можете найти определения типов в верхней части MTPacket.h . Если вы не знакомы с typedef и enum , вы можете прочитать больше об этом в Stack Overflow . Это значительно облегчит работу с классом MTPacket .

Свойство data класса имеет тип id . Это означает, что это может быть любой объект Objective-C. Единственное требование — это соответствие протоколу NSCoding . Большинство членов платформы Foundation, таких как NSArray , NSDictionary и NSNumber , соответствуют протоколу NSCoding .

Чтобы упростить инициализацию экземпляров класса MTPacket , мы объявляем назначенный инициализатор, который принимает данные, тип и действие пакета в качестве аргументов.

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
#import <Foundation/Foundation.h>
 
extern NSString * const MTPacketKeyData;
extern NSString * const MTPacketKeyType;
extern NSString * const MTPacketKeyAction;
 
typedef enum {
    MTPacketTypeUnknown = -1
} MTPacketType;
 
typedef enum {
    MTPacketActionUnknown = -1
} MTPacketAction;
 
@interface MTPacket : NSObject
 
@property (strong, nonatomic) id data;
@property (assign, nonatomic) MTPacketType type;
@property (assign, nonatomic) MTPacketAction action;
 
#pragma mark —
#pragma mark Initialization
— (id)initWithData:(id)data type:(MTPacketType)type action:(MTPacketAction)action;
 
@end

Реализация класса MTPacket не должна быть слишком сложной, если вы знакомы с протоколом NSCoding . Как мы видели ранее, протокол NSCoding определяет два метода, и оба требуются. Они автоматически вызываются, когда экземпляр класса кодируется ( encodeWithCoder: или декодируется ( initWithCoder: encodeWithCoder: . Другими словами, вам никогда не придется вызывать эти методы самостоятельно. Мы увидим, как это работает чуть позже в этой статье.

Как вы можете видеть ниже, реализация назначенного инициализатора initWithData:type:action: не может быть проще. В файле реализации также становится понятно, почему мы объявили три строковые константы в интерфейсе класса. Рекомендуется использовать константы для ключей, которые вы используете в протоколе NSCoding . Основная причина не в производительности, а в ошибках ввода. Ключи, которые вы передаете при кодировании свойств класса, должны быть идентичны ключам, которые используются при декодировании экземпляров класса.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#import «MTPacket.h»
 
NSString * const MTPacketKeyData = @»data»;
NSString * const MTPacketKeyType = @»type»;
NSString * const MTPacketKeyAction = @»action»;
 
@implementation MTPacket
 
#pragma mark —
#pragma mark Initialization
— (id)initWithData:(id)data type:(MTPacketType)type action:(MTPacketAction)action {
    self = [super init];
 
    if (self) {
        self.data = data;
        self.type = type;
        self.action = action;
    }
 
    return self;
}
 
#pragma mark —
#pragma mark NSCoding Protocol
— (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.data forKey:MTPacketKeyData];
    [coder encodeInteger:self.type forKey:MTPacketKeyType];
    [coder encodeInteger:self.action forKey:MTPacketKeyAction];
}
 
— (id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
 
    if (self) {
        [self setData:[decoder decodeObjectForKey:MTPacketKeyData]];
        [self setType:[decoder decodeIntegerForKey:MTPacketKeyType]];
        [self setAction:[decoder decodeIntegerForKey:MTPacketKeyAction]];
    }
 
    return self;
}
 
@end

Прежде чем перейти к следующему фрагменту головоломки, я хочу убедиться, что класс MTPacket работает так, как ожидалось. Какой лучший способ проверить это, чем отправка пакета, как только соединение установлено? Когда это сработает, мы можем начать рефакторинг сетевой логики, поместив ее в выделенный контроллер.

Когда соединение установлено, экземпляр приложения, в котором размещена игра, уведомляется об этом путем вызова метода socket:didAcceptNewSocket: делегат протокола GCDAsyncSocketDelegate . Мы реализовали этот метод в предыдущей статье . Посмотрите на его реализацию ниже, чтобы освежить вашу память. Последняя строка его реализации теперь должна быть понятной. Мы сообщаем новому сокету начать чтение данных и передаем тег целое число в качестве последнего параметра. Мы не устанавливаем тайм-аут ( -1 ), потому что мы не знаем, когда можно ожидать поступления первого пакета.

Однако нас действительно интересует первый аргумент readDataToLength:withTimeout:tag: Почему мы передаем sizeof(uint64_t) в качестве первого аргумента?

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];
}

Функция sizeof возвращает длину в байтах аргумента функции, uint64_t , который определен в stdint.h (см. Ниже). Как я объяснил ранее, заголовок, который предшествует каждому отправляемому пакету, имеет фиксированную длину (рисунок 2), которая сильно отличается от заголовка HTTP-запроса или ответа. В нашем примере заголовок имеет только одну цель, сообщая получателю размер пакета, которому он предшествует. Другими словами, сообщая сокету, что нужно читать входящие данные, размер заголовка ( sizeof(uint64_t) ), мы знаем, что мы прочтем полный заголовок. Анализируя заголовок, как только он был извлечен из входящего потока данных, получатель знает размер тела, следующего за заголовком.

1
typedef unsigned long long uint64_t;
Создание игр с Bonjour - Отправка данных - Использование заголовка с фиксированной длиной
Использование заголовка с фиксированной длиной

Импортируйте заголовочный файл класса MTPacket и MTPacket реализацию socket:didAcceptNewSocket: как показано ниже ( MTHostGameViewController.m ). После указания нового сокета начать мониторинг входящего потока данных, мы создаем экземпляр класса MTPacket , заполняем его фиктивными данными и передаем пакет sendPacket: .

1
#import «MTPacket.h»
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
— (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];
 
    // Create Packet
    NSString *message = @»This is a proof of concept.»;
    MTPacket *packet = [[MTPacket alloc] initWithData:message type:0 action:0];
 
    // Send Packet
    [self sendPacket:packet];
}

Как я писал ранее, мы можем отправлять двоичные данные только через TCP-соединение. Это означает, что нам нужно закодировать MTPacket экземпляр MTPacket . Поскольку класс MTPacket соответствует протоколу NSCoding , это не проблема. Взгляните на метод sendPacket: показанный ниже. Мы создаем экземпляр NSMutableData и используем его для инициализации архиватора с NSMutableData . Класс NSKeyedArchiver является подклассом NSCoder и имеет возможность кодировать объекты, соответствующие протоколу NSCoding . Имея в своем распоряжении архиватор с ключами, мы кодируем packet .

Затем мы создаем еще NSMutableData экземпляр NSMutableData , который будет объектом данных, который мы передадим в сокет чуть позже. Однако объект данных не только содержит закодированный экземпляр MTPacket . Также необходимо включить заголовок, предшествующий кодированному пакету. Мы храним длину закодированного пакета в переменной с именем headerLength которая имеет тип uint64_t . Затем мы добавляем заголовок в буфер NSMutableData . Вы headerLength символ & предшествующий headerLength ? Метод appendBytes:length: ожидает буфера байтов, а не значения значения headerLength . Наконец, мы добавляем содержимое packetData в буфер. Затем буфер передается в writeData:withTimeout:tag: Библиотека CocoaAsyncSocket заботится о мельчайших деталях отправки данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (void)sendPacket:(MTPacket *)packet {
    // Encode Packet Data
    NSMutableData *packetData = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:packetData];
    [archiver encodeObject:packet forKey:@»packet»];
    [archiver finishEncoding];
 
    // Initialize Buffer
    NSMutableData *buffer = [[NSMutableData alloc] init];
 
    // Fill Buffer
    uint64_t headerLength = [packetData length];
    [buffer appendBytes:&headerLength length:sizeof(uint64_t)];
    [buffer appendBytes:[packetData bytes] length:[packetData length]];
 
    // Write Buffer
    [self.socket writeData:buffer withTimeout:-1.0 tag:0];
}

Чтобы получить только что отправленный пакет, нам нужно изменить класс MTJoinGameViewController . Помните, что в предыдущей статье мы реализовали метод socket:didConnectToHost:port: . Этот метод вызывается, когда соединение установлено после того, как клиент присоединился к игре. Посмотрите на его оригинальную реализацию ниже. Как и в MTHostGameViewController классом MTHostGameViewController , мы сообщаем сокету начать чтение данных без тайм-аута.

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];
}

Когда сокет прочитает полный заголовок, предшествующий пакетным данным, он вызовет метод socket:didReadData:withTag: делегат. readDataToLength:withTimeout:tag: тег — это тот же тег в readDataToLength:withTimeout:tag: метод. Как вы можете видеть ниже, реализация socket:didReadData:withTag: на удивление проста. Если tag равен 0 , мы передаем переменную data в parseHeader: который возвращает заголовок, то есть длину пакета, следующего за заголовком. Теперь мы знаем размер закодированного пакета и передаем эту информацию в readDataToLength:withTimeout:tag: Время ожидания установлено на 30 (секунд), а последний параметр, тег, установлен на 1 .

01
02
03
04
05
06
07
08
09
10
— (void)socket:(GCDAsyncSocket *)socket didReadData:(NSData *)data withTag:(long)tag {
    if (tag == 0) {
        uint64_t bodyLength = [self parseHeader:data];
        [socket readDataToLength:bodyLength withTimeout:-1.0 tag:1];
 
    } else if (tag == 1) {
        [self parseBody:data];
        [socket readDataToLength:sizeof(uint64_t) withTimeout:30.0 tag:0];
    }
}

Прежде чем мы рассмотрим реализацию parseHeader: давайте сначала продолжим наше исследование socket:didReadData:withTag: Если tag равен 1 , мы знаем, что мы прочитали полный кодированный пакет. Мы анализируем пакет и повторяем цикл, говоря сокету, чтобы он следил за заголовком следующего поступающего пакета. Важно, чтобы мы передали -1 для тайм-аута (без тайм-аута), так как мы не знаем, когда придет следующий пакет.

В parseHeader: метод функция memcpy делает всю тяжелую работу за нас. Мы копируем содержимое data в переменную headerLength типа uint64_t . Если вы не знакомы с функцией memcpy , вы можете прочитать больше об этом здесь .

1
2
3
4
5
6
— (uint64_t)parseHeader:(NSData *)data {
    uint64_t headerLength = 0;
    memcpy(&headerLength, [data bytes], sizeof(uint64_t));
 
    return headerLength;
}

В parseBody: мы делаем обратное тому, что мы делали в sendPacket: в классе MTHostGameViewController . Мы создаем экземпляр NSKeyedUnarchiver , передаем данные, которые мы читаем из потока чтения, и создаем экземпляр MTPacket путем декодирования данных с использованием ключевого unarchiver. Чтобы доказать, что все работает как надо, мы записываем данные, тип и действие пакета в консоль Xcode. Не забудьте импортировать заголовочный файл класса MTPacket .

1
#import «MTPacket.h»
1
2
3
4
5
6
7
8
9
— (void)parseBody:(NSData *)data {
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    MTPacket *packet = [unarchiver decodeObjectForKey:@»packet»];
    [unarchiver finishDecoding];
 
    NSLog(@»Packet Data > %@», packet.data);
    NSLog(@»Packet Type > %i», packet.type);
    NSLog(@»Packet Action > %i», packet.action);
}

Запустите два экземпляра приложения. Разместите игру в одном экземпляре и присоединитесь к ней в другом. Вы должны увидеть содержимое пакета, записываемого на консоль Xcode.

1
2
3
4
5
2013-04-16 10:11:39.738 Four in a Row[1295:c07] Did Connect with Service: domain(local.) type(_fourinarow._tcp.) name(Tiger) port(58243)
2013-04-16 10:11:41.033 Four in a Row[1295:c07] Socket Did Connect to Host: 193.145.15.148 Port: 58243
2013-04-16 10:11:41.042 Four in a Row[1295:c07] Packet Data > This is a proof of concept.
2013-04-16 10:11:41.043 Four in a Row[1295:c07] Packet Type > 0
2013-04-16 10:11:41.044 Four in a Row[1295:c07] Packet Action > 0

Не удобно помещать сетевую логику в MTHostGameViewController и MTJoinGameViewController . Это только даст нам проблемы в будущем. Более целесообразно использовать MTHostGameViewController и MTJoinGameViewController для установления соединения и передачи соединения — сокета — контроллеру, который отвечает за контроль и прохождение игры.

Чем сложнее проблема, тем больше решений у проблемы, и эти решения часто очень специфичны для проблемы. Другими словами, решение, представленное в этой статье, является жизнеспособным вариантом, но не рассматривайте его как единственное решение. Для одного из моих проектов, Pixelstream , я также использовал Bonjour и библиотеку CocoaAsyncSocket. Однако мой подход к этому проекту сильно отличается от того, который я здесь представляю. В Pixelstream мне нужно иметь возможность отправлять пакеты из разных мест приложения, и поэтому я решил использовать один объект, который управляет соединением. В сочетании с блоками завершения и очередью пакетов это решение очень хорошо работает для Pixelstream. В этой статье, однако, установка менее сложна, потому что проблема довольно проста. Не усложняйте вещи, если вам не нужно.

Стратегия, которую мы будем использовать, проста. И MTHostGameViewController и MTJoinGameViewController имеют делегата, который уведомляется, когда устанавливается новое соединение. Делегатом будет наш экземпляр MTViewController . Последний создаст игровой контроллер, экземпляр класса MTGameController , который управляет подключением и MTGameController игры. Класс MTGameController будет отвечать за соединение: отправлять и получать пакеты, а также предпринимать соответствующие действия в зависимости от содержимого пакетов. Если бы вы работали над более сложной игрой, было бы хорошо разделить сетевую и игровую логику, но я не хочу слишком сильно усложнять вещи в этом примере проекта. В этой серии я хочу убедиться, что вы понимаете, как различные части сочетаются друг с другом, чтобы вы могли адаптировать эту стратегию к любому проекту, над которым вы работаете.

Протоколы делегатов, которые нам нужно создать, не являются сложными. Каждый протокол имеет два метода. Хотя у меня аллергия на дублирование, я думаю, что полезно создать отдельный протокол делегата для каждого класса, классов MTHostGameViewController и MTJoinGameViewController .

Объявление протокола делегата для класса MTHostGameViewController показано ниже. Если вы уже создали собственные протоколы, то никаких сюрпризов вы не найдете.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
#import <UIKit/UIKit.h>
 
@class GCDAsyncSocket;
@protocol MTHostGameViewControllerDelegate;
 
@interface MTHostGameViewController : UIViewController
 
@property (weak, nonatomic) id delegate;
 
@end
 
@protocol MTHostGameViewControllerDelegate
— (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket;
— (void)controllerDidCancelHosting:(MTHostGameViewController *)controller;
@end

Протокол делегирования, объявленный в классе MTJoinGameViewController практически идентичен. Единственными отличиями являются сигнатуры методов делегатов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
#import <UIKit/UIKit.h>
 
@class GCDAsyncSocket;
@protocol MTJoinGameViewControllerDelegate;
 
@interface MTJoinGameViewController : UITableViewController
 
@property (weak, nonatomic) id delegate;
 
@end
 
@protocol MTJoinGameViewControllerDelegate
— (void)controller:(MTJoinGameViewController *)controller didJoinGameOnSocket:(GCDAsyncSocket *)socket;
— (void)controllerDidCancelJoining:(MTJoinGameViewController *)controller;
@end

Нам также необходимо обновить действия hostGame: и joinGame: в классе MTViewController . Единственное изменение, которое мы делаем, — это назначение экземпляра MTViewController в качестве делегата экземпляров MTHostGameViewController и MTJoinGameViewController .

01
02
03
04
05
06
07
08
09
10
11
12
13
— (IBAction)hostGame:(id)sender {
    // Initialize Host Game View Controller
    MTHostGameViewController *vc = [[MTHostGameViewController alloc] initWithNibName:@»MTHostGameViewController» bundle:[NSBundle mainBundle]];
 
    // Configure Host Game View Controller
    [vc setDelegate:self];
 
    // Initialize Navigation Controller
    UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:vc];
 
    // Present Navigation Controller
    [self presentViewController:nc animated:YES completion:nil];
}
01
02
03
04
05
06
07
08
09
10
11
12
13
— (IBAction)joinGame:(id)sender {
    // Initialize Join Game View Controller
    MTJoinGameViewController *vc = [[MTJoinGameViewController alloc] initWithStyle:UITableViewStylePlain];
 
    // Configure Join Game View Controller
    [vc setDelegate:self];
 
    // Initialize Navigation Controller
    UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:vc];
 
    // Present Navigation Controller
    [self presentViewController:nc animated:YES completion:nil];
}

Это также означает, что класс MTViewController должен соответствовать протоколам делегатов MTHostGameViewControllerDelegate и MTJoinGameViewControllerDelegate и реализовывать методы каждого протокола. Мы рассмотрим реализацию этих методов делегатов через несколько минут. Во-первых, я хотел бы продолжить рефакторинг классов MTHostGameViewController и MTJoinGameViewController .

1
2
3
4
5
6
7
8
#import «MTViewController.h»
 
#import «MTHostGameViewController.h»
#import «MTJoinGameViewController.h»
 
@interface MTViewController () <MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate>
 
@end

Первое, что нам нужно сделать, это обновить socket:didAcceptNewSocket: метод делегата протокола делегирования GCDAsyncSocket . Метод становится намного проще, поскольку работа переносится на делегата. Мы также вызываем endBroadcast , вспомогательный метод, который мы реализуем через мгновение. Когда соединение установлено, мы отклоняем хост-контроллер, и игра может начаться.

01
02
03
04
05
06
07
08
09
10
11
12
— (void)socket:(GCDAsyncSocket *)socket didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
    NSLog(@»Accepted New Socket from %@:%hu», [newSocket connectedHost], [newSocket connectedPort]);
 
    // Notify Delegate
    [self.delegate controller:self didHostGameOnSocket:newSocket];
 
    // End Broadcast
    [self endBroadcast];
 
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

В endBroadcast мы убеждаемся, что мы все убираем. Это также хороший момент, чтобы обновить cancel: действие, которое мы оставили незавершенным в предыдущей статье.

01
02
03
04
05
06
07
08
09
10
11
— (void)endBroadcast {
    if (self.socket) {
        [self.socket setDelegate:nil delegateQueue:NULL];
        [self setSocket:nil];
    }
 
    if (self.service) {
        [self.service setDelegate:nil];
        [self setService:nil];
    }
}

В действии cancel: мы уведомляем делегата, вызывая второй метод делегата, а также вызываем endBroadcast как делали ранее.

01
02
03
04
05
06
07
08
09
10
— (void)cancel:(id)sender {
    // Cancel Hosting Game
    [self.delegate controllerDidCancelHosting:self];
 
    // End Broadcast
    [self endBroadcast];
 
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

Перед тем, как продолжить наш рефакторинг, рекомендуется очистить метод dealloc контроллера представления, как показано ниже.

01
02
03
04
05
06
07
08
09
10
— (void)dealloc {
    if (_delegate) {
        _delegate = nil;
    }
 
    if (_socket) {
        [_socket setDelegate:nil delegateQueue:NULL];
        _socket = nil;
    }
}

Как и в случае с socket:didAcceptNewSocket: метод, нам нужно обновить метод socket:didConnectToHost:port: метод, как показано ниже. Мы уведомляем делегата, прекращаем просмотр сервисов и отклоняем контроллер представления.

01
02
03
04
05
06
07
08
09
10
11
12
— (void)socket:(GCDAsyncSocket *)socket didConnectToHost:(NSString *)host port:(UInt16)port {
    NSLog(@»Socket Did Connect to Host: %@ Port: %hu», host, port);
 
    // Notify Delegate
    [self.delegate controller:self didJoinGameOnSocket:socket];
 
    // Stop Browsing
    [self stopBrowsing];
 
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

Мы также обновляем методы cancel: и dealloc как мы делали это в классе MTHostGameViewController .

01
02
03
04
05
06
07
08
09
10
— (void)cancel:(id)sender {
    // Notify Delegate
    [self.delegate controllerDidCancelJoining:self];
 
    // Stop Browsing Services
    [self stopBrowsing];
 
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}
01
02
03
04
05
06
07
08
09
10
— (void)dealloc {
    if (_delegate) {
        _delegate = nil;
    }
 
    if (_socket) {
        [_socket setDelegate:nil delegateQueue:NULL];
        _socket = nil;
    }
}

Чтобы убедиться, что мы ничего не сломали, реализуйте методы делегата обоих протоколов в классе MTViewController как показано ниже, и запустите два экземпляра приложения, чтобы проверить, не сломали ли мы что-нибудь. Если все идет хорошо, вы должны увидеть соответствующие сообщения, записываемые в консоль XCode, и контроллеры модального представления должны автоматически отклоняться, когда игра присоединяется, то есть когда устанавливается соединение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
#pragma mark —
#pragma mark Host Game View Controller Methods
— (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket {
    NSLog(@»%s», __PRETTY_FUNCTION__);
}
 
— (void)controllerDidCancelHosting:(MTHostGameViewController *)controller {
    NSLog(@»%s», __PRETTY_FUNCTION__);
}
 
#pragma mark —
#pragma mark Join Game View Controller Methods
— (void)controller:(MTJoinGameViewController *)controller didJoinGameOnSocket:(GCDAsyncSocket *)socket {
    NSLog(@»%s», __PRETTY_FUNCTION__);
}
 
— (void)controllerDidCancelJoining:(MTJoinGameViewController *)controller {
    NSLog(@»%s», __PRETTY_FUNCTION__);
}

Класс MTViewController не будет отвечать за обработку соединения и прохождение игры. За это MTGameController пользовательский класс контроллера, MTGameController . Одна из причин создания отдельного класса контроллеров заключается в том, что после запуска игры мы не будем различать сервер и клиент. Поэтому целесообразно иметь контроллер, отвечающий за соединение и игру, но не различающий сервер и клиент. Другая причина заключается в том, что единственной обязанностью классов MTHostGameViewController и MTJoinGameViewController является поиск игроков в локальной сети и установление соединения. У них не должно быть никаких других обязанностей.

Создайте новый подкласс MTGameController и назовите его MTGameController (рисунок 3). Интерфейс класса MTGameController довольно прост, как вы можете видеть ниже. Это изменится, как только мы начнем реализовывать игровую логику, но это будет делать пока. Назначенный инициализатор принимает один аргумент, экземпляр GCDAsyncSocket которым он будет управлять.

Создание игр с Bonjour - Отправка данных - Создание класса игрового передатчика
Создание класса игрового контроллера
01
02
03
04
05
06
07
08
09
10
11
#import <Foundation/Foundation.h>
 
@class GCDAsyncSocket;
 
@interface MTGameController : NSObject
 
#pragma mark —
#pragma mark Initialization
— (id)initWithSocket:(GCDAsyncSocket *)socket;
 
@end

Прежде чем мы реализуем initWithSocket: нам нужно создать частное свойство для сокета. Создайте расширение класса, как показано ниже, и объявите свойство типа GCDAsyncSocket именем socket . Я также позволил себе импортировать заголовочный файл класса MTPacket и определить TAG_HEAD и TAG_BODY чтобы упростить работу с тегами в GCDAsyncSocketDelegate делегата GCDAsyncSocketDelegate . Конечно, класс MTGameController должен соответствовать протоколу делегата GCDAsyncSocketDelegate , чтобы все работало.

01
02
03
04
05
06
07
08
09
10
11
12
#import «MTGameController.h»
 
#import «MTPacket.h»
 
#define TAG_HEAD 0
#define TAG_BODY 1
 
@interface MTGameController ()
 
@property (strong, nonatomic) GCDAsyncSocket *socket;
 
@end

Реализация initWithSocket: показана ниже и не должна быть слишком удивительной. Мы сохраняем ссылку на сокет в только что созданном нами частном свойстве, устанавливаем игровой контроллер в качестве делегата сокета и сообщаем сокету, чтобы он начал читать входящие данные, то есть перехватывать первый поступающий заголовок.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#pragma mark —
#pragma mark Initialization
— (id)initWithSocket:(GCDAsyncSocket *)socket {
    self = [super init];
 
    if (self) {
        // Socket
        self.socket = socket;
        self.socket.delegate = self;
 
        // Start Reading Data
        [self.socket readDataToLength:sizeof(uint64_t) withTimeout:-1.0 tag:TAG_HEAD];
    }
 
    return self;
}

Остальная часть процесса рефакторинга также не сложна, потому что мы уже проделали большую часть работы в MTHostGameViewController и MTJoinGameViewController . Давайте начнем с рассмотрения реализации протокола делегата GCDAsyncSocketDelegate . Реализация не отличается от того, что мы видели ранее в MTHostGameViewController и MTJoinGameViewController .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
— (void)socketDidDisconnect:(GCDAsyncSocket *)socket withError:(NSError *)error {
    NSLog(@»%s», __PRETTY_FUNCTION__);
 
    if (self.socket == socket) {
        [self.socket setDelegate:nil];
        [self setSocket:nil];
    }
}
 
— (void)socket:(GCDAsyncSocket *)socket didReadData:(NSData *)data withTag:(long)tag {
    if (tag == 0) {
        uint64_t bodyLength = [self parseHeader:data];
        [socket readDataToLength:bodyLength withTimeout:-1.0 tag:1];
 
    } else if (tag == 1) {
        [self parseBody:data];
        [socket readDataToLength:sizeof(uint64_t) withTimeout:-1.0 tag:0];
    }
}

Реализация sendPacket: parseHeader: и parseBody: ничем не отличаются.

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
30
31
32
33
34
35
— (void)sendPacket:(MTPacket *)packet {
    // Encode Packet Data
    NSMutableData *packetData = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:packetData];
    [archiver encodeObject:packet forKey:@»packet»];
    [archiver finishEncoding];
 
    // Initialize Buffer
    NSMutableData *buffer = [[NSMutableData alloc] init];
 
    // Fill Buffer
    uint64_t headerLength = [packetData length];
    [buffer appendBytes:&headerLength length:sizeof(uint64_t)];
    [buffer appendBytes:[packetData bytes] length:[packetData length]];
 
    // Write Buffer
    [self.socket writeData:buffer withTimeout:-1.0 tag:0];
}
 
— (uint64_t)parseHeader:(NSData *)data {
    uint64_t headerLength = 0;
    memcpy(&headerLength, [data bytes], sizeof(uint64_t));
 
    return headerLength;
}
 
— (void)parseBody:(NSData *)data {
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    MTPacket *packet = [unarchiver decodeObjectForKey:@»packet»];
    [unarchiver finishDecoding];
 
    NSLog(@»Packet Data > %@», packet.data);
    NSLog(@»Packet Type > %i», packet.type);
    NSLog(@»Packet Action > %i», packet.action);
}

Метод parseBody: будет играть важную роль чуть позже в истории, но пока это будет parseBody: . Наша цель на данный момент — заставить все работать снова после завершения процесса рефакторинга.

Прежде чем мы продолжим, важно реализовать метод dealloc класса MTGameController как показано ниже. Всякий раз, когда игровой контроллер освобождается, экземпляру необходимо разорвать соединение, вызвав disconnect на экземпляре GCDAsyncSocket .

1
2
3
4
5
6
7
— (void)dealloc {
    if (_socket) {
        [_socket setDelegate:nil delegateQueue:NULL];
        [_socket disconnect];
        _socket = nil;
    }
}

Класс MTViewController будет управлять игровым контроллером и взаимодействовать с ним. MTViewController отобразит игру и позволит пользователю взаимодействовать с ней. MTGameController и MTViewController должны взаимодействовать друг с другом, и для этой цели мы будем использовать другой протокол делегирования. Связь асимметрична в том смысле, что контроллер вида знает об игровом контроллере, но игровой контроллер не знает о контроллере вида. Мы будем расширять протокол по мере продвижения, но пока контроллер представления должен уведомляться только тогда, когда соединение потеряно.

Повторно посетите MTGameController.h и объявите протокол делегирования, как показано ниже. Кроме того, для делегата игрового контроллера создается открытое свойство.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>
 
@class GCDAsyncSocket;
@protocol MTGameControllerDelegate;
 
@interface MTGameController : NSObject
 
@property (weak, nonatomic) id delegate;
 
#pragma mark —
#pragma mark Initialization
— (id)initWithSocket:(GCDAsyncSocket *)socket;
 
@end
 
@protocol MTGameControllerDelegate
— (void)controllerDidDisconnect:(MTGameController *)controller;
@end

Мы можем сразу же использовать протокол делегата, уведомив делегат игрового контроллера в одном из GCDAsyncSocketDelegate делегата socketDidDisconnect:withError: быть точным, socketDidDisconnect:withError:

01
02
03
04
05
06
07
08
09
10
11
— (void)socketDidDisconnect:(GCDAsyncSocket *)socket withError:(NSError *)error {
    NSLog(@»%s», __PRETTY_FUNCTION__);
 
    if (self.socket == socket) {
        [self.socket setDelegate:nil];
        [self setSocket:nil];
    }
 
    // Notify Delegate
    [self.delegate controllerDidDisconnect:self];
}

Последняя часть головоломки рефакторинга — использование MTGameController . Создайте частное свойство в классе MTViewController , MTViewController класс MTGameControllerDelegate протоколом MTGameControllerDelegate и импортируйте файл MTGameController класса MTGameController .

01
02
03
04
05
06
07
08
09
10
11
#import «MTViewController.h»
 
#import «MTGameController.h»
#import «MTHostGameViewController.h»
#import «MTJoinGameViewController.h»
 
@interface MTViewController () <MTGameControllerDelegate, MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate>
 
@property (strong, nonatomic) MTGameController *gameController;
 
@end

В controller:didHostGameOnSocket: и controller:didJoinGameOnSocket: мы вызываем startGameWithSocket: и передаем сокет нового соединения.

1
2
3
4
5
6
— (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket {
    NSLog(@»%s», __PRETTY_FUNCTION__);
 
    // Start Game with Socket
    [self startGameWithSocket:socket];
}
1
2
3
4
5
6
— (void)controller:(MTJoinGameViewController *)controller didJoinGameOnSocket:(GCDAsyncSocket *)socket {
    NSLog(@»%s», __PRETTY_FUNCTION__);
 
    // Start Game with Socket
    [self startGameWithSocket:socket];
}

В вспомогательном методе startGameWithSocket: мы создаем экземпляр класса MTGameController , передавая сокет и MTGameController ссылку на игровой контроллер в свойстве gameController контроллера представления. Контроллер представления также служит делегатом игрового контроллера, как мы обсуждали ранее.

1
2
3
4
5
6
7
— (void)startGameWithSocket:(GCDAsyncSocket *)socket {
    // Initialize Game Controller
    self.gameController = [[MTGameController alloc] initWithSocket:socket];
 
    // Configure Game Controller
    [self.gameController setDelegate:self];
}

В методе controllerDidDisconnect: делегат протокола MTGameControllerDelegate мы endGame вспомогательный метод endGame в котором мы endGame игровой контроллер.

1
2
3
4
5
6
— (void)controllerDidDisconnect:(MTGameController *)controller {
    NSLog(@»%s», __PRETTY_FUNCTION__);
 
    // End Game
    [self endGame];
}
1
2
3
4
5
— (void)endGame {
    // Clean Up
    [self.gameController setDelegate:nil];
    [self setGameController:nil];
}

Чтобы убедиться, что все работает, мы должны проверить нашу настройку. Давайте откроем XIB-файл MTViewController и добавим еще одну кнопку в левом верхнем углу под названием « Отключить» (рисунок 4). Пользователь может нажать на эту кнопку, когда он хочет закончить или выйти из игры.Мы показываем эту кнопку только тогда, когда соединение установлено. Когда соединение активно, мы скрываем кнопки для размещения и присоединения к игре. Внесите необходимые изменения в MTViewcontroller.xib (рисунок 4), создайте выход для каждой кнопки в MTViewController.h и подключите выходы в MTViewcontroller.xib .

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>
 
@interface MTViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
 
@end

Наконец, создайте действие с именем disconnect:в MTViewController.m и подключите его с заголовком кнопки Отключить .

1
2
3
- (IBAction)disconnect:(id)sender {
    [self endGame];
}
Создание игры с Bonjour - Отправка данных - Обновление пользовательского интерфейса
Обновление пользовательского интерфейса

В setupGameWithSocket:методе, мы прячем hostButtonи joinButton, и мы показываем disconnectButton. В этом endGameметоде мы делаем прямо противоположное, чтобы убедиться, что пользователь может вести игру или присоединиться к ней. Нам также нужно скрыть метод disconnectButtonконтроллера вида viewDidLoad.

01
02
03
04
05
06
07
08
09
10
11
12
- (void)startGameWithSocket:(GCDAsyncSocket *)socket {
    // Initialize Game Controller
    self.gameController = [[MTGameController alloc] initWithSocket:socket];
 
    // Configure Game Controller
    [self.gameController setDelegate:self];
 
    // Hide/Show Buttons
    [self.hostButton setHidden:YES];
    [self.joinButton setHidden:YES];
    [self.disconnectButton setHidden:NO];
}
01
02
03
04
05
06
07
08
09
10
- (void)endGame {
    // Clean Up
    [self.gameController setDelegate:nil];
    [self setGameController:nil];
 
    // Hide/Show Buttons
    [self.hostButton setHidden:NO];
    [self.joinButton setHidden:NO];
    [self.disconnectButton setHidden:YES];
}
1
2
3
4
5
6
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Hide Disconnect Button
    [self.disconnectButton setHidden:YES];
}

Чтобы проверить, все ли еще работает, нам нужно отправить тестовый пакет, как мы делали это чуть ранее в этой статье. Объявите метод с именем testConnectionв MTGameController.h и реализуйте его, как показано ниже.

1
- (void)testConnection;
1
2
3
4
5
6
7
8
- (void)testConnection {
    // Create Packet
    NSString *message = @"This is a proof of concept.";
    MTPacket *packet = [[MTPacket alloc] initWithData:message type:0 action:0];
 
    // Send Packet
    [self sendPacket:packet];
}

Контроллер представления должен вызывать этот метод всякий раз, когда было установлено новое соединение. Хорошее место для этого — controller:didHostGameOnSocket:метод делегата после инициализации игрового контроллера.

1
2
3
4
5
6
7
8
9
- (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket🙁GCDAsyncSocket *)socket {
    NSLog(@»%s», __PRETTY_FUNCTION__);
 
    // Start Game with Socket
    [self startGameWithSocket:socket];
 
    // Test Connection
    [self.gameController testConnection];
}

Запустите приложение еще раз, чтобы убедиться, что все работает после процесса рефакторинга.


Сейчас настало время , чтобы очистить MTHostGameViewControllerи MTJoinGameViewControllerклассы, избавившись от любого кода , который больше не принадлежит в этих классах. Для MTHostGameViewControllerкласса это означает удаление sendPacket:метода, а для MTJoinGameViewControllerкласса — удаление socket:didReadData:withTag:метода CocoaAsyncSocketDelegateпротокола делегата, а также parseBody:методов code> parseHeader: и helper.


Я могу представить, что эта статья оставила вас немного ошеломленными или ошеломленными. Было много чего взять и обработать. Однако я хочу подчеркнуть, что сложность этой статьи была связана главным образом с тем, как структурировано само приложение, а не с тем, как работать с Bonjour и библиотекой CocoaAsyncSocket. Зачастую сложно создать приложение таким образом, чтобы свести к минимуму зависимости и сохранить приложение компактным, производительным и модульным. Это главная причина, почему мы реорганизовали нашу первоначальную реализацию сетевой логики.

Теперь у нас есть контроллер представления, который заботится о отображении игры пользователю ( MTViewController), и контроллер ( MTGameController), который обрабатывает логику игры и соединения. Как я упоминал ранее, можно разделить логическую схему соединения и игры, создав отдельный класс для каждого из них, но для этого простого приложения это не нужно.


Мы добились значительного прогресса в нашем игровом проекте, но не хватает одного ингредиента … игры! В следующей части этой серии мы создадим игру и используем основы, которые мы создали до сих пор.