مساحة اعلانية

آخر المواضيع

إنشاء لعبة باستخدام بروتوكول Bonjour- إرسال البيانات


مقدمــــــــــــــــة

كما رأينا في البرنامج التعليمي السابق، مكتبة CocoaAsyncSocket يجعل العمل مع ال sockets سهل للغاية. ومع ذلك، هناك ما هو أكثر من إرسال نص بسيط من جهاز إلى آخر، كما فعلنا في البرنامج التعليمي السابق. في المقال  الأول من هذه السلسلة، ذكرت أن بروتوكول TCP يمكنه إدارة تدفق البيانات في اتجاهين. 


المشكلة، ومع ذلك، هو أنه دفق مستمر من البيانات. بروتوكول TCP يعتني إرسال البيانات من أحد طرفي الاتصال إلى الآخر، ولكن الأمر متروك إلى المتلقي لفهم ما يجري إرساله من خلال هذا الاتصال.
هناك عدة حلول لهذه المشكلة. بروتوكول HTTP، المبني على أعلى من بروتوكول TCP، يرسل header HTTP مع كل طلب ورد على الطلب. header HTTP يحتوي على معلومات حول الطلب أو الاستجابة، التي يمكن للمتلقي استخدامها لفهم البيانات الواردة. عنصر رئيسي من عناصر header HTTP هو طول ال body. إذا كان المتلقي يعرف طول الجسم من الطلب أو الرد، فيمكنه استخراج أصل البيانات المرسلة وتجريدها من المعلومات الاخرى المرسلة .
هذا جيد، ولكن كيف يمكن للمتلقي معرفة طول الheader ؟ ينتهي كل حقل header HTTP مع CRLF (Carriage Return, Line Feed)  HTTP header ينتهي أيضا ب CRLF. وهذا يعني أن رأس كل طلب HTTP واستجابة ينتهي مع اثنين من CRLFs. عندما يقرأ المتلقي البيانات الواردة ، فليس عليه سوى البحث عن هذا النمط (اثنان CRLFs على التوالي). بذلك، يمكن للمتلقي تحديد واستخراج رأس طلب HTTP أو الاستجابة. مع تحديد ال HTTp header، يمكن تحديد واستخلاص ال HTTP body  بسهولة.
الاستراتيجية التي سنتبعها تختلف عن كيفية عمل بروتوكولات HTTP. كل حزمة من البيانات التي نرسلها من خلال الاتصال تكون مدمجة مع ال header  له طول ثابت. header أقل تعقيداً من header HTTP. ال header الذي سوف نستخدمه يحتوي على جزئية واحدة من المعلومات، وطول الجسم أو الحزمة التي تأتي بعد ال header. وبعبارة أخرى، فإن الرأس هو ليس أكثر من الرقم الذي يبلغ المتلقي عن طول الجسم. بمعرفة هذا، يتمكن المتلقي من استخراج الجسم أو الحزمة بنجاح من تيار وارد من البيانات. على الرغم من أن هذا هو نهج بسيط، إلا أنه يعمل بشكل جيد كما سترون في هذا البرنامج التعليمي.

1. الحزم
من المهم أن نفهم أن هذه الأساليب المذكورة أعلاه صممت خصيصا لبروتوكول TCP وأنها تعمل فقط بسبب كيفية عمل بروتوكول TCP. بروتوكول TCP يضمن أن كل حزمة تصل إلى وجهتها في الترتيب الذي تم إرساله.
وبالتالي، فإن الاستراتيجيات التي أشرت إليها تعمل بشكل جيد جدا.


الخطوة 1: إنشاء فئة حزم
على الرغم من أنه يمكننا إرسال أي نوع من البيانات عبر اتصال TCP، فمن المستحسن تخصيص  بنية معينة لاحتواء البيانات التي نود إرسالها. يمكننا تحقيق ذلك من خلال خلق طبقة حزمة مخصصة. وميزة هذا ستتضح عندما نبدأ باستخدام فئة الحزمة. الفكرة بسيطة. ال class هي class Objective-C  الذي يحمل البيانات؛ الجسم، اذا صح التعبير. فإنه يشمل أيضا بعض معلومات إضافية حول الحزمة، يُدعى ال header . والفرق الرئيسي مع بروتوكول HTTP هو أن الرأس والجسم ليسا منفصلين تماماً. سوف يحتاج class الحزمة أيضا أن يتوافق مع بروتوكول NSCoding، وهو ما يعني أن مثيلات الفئة يمكن أن يتم تشفيرها وفك الشفرة. هذا هو مفتاح الحل إذا أردنا أن نرسل مثيلات class الحزمة من خلال اتصال TCP.
إنشاء class جديد من ال Objective-C  وجعلها فئة فرعية من NSObject، وتسميته MTPacket (الشكل 1). لهذه اللعبة التي نقوم ببنائها، يمكن أن يكون class الحزمة بسيط إلى حد ما. ال class  له ثلاثة خصائص، type، action، وdata . يتم استخدام الخاصية type لتحديد الغرض من الحزمة في حين أن خاصية action تتضمن نية الحزمة. يتم استخدام خاصية data لتخزين المحتويات الفعلية أو تحميل الحزمة. وسيصبح الأمر أكثر وضوحا بمجرد البدء باستخدام class الحزمة في لعبتنا.


إنشاء class الحزم

سننتوقف لحظة لتفقد واجهة ال class MTPacket المبين أدناه. كما ذكرت، فمن الضروري أن class instances يمكن تشفيرها وفك الشفرة التي تتفق مع بروتوكول NSCoding. لتتوافق مع بروتوكول NSCoding، نحن بحاجة فقط لبناء two methods، encodeWithCoder: وinitWithCoder :.


من المهم ذكره أن الخصائص type و action هي من نوع MTPacketType وMTPacketAction، على التوالي. يمكنك العثور على تعريفات type في الجزء العلوي من MTPacket.h. إذا لم تكن الرموز المميزة ل typedef و enum  مألوفة لديك، يمكنك قراءة المزيد حول هذا الموضوع Stack Overflow. وسيكون العمل مع فئة MTPacket أسهل.

خاصية data class هي من نوع id. وهذا يعني أنه يمكن أن يكون أي كائن Objective-C. والشرط الوحيد هو أنه يتوافق مع بروتوكول NSCoding. معظم أعضاء foundation framework، مثل NSArray، NSDictionary، وNSNumber، تتفق مع بروتوكول NSCoding.

لتجعل من السهل تعريف مثيلات class MTPacket، فإننا سنقوم بتعريف مهيئ المعين أن يأخذ حزمة من البيانات، والنوع، والعمل كوسائط.
 #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 ، two methods وكلاهما مطلوب. يتم استدعاءهم تلقائيا عندما يتم ترميز مثيل من فئة (encodeWithCoder :) أو فك (initWithCoder :). وبعبارة أخرى، لن تحتاج أبداً لاستدعاء هذه ال methods بنفسك. سوف نرى كيف يعمل هذا في وقت لاحق في هذه المقالة.

كما ترون أدناه، بناء ال initWithData:type:action:   سيكون سهلاً في ملف التنفيذ، لقد أصبح واضحاً أيضا لماذا أعلنا ثلاثة ثوابت في واجهة ال class. ومن الجيد استخدام الثوابت لمفاتيح تستخدمها في بروتوكول NSCoding. السبب الرئيسي هو عدم الأداء، ولكن أخطاء الكتابة. المفاتيح التي يمكنك تمرير عند ترميز خصائص الفئة التي يجب أن تكون مطابقة للمفاتيح التي يتم استخدامها عند فك مثيلات الفئة.

#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

الخطوة 2: إرسال البيانات
قبل أن ننتقل للجزء الآخر من العمل، أود التأكد أن كلاس MTPacket يعمل كما هو متوقع. أفضل طريقة لفحص هذا إرسال حزمة بمجرد الحصول على الاتصال، بمجرد أن تعمل نستطيع أن نبدأ بإعادة ترتيب منطق الشبكة بوضعه في المتحكم(controller).

عند الحصول على الاتصال، يتم إخطار مثيل التطبيق الذي يستضيف اللعبة بذلك  عن طريق استدعاء socket:didAcceptNewSocket وهي ال method المساعدة لبروتوكول GCDAsyncSocketDelegateقمنا ببناء هذه ال method في المقال السابق   . انظر الكود التالي لتراجع ذاكرتك. من المفترض أن يكون آخر سطر في الكود واضحاً الآن. قمنا بإخبار المقبس الجديد أن يبدأ بقراءة البيانات وقمنا بتمرير رقم كوسيط أخير parameter. لم نقم بإعداد timeout (-1) لأننا لا نستطيع توقع توقيت وصول أول حزمة.
مايهم الآن هو أننا لم قمنا بتمرير sizeof(uint64_t) كأول وسيط لل readDataToLength:withTimeout:tag؟؟
 - (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 بال byte والمعرفة في stdint.h.(انظر بالأسفل). وكما وضحت مسبقاً، ال header الذي يسبق كل حزمة نرسلها له طول محدد.(الشكل 2)، وهذا مختلف تماماً عن ال header الخاص بطلب أو رد ال HTTP. في مثالنا، ال header له غرض واحد فقط ، وهو إخبار المستلم حجم الحزمة التي يسبقها. بطريقة أخرى عن طريق إخبار ال socket بقراءة البيانات القادمة من ال header والمتمثلة في (sizeof(uint64_t)) ، نستطيع معرفة أننا قمنا بقراءة كل بيانات ال header. بتحليل ال header بمجرد أن يتم استخلاصه من البيانات المستلمة، يعرف المستلم حجم ال body الذي يتبع الheader.

typedef unsigned long long   uint64_t;

استخدام header ذو طول محدد
قم باستيراد ملف ال header لكلاس ال MTPacket وعدل الكود ل socket:didAcceptNewSocket كما هو موضح في الأسفل(MTHostGameViewController.m). بعد إعطاء الأوامر لل socket الجديدة بالبدء بمراقبة البيانات الواردة، نقوم بإنشاء مثيل لكلاس MTPacket، إعطائه بيانات وهمية، وتمرير الحزمة إلى ال sendPacket: method.
 #import "MTPacket.h" - (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];}

كما قلت مسبقاً، نستطيع فقط إرسال بيانات ثنائية binary data من خلال اتصال ال TCP. وها يعني أننا بحاجة لتشفير مثيل ال MTPacket الذي قمنا بإنشائه. ولأن كلاس ال MTPacket يتوافق مع بروتوكول NSCoding فهذه لن تكون مشكلة. انظر إلى sendPacket: method في الأسفل.
لقد أنشأنا مثيل NSMutableData واستخدمناه لتعريف الأرشيف. وكلاس NSKeyedArchiver هو كلاس فرعي من الكلاس NSCoder والقادر على تشفير الكائنات المتوافقة مع بروتوكول NSCoding.بوجود الأرشيف بحوزتنا نقوم بتشفير الحزمة.
ثم نقوم بإنشاء مثيل جديد من NSMutableData، والذي سيكوون كائن البيانات التي ستمرر لل socket لاحقاً. كائن البيانات لا يحتوي فقط مثيل MTPacket ولكنه يحتاج إلى أن يتضمن ال header الذي يسبق الحزمة المشفرة. نقوم بتخزين طول الحزمة المشفرة في متغير اسمه headerLength وهو من نوع uint64_t. ثم نقوم بلصق الheader لل NSMutableData buffer. هل لاحظت إشارة & التي تسبق قيمة  ال headerLength؟ ال appendBytes:length: method تتوقع buffer of bytes وليس قيمة الheaderLength.
وأخيراً، نقوم بإضافة محتويات الحزمة packetData إلى ال buffer. ومن ثم يتم تمرير ال buffer إلى writeData:withTimeout:tag:.. مكتبة CocoaAsyncSocket تراعي تفاصيل إرسال البيانات.

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

الخطوة 3: استلام البيانات
لاستلام الحزمة التي أرسلناها للتو، نحتاج لتعديل كلاس MTJoinGameViewController. تذكر أننا قمنا بإنشاء socket:didConnectToHost:port: delegate method في المقال السابق، وهي تستدعى عندما يتم الحصول على الاتصال بعد التحاق ال client باللعبة. ألق نظرة على الكود الأصلي في الأسفل. فقط كما قمنا بعمله في كلاس MTHostGameViewController نقوم بإخبار الsocket أن تبدأ بقراءة البيانات من دون تحديد وقت نهاية.

 - (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 بقراءة الheader الذي يسبق البيانات بالكامل، ستستدعي socket:didReadData:withTag: delegate method وال tag التي تم تمريرها هي فعليا نفسها في readDataToLength:withTimeout:tag: method وكما تستطيع أن ترى في الأسفل، بناء ال socket:didReadData:withTag يعتبر بسيط جداً. إذا كانت ال tag تساوي صفر نقوم بتمرير متغير البيانات إلى parseHeader، والتي تقوم بإرجاع ال header والذي يعبر عن طول البيانات التي تتبع ال header. نحن الآن نعرف طول الحزمة المشفرة ونقوم بتمرير هذه البيانات إلى readDataToLength:withTimeout:tag ويتم وضع الوقت النهائي 30 ثانية وآخر وسيط ال tag يتم إعطاؤه القيمة 1.
 - (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:withTa، إذا كان ال tag يساوي 1، نعرف أننا قمنا بقراءة الحزمة المشفرة بالكامل، نحلل الحزمة و نعيد الكرة بإخبار ال socket بانتظار قراءة ال header للحزمة التالية، من المهم تمرير -1 للزمن النهائي أي وقت غير محدد وذلك لأننا لا نعلم زمن وصول الحزمة التالية.
في parseHeader: method  دالة ال memcpy هي أهم شيء متبقي لنا. نحن نقوم بنسخ محتويات البيانات في المتغير headerLength من نوع uint64_tإذا لم تكن دالة memcpy واضحة لك بإمكانك الاطلاع على المزيد من هنا .

- (uint64_t)parseHeader:(NSData *)data {    uint64_t headerLength = 0;    memcpy(&headerLength, [data bytes], sizeof(uint64_t));     return headerLength;}
في parseBody نقوم بعمل عكس ما تقوم به sendPacket: method في كلاس MTHostGameViewController. نقوم بإنشاء مثيل من NSKeyedUnarchiver، نمرر البيانات التي قرأناها، وننشئ مثيل من MTPacket عن طريق فك تشفير البيانات باستخدام unarchiver.  لإثبات أن كل شيء يعمل كما يرام، نقوم بتخزين بيانات الحزمة ونوعها والحدث في Xcode console. لا تنس أن تقوم باستيراد ملف ال header لكلاس MTPacket.

#import "MTPacket.h" - (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 console.
 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: 582432013-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 > 02013-04-16 10:11:41.044 Four in a Row[1295:c07] Packet Action > 0 
2- إعادة الصياغةليس من المناسب وضع منطق الشبكات في كلاس MTHostGameViewController أو MTJoinGameViewController. من الأنسب استخدام MTHostGameViewController و MTJoinGameViewController للحصول على الاتصال و تمرير الاتصال إلى المتحكم والذي بدوره يتحكم بسير اللعبة.

كلما كانت المشكلة أكثر تعقيداً كلما ازدادت الحلول وهذه الحلول بالعادة خاصة جداً لحل هذه المشكلة.

بمعنى آخر الحل المعروض في هذا التقرير ليس الوحيد. في أحد مشاريعي Pixelstream قمت باستخدام بروتوكل Bonjour و مكتبة CocoaAsyncSocket. طريقتي في هذا المشروع مختلفة جداً عما قمت باستخدامه هنا. في المشروع المذكور كنت أحتاج  لإرسال الحزم من أماكن مختلفة من التطبيق ولذلك اخترت كائن منفرد لإدارة الاتصال، بالإضافة لاستخدام ال blocks المكملة و طابور الحزم و هذا الحل عمل جيداً في المشروع المذكور. في هذا المقال الإعداد أقل تعقيداً لأن المشكلة بسيطة. ومن هنا نستنتج أنه لا يجب عليك تعقيد المشكلة إذا كانت هي بالأساس سهلة وممكن حلها ببساطة أكبر.
الاستراتيجية التي سنستخدمها سهلة للغاية. كل من كلاس MTHostGameViewController و كلاس MTJoinGameViewController لها نائب ويلاحظ عند الحصول على اتصال جديد. النائب سيكون مثيل ال MTViewControllerالرسالة ستنشئ متحكم جديد للعبة، مثيل من كلاس MTGameController والذي يدير الاتصال وسير اللعبة. كلاس MTGameController ستكون مهمته في الاتصال: إرسال واستقبال الحزم وعمل المناسب بالاعتماد على محتويات الحزمة.
إذا كنت تعمل على لعبة أكثر تعقيداً، سيكون من الأفضل فصل الشبكة عن منطق اللعبة، ولكني لا أود تعقيد الأمور في هذا المثال. في هذه السلسلة أريد التأكد من أنك فهمت كيفية ارتباط الجزئيات المختلفة مع بعضها البعض بحيث تستطيع تطبيق هذه الاستراتيجية على أي مشروع تعمل عليه.
الخطوة 1: إنشاء البروتوكولات النائبة
البروتوكولات النائبة التي نحتاج لبنائها ليست معقدة. كل بروتوكول له اثنتين من ال methods. ومع ذلك أنا متحيز للتكرار، أعتقد أنه من المفيد إنشاء بروتوكول نائب لكل من الكلاس MTHostGameViewController و MTJoinGameViewController.
تعريف البروتوكول النائب لكلاس MTHostGameViewController موضح بالأسفل. إذا قمت بإنشاء بروتوكولات مخصصة مسبقاً فلن تجد أي صعوبة.

 #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 مشابه جداً. الشيء المختلف هو تواقيع ال methods لل methods المساعدة.
#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.

- (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];} - (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 وبناء ال methods لكل بروتوكول. سنلقي نظرة على بناء ال delegate methods بعد قليل. ولكن أولاً أود إكمال إعادة ترتيب كل من الكلاس MTHostGameViewController و MTJoinGameViewController.
 #import "MTViewController.h" #import "MTHostGameViewController.h"#import "MTJoinGameViewController.h" @interface MTViewController () <MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate> @end

الخطوة 2: إعادة صياغة MTHostGameViewController
أول شيء يجب علينا فعله هو تحديث socket:didAcceptNewSocket: delegate method للبروتوكول النائب GCDAsyncSocket.ال method أصبحت أبسط بكثير وذلك لانتقال جزء من العمل إلى النائب. نستدعي أيضاً endBroadcast وهي method مساعدة سنقوم بإنشائها الآن. عند الحصول على الاتصال، يمكن للعبة أن تبدأ.

- (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 الذي لم نكمله في المقالة السابقة.
 - (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، ننبه النائب عن طريق استدعاء الmethod النائبة الثانية و نستدعي أيضاً endBroadcast كما فعلنا مسبقاً.

- (void)cancel:(id)sender {    // Cancel Hosting Game    [self.delegate controllerDidCancelHosting:self];     // End Broadcast    [self endBroadcast];     // Dismiss View Controller    [self dismissViewControllerAnimated:YES completion:nil];}

قبل إكمال إعادة الصياغة، من الجيد محو الأمور في method  متحكم العرض dealloc كما ترى في الأسفل:

- (void)dealloc {    if (_delegate) {        _delegate = nil;    }     if (_socket) {        [_socket setDelegate:nil delegateQueue:NULL];        _socket = nil;    }}

الخطوة 3: إعادة صياغة MTJoinGameViewController
وكما فعلنا في socket:didAcceptNewSocket: method ، نحتاج لتحديث socket:didConnectToHost:port: method كما سنرى في الأسفل. وننبه النائب بإيقاف استعراض الخدمات. وإزالة متحكم العرض.

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

ونقوم أيضاً بتحديث ال methods الاثنتين cancel و dealloc كما فعلنا في كلاس MTHostGameViewController.

- (void)cancel:(id)sender { // Notify Delegate [self.delegate controllerDidCancelJoining:self]; // Stop Browsing Services [self stopBrowsing]; // Dismiss View Controller [self dismissViewControllerAnimated:YES completion:nil];}
- (void)dealloc { if (_delegate) { _delegate = nil; } if (_socket) { [_socket setDelegate:nil delegateQueue:NULL]; _socket = nil; }}
الآن نقوم ببناء كلا delegate methods للبروتوكولين في الكلاس  MTViewController كما ترى في الأسفل وتشغيل مثيلين من التطبيق لفحص أننا لم نقم بتعطيل شيء. إذا كان كل شيء على ما يرام، سترى الرسائل المناسبة تم تخزينها في وحدة تحكم Xcode. ومتحكمات العرض الشكلية المفترض أن ترفض تلقائياً عند الانضمام للعبة، أي عند حدوث الاتصال.
#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__);}

3- بناء متحكم اللعب

الخطوة 1: إنشاء كلاس متحكم اللعب

كلاس MTViewController لن يكون مسؤولاً عن التعامل مع الاتصال وسير اللعب. كلاس متحكم مخصص وهو MTGameController سيكون مسؤولاً عن ذلك.
أحد أسباب إنشاء متحكم منفصل أنه بمجرد بدء اللعب لن نستطيع التمييز بين الخادم والتابع. لذلك فإنه من المناسب أن يكون هناك كلاس مستقل مسؤول عن الاتصال وسير اللعب، ولكن هذا لن يفرق الخادم عن التابع. وسبب آخر هو أن المسؤولية الوحيدة التي تقع على عاتق كل من MTHostGameViewController و MTJoinGameViewController هي إيجاد لاعبين على الشبكة المحليةوالحصول على اتصال ولا يجب أن يكون لديها أي مسؤوليات أخرى.

أنشئ كلاس فرعي جديد من NSObject وسميه MTGameController كما في الشكل(3). واجهة كلاس MTGameController سهلة كما ترى في الأسفل. وهذا سيتغير بمجرد أن نبدأ ببناء منطق اللعب، ولكن هذا سيكفي حالياً.
المعرف المحدد يأخذ وسيط واحد، الكائن GCDAsyncSocket الذي سيديره.
                                                                                                                    الشكل(3): إنشاء كلاس متحكم اللعب
#import <Foundation/Foundation.h> @class GCDAsyncSocket; @interface MTGameController : NSObject #pragma mark -#pragma mark Initialization- (id)initWithSocket:(GCDAsyncSocket *)socket; @end

قبل أن نبني  initWithSocket نحتاج لإنشاء خاصية لل socketأنشئ امتداد كلاس كما هو موضح بالأسفل وعرف خاصية من نوع GCDAsyncSocket اسمها socketوقم هنا بعمل استيراد لملف ال header لكلاس MTPacket وعرف TAG_HEAD و TAG_BODY لجعل العمل مع tags أسهل في  GCDAsyncSocketDelegate delegate methods. وبالطبع كلاس MTGameController يحتاج إلى التوافق مع البروتوكول النائب GCDAsyncSocketDelegate، كلاس MTGameController يحتاج للتوافق مع البروتوكول النائب GCDAsyncSocketDelegate لجعل كل شيء يعمل بالشكل المطلوب.

#import "MTGameController.h" #import "MTPacket.h" #define TAG_HEAD 0#define TAG_BODY 1 @interface MTGameController () @property (strong, nonatomic) GCDAsyncSocket *socket; @end
بناء initWithSocket موضح بالأسفل ومن المفترض أن يكون واضحاً وسهلاً. نقوم بتخزين مؤشر على ال socket في الخاصية الخاصة التي أنشأناها للتو، أعد متحكم اللعب كمندوب الsocket، وأخبر ال socket أن تبدأ بقراءة البيانات الواردة.

#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.
 - (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 أيضاً.
 - (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: method ستلعب دوراً مهماً لاحقاً، ولكن هذا سيكفي الآن. هدفنا في هذه النقطة هو الحصول على كل شيء يعمل بعد انهاء إعادة الصياغة.
وقبل أن نكمل من المهم بناء dealloc method لكلاس MTGameController كما هو موضح بالأسفل. كلما يتم عمل deallocate لمتحكم اللعب المثيل يحتاج أن يقطع الاتصال باستدعاء disconnect على المثيل GCDAsyncSocket.

- (void)dealloc {    if (_socket) {        [_socket setDelegate:nil delegateQueue:NULL];        [_socket disconnect];        _socket = nil;    }} 
الخطوة 2: إنشاء بروتوكول مندوب آخر
كلاس MTViewController سيدير متحكم اللعب ويتفاعل معه. ويعرض اللعبة ويجعل المستخدم يتفاعل معها. المثيل MTGameController و MTViewController يحتاجان للتواصل مع بعضهما وسنستخدم بروتوكول آخر لهذا الغرض. وهو غير متماثل بحيث متحكم العرض يعرف عن متحكم اللعب ولكن متحكم اللعب لا يعرف عن متحكم العرض. سنعرف بالتفصيل عن البروتوكول الجديد ولكن حتى الان يجب أن نعلم أنه يجب تنبيه متحكم العرض عند انقطاع الاتصال.
افتح الملف MTGameController.h وعرف البروتوكول النائب كما ترى في الأسفل. بالإضافة إلى ذلك يتم إنشاء خاصية لنائب متحكم اللعب.

#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

يمكننا الآن استخدام البروتوكول عن طريق إعلام مندوب متحكم اللعب في إحدى ال delegate methodsc ل GCDAsyncSocketDelegate، socketDidDisconnect:withError لنكون أكثر دقة.
 - (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];}

الخطوة 3: تحديث كلاس MTViewController
آخر جزء من إعادة الصياغة هو استخدام MTGameController. أنشئ خاصية خاصة في كلاس MTViewController، واجعل الكلاس متوافقا مع بروتوكول MTGameControllerDelegate، وقم باستيراد ملف ال header لكلاس MTGameController.


#import "MTViewController.h" #import "MTGameController.h"#import "MTHostGameViewController.h"#import "MTJoinGameViewController.h" @interface MTViewController () <MTGameControllerDelegate, MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate> @property (strong, nonatomic) MTGameController *gameController; @end

في المتحكم  didHostGameOnSocket و didJoinGameOnSocket نستدعي startGameWithSocket ونقوم بتمرير socket الاتصال الجديد.

- (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket {    NSLog(@"%s", __PRETTY_FUNCTION__);     // Start Game with Socket    [self startGameWithSocket:socket];}  - (void)controller:(MTJoinGameViewController *)controller didJoinGameOnSocket:(GCDAsyncSocket *)socket {    NSLog(@"%s", __PRETTY_FUNCTION__);     // Start Game with Socket    [self startGameWithSocket:socket];}

في startGameWithSocket: helper method قمنا بتعريف مثيل من كلاس MTGameController بتمرير ال socket وتخزين مؤشر لمتحكم اللعب في خاصية متحكم العرض gameControllerمتحكم العرض أيضاً يخدم مندوب متحكم اللعب كما ناقشنا ذلك مسبقاً.

- (void)startGameWithSocket:(GCDAsyncSocket *)socket {    // Initialize Game Controller    self.gameController = [[MTGameController alloc] initWithSocket:socket];     // Configure Game Controller    [self.gameController setDelegate:self];}
في controllerDidDisconnect: delegate method لبروتوكول MTGameControllerDelegate نستدعي ال method المساعدة endgame والتي نقوم فيها بمسح وحدة متحكم اللعب.
 - (void)controllerDidDisconnect:(MTGameController *)controller {    NSLog(@"%s", __PRETTY_FUNCTION__);     // End Game    [self endGame];}


- (void)endGame {    // Clean Up    [self.gameController setDelegate:nil];    [self setGameController:nil];}
للتأكد من أن كل شيء يعمل بالطريقة الصحيحة، لنقم بفتح ملف XIB ل MTViewController ونضيف زر آخر في أعلى اليسار ونسميه Disconnect كما في (الشكل 4). يستطيع المستخدم النقر على هذا الزر عندما يريد مغادرة اللعبة. نقوم بعرض هذا الزر فقط عند الحصول على اتصال. عندما يكون الاتصال فعال نقوم بإخفاء الأزرار الخاصة بالاستضافة والانضمام إلى اللعب host and join. عدل في ملف MTViewcontroller.xib كم في (الشكل 4)، أنشئ منفذ لكل زر في MTViewController.h وصل المنافذ في MTViewcontroller.xib.
 #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 وصله بزر ال Disconnect.

- (IBAction)disconnect:(id)sender {    [self endGame];}

الشكل(4) تعديل واجهة المستخدم

في setupGameWithSocket: method نقوم بإخفاء زر hostButton وكذلك زر joinButton ونظهر زر disconnectButtonفي endGame method نفعل العكس تماماً. كما أننا نخفي disconnectButton من viewDidLoad method في متحكم اللعب.

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


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

- (void)viewDidLoad {    [super viewDidLoad];     // Hide Disconnect Button    [self.disconnectButton setHidden:YES];}

لفحص ما إذا كان كل شيء يعمل بشكل جيد سنقوم بإرسال حزمة للفحص كما فعلنا مسبقاً في هذا المقال. عرف method اسمها testConnection في ملف MTGameController.h وقم ببنائها كما ترى بالأسفل.

- (void)testConnection;
 - (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];} 
متحكم اللعب يجب أن يستدعي هذه الmethod بمجرد الحصول على اتصال. وأفضل مكان لعمل هذا في controller:didHostGameOnSocket: delegate method بعد أن يتم تعريف متحكم اللعب.
 - (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket {    NSLog(@"%s", __PRETTY_FUNCTION__);     // Start Game with Socket    [self startGameWithSocket:socket];     // Test Connection    [self.gameController testConnection];}

شغل التطبيق مرة أخرى لتتأكد أن كل شيء يعمل على ما يرام.

4- التنظيف أو المسح
الآن هو الوقت لتنظيف كلاس MTHostGameViewController و MTJoinGameViewController بالتخلص من أي كود غير لازم، وهذا يعني إزالة sendPacket: method من كلاس MTHostGameViewController، وإزالة socket:didReadData:withTag: method من بروتوكول CocoaAsyncSocketDelegate بالإضافة لإزالة ال methods المساعدة code>parseHeader و parseBody.

الملخص

أستطيع تخيل أن هذا المقال لم يكن سهلاً ففيه العديد من العمليات. مع ذلك أود أن أؤكد أن الصعوبة هنا تكمن في بناء التطبيق نفسه وليس في استخدام بروتوكول Bonjour ومكتبة CocoaAsyncSocket. وأعتقد أنه تحدياً كبيراً للمهندس أن يبني تطبيق بهذه الطريقة بحيث تقلل من التبعيات ويبقي التطبيق يعمل بشكل جيد وكفاءة عالية، وهذا هو تحديداً ما دفعنا لإعادة صياغة بناءنا الأصلي لمنطق الشبكة.

لدينا الآن متحكم عرض يهتم بعرض اللعبة للمستخدم MTViewController ومتحكم اللعب الذي يهتم بأمور الاتصال وسير اللعب MTGameController. كما ذكرت آنفاً من الممكن فصل الاتصال عن منطق اللعب من خلال إنشاء كلاس منفصل لكل منهما، ولكن هذا لم يكن ضرورياً في هذا التطبيق البسيط.
الاستنتاج
لقد حققنا تقدم كبير من خلال مشروعنا هذا، ولكن هناك شيء واحد مفقود الآن وهو اللعبة! في المرة القادمة من هذه السلسلة سننشئ اللعبة والاستفادة من الأساس الذي قمنا به حتى الآن.

الكــاتــب

    • مشاركة

ليست هناك تعليقات:

جميع الحقوق محفوظة لــ الشبح للمعلوميات 2019 ©