الدردشة على دائرة الرقابة الداخلية: باستخدام مآخذ


الصورة التي أنشأتها rawpixel.com

في هذا المنشور ، سننتقل إلى طبقة TCP ، ونتعرف على مآخذ وأدوات Core Foundation باستخدام مثال تطوير تطبيق الدردشة.

يقدر وقت القراءة: 25 دقيقة.

لماذا مآخذ؟


قد تتساءل: "لماذا يجب أن أخفض مستوى واحد عن URLSession ؟" إذا كنت ذكيًا بما فيه الكفاية ولم تطرح هذا السؤال ، فانتقل مباشرةً إلى القسم التالي.

الجواب ليست ذكية جدا
سؤال رائع! الحقيقة هي أن استخدام URLSession يعتمد على بروتوكول HTTP ، أي أن الاتصال في نمط استجابة الطلب ، كما يلي تقريبًا:

  • طلب من الخادم بعض البيانات بتنسيق JSON
  • الحصول على هذه البيانات ، العملية ، العرض ، إلخ.

ولكن ماذا لو كنا بحاجة إلى خادم بمبادرة منه لنقل البيانات إلى التطبيق الخاص بك؟ هنا HTTP عاطل عن العمل.

بالطبع ، يمكننا سحب الخادم باستمرار ومعرفة ما إذا كانت هناك بيانات لنا (الملقب الاقتراع ). أو يمكننا أن نكون أكثر تطوراً ونستخدم الاقتراع الطويل . لكن كل هذه العكازات غير ملائمة قليلاً في هذه الحالة.

بعد كل شيء ، لماذا تقتصر على نموذج طلب - استجابة إذا كان يناسب مهمتنا أقل قليلا من لا شيء؟

في هذا الدليل ، سوف تتعلم كيفية الغوص في مستوى أقل من التجريد واستخدام SOCKETS مباشرة في تطبيق الدردشة.

بدلاً من التحقق من الخادم بحثًا عن رسائل جديدة ، سيستخدم تطبيقنا التدفقات التي تظل مفتوحة أثناء جلسة الدردشة.

الابتداء


قم بتنزيل المواد المصدر . هناك تطبيق عميل وهمية وخادم بسيط مكتوب في الذهاب .

لا يلزمك الكتابة في Go ، لكن ستحتاج إلى تشغيل تطبيق الخادم حتى تتمكن تطبيقات العميل من الاتصال به.

قم بتشغيل تطبيق الخادم


المواد المصدر لها كل من التطبيق المترجم والمصدر. إذا كان لديك جنون العظمة الصحي ولا تثق في التعليمات البرمجية المترجمة الخاصة بشخص آخر ، فيمكنك ترجمة التعليمات البرمجية المصدر بنفسك.

إذا كنت شجاعًا ، فافتح "المحطة الطرفية" ، انتقل إلى الدليل باستخدام المواد التي تم تنزيلها وقم بتشغيل الأمر:

sudo ./server

عند المطالبة ، أدخل كلمة المرور الخاصة بك. بعد ذلك يجب أن ترى رسالة

الاستماع على 127.0.0.1:80.

ملاحظة: يبدأ تطبيق الخادم في الوضع المميز (الأمر "sudo") لأنه يستمع إلى المنفذ 80. تتطلب جميع المنافذ التي بها أرقام أقل من 1024 وصولًا خاصًا.

خادم الدردشة الخاص بك جاهز! يمكنك الذهاب إلى القسم التالي.

إذا كنت ترغب في تجميع شفرة مصدر الخادم بنفسك ،
ثم في هذه الحالة ، تحتاج إلى تثبيت Go باستخدام Homebrew .

إذا لم يكن لديك Homebrew ، فأنت بحاجة إلى تثبيته أولاً. افتح المحطة الطرفية والصق السطر التالي هناك:

/usr/bin/ruby -e \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


ثم استخدم هذا الأمر لتثبيت Go:

brew install go

في النهاية ، انتقل إلى الدليل الذي يحتوي على مواد المصدر التي تم تنزيلها وقم بترجمة الكود المصدري لتطبيق الخادم:

go build server.go

أخيرًا ، يمكنك تشغيل الخادم بالأمر في بداية هذا القسم .

نحن ننظر إلى ما لدينا في العميل


الآن افتح مشروع DogeChat ، وقم بتجميعه واعرف ما هو موجود.



كما ترون ، يسمح لك DogeChat الآن بإدخال اسم مستخدم والانتقال إلى قسم الدردشة نفسه.

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

إنشاء غرفة دردشة


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

نظرًا لأن لدينا ChatRoomViewController ، فمن المنطقي تطوير فئة ChatRoom التي ستقوم بجميع الأعمال القاسية.

دعونا نفكر فيما ستوفره الطبقة الجديدة:

  • فتح اتصال لتطبيق الخادم ؛
  • توصيل مستخدم بالاسم المحدد من قبله للدردشة ؛
  • إرسال واستقبال الرسائل ؛
  • إغلاق الاتصال في النهاية.

الآن وبعد أن عرفنا ما نريده من هذه الفئة ، اضغط على Command-N ، وحدد Swift File واسمه ChatRoom .

خلق I / O تيارات


استبدال محتويات ChatRoom.swift بهذا:

 import UIKit class ChatRoom: NSObject { //1 var inputStream: InputStream! var outputStream: OutputStream! //2 var username = "" //3 let maxReadLength = 4096 } 

هنا نحدد فئة ChatRoom ونعلن الخصائص التي نحتاجها.

  1. أولاً نحدد تيارات الإدخال / الإخراج. سيسمح لنا استخدامها كزوج بإنشاء اتصال مقبس بين التطبيق وخادم الدردشة. بالطبع ، سوف نرسل الرسائل باستخدام دفق الإخراج ، ونتلقى باستخدام دفق الإدخال.
  2. بعد ذلك ، نحدد اسم المستخدم.
  3. وأخيرًا ، نحدد المتغير maxReadLength ، والذي يحد من الحد الأقصى لطول الرسالة الواحدة.

انتقل الآن إلى ملف ChatRoomViewController.swift وأضف هذا السطر إلى قائمة خصائصه:

 let chatRoom = ChatRoom() 

الآن وقد أنشأنا البنية الأساسية للفصل الدراسي ، فقد حان الوقت للقيام بأول المهام المخططة: فتح الاتصال بين التطبيق والخادم.

اتصال مفتوح


نعود إلى ChatRoom.swift وأضف هذا الأسلوب لتعريفات الملكية:

 func setupNetworkCommunication() { // 1 var readStream: Unmanaged<CFReadStream>? var writeStream: Unmanaged<CFWriteStream>? // 2 CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, "localhost" as CFString, 80, &readStream, &writeStream) } 

إليك ما نفعله هنا:

  1. أولاً نحدد متغيرين لتدفقات المقبس دون استخدام الإدارة التلقائية للذاكرة
  2. ثم ، نحن باستخدام هذه المتغيرات نفسها ، نقوم بإنشاء تدفقات مباشرة مرتبطة بالمضيف ورقم المنفذ.

وظيفة لديه أربع حجج. الأول هو نوع مخصص الذاكرة الذي سنستخدمه عند تهيئة مؤشرات الترابط. يجب عليك استخدام kCFAllocatorDefault ، على الرغم من أن هناك خيارات أخرى ممكنة في حال كنت ترغب في تغيير سلوك مؤشرات الترابط.

ملاحظة المترجم
تقول وثائق الدالة CFStreamCreatePairWithSocketToHost : استخدم NULL أو kCFAllocatorDefault . ووصف kCFAllocatorDefault يقول أنه مرادف لـ NULL . الدائرة مغلقة!

ثم وضعنا اسم المضيف. في حالتنا ، نحن نتصل بالخادم المحلي. إذا كان الخادم الخاص بك موجودًا في مكان آخر ، فيمكنك تعيين عنوان IP الخاص به.

ثم رقم المنفذ الذي يستمع إليه الخادم.

أخيرًا ، نقوم بتمرير مؤشرات إلى تدفقات الإدخال / الإخراج الخاصة بنا بحيث يمكن للوظيفة تهيئة وتوصيلها بالتيارات التي تنشئها.

الآن وبعد أن أصبح لدينا التدفقات الأولية ، يمكننا حفظ الروابط إليها عن طريق إضافة هذه الخطوط في نهاية طريقة setupNetworkCommunication () :

 inputStream = readStream!.takeRetainedValue() outputStream = writeStream!.takeRetainedValue() 

إن استخدام takeRetainedValue () كما هو مطبق على كائن غير مُدار يسمح لنا بالحفاظ على مرجع إليه ، وفي الوقت نفسه ، نتجنب تسرب الذاكرة في المستقبل. الآن يمكننا استخدام مؤشرات الترابط الخاصة بنا أينما نريد.

نحتاج الآن إلى إضافة سلاسل العمليات هذه إلى حلقة التشغيل بحيث يعالج تطبيقنا أحداث الشبكة بشكل صحيح. للقيام بذلك ، أضف هذين الخطين في نهاية setupNetworkCommunication () :

 inputStream.schedule(in: .current, forMode: .common) outputStream.schedule(in: .current, forMode: .common) 

وأخيرا حان الوقت للإبحار! للبدء ، أضف هذا في نهاية طريقة setupNetworkCommunication () :

 inputStream.open() outputStream.open() 

الآن لدينا اتصال مفتوح بين عملائنا وتطبيق الخادم.

يمكننا تجميع تطبيقنا وتشغيله ، لكنك لن ترى أي تغييرات حتى الآن ، لأنه في حين أننا لا نفعل أي شيء من خلال اتصال عميل الخادم الخاص بنا.

الاتصال للدردشة


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

هذا يقودنا إلى استنتاج مهم: نظرًا لأن لدينا نوعين من الرسائل ، نحتاج إلى التمييز بينهما بطريقة أو بأخرى.

بروتوكول الدردشة


تتمثل إحدى مزايا استخدام طبقة TCP في أنه يمكننا تحديد "بروتوكول" الاتصال الخاص بنا.

إذا استخدمنا HTTP ، فسنحتاج إلى استخدام هذه الكلمات المختلفة GET ، PUT ، PATCH . سنحتاج إلى تكوين عناوين URL واستخدام الرؤوس الصحيحة وكل ذلك.

لدينا نوعان فقط من الرسائل. سوف نرسل

iam:Luke

لدخول الدردشة وتقديم نفسك.

وسوف نرسل

msg:Hey, how goes it, man?

لإرسال رسالة دردشة لجميع المجيبين.

الأمر بسيط للغاية ، لكنه غير مبدئي تمامًا ، لذلك لا تستخدم هذه الطريقة في المشروعات المهمة.

الآن نحن نعرف ما يتوقعه الخادم الخاص بنا ويمكننا كتابة طريقة في فئة ChatRoom تسمح للمستخدم بالاتصال بالدردشة. الوسيطة الوحيدة هي لقب المستخدم.

أضف هذه الطريقة داخل ChatRoom.swift :

 func joinChat(username: String) { //1 let data = "iam:\(username)".data(using: .utf8)! //2 self.username = username //3 _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } //4 outputStream.write(pointer, maxLength: data.count) } } 

  1. أولاً ، نشكل رسالتنا باستخدام "البروتوكول" الخاص بنا
  2. حفظ اسم للرجوع إليها في المستقبل.
  3. يوفر withUnsafeBytes (_ :) طريقة ملائمة للعمل مع مؤشر غير آمن داخل الإغلاق.
  4. وأخيرا ، نرسل رسالتنا إلى تيار الإخراج. قد يبدو هذا أكثر تعقيدًا مما تتوقع ، ولكن الكتابة (_: maxLength :) تستخدم المؤشر غير الآمن الذي تم إنشاؤه في الخطوة السابقة.

الآن طريقتنا جاهزة ، افتح ChatRoomViewController.swift وأضف مكالمة إلى هذه الطريقة في نهاية viewWillAppear (_ :) .

 chatRoom.joinChat(username: username) 

الآن تجميع وتشغيل التطبيق. أدخل اسم الشهرة الخاص بك وانقر على العودة لرؤية ...



... مرة أخرى لم يتغير شيء!

الانتظار ، كل شيء على مايرام! انتقل إلى نافذة المحطة. هناك سترى الرسالة التي انضم إليها Vasya أو شيء من هذا القبيل إذا كان اسمك ليس Vasya.

هذا شيء رائع ، لكن سيكون من الجيد الحصول على إشارة إلى اتصال ناجح على شاشة هاتفك.

الرد على الرسائل الواردة


يرسل الخادم رسائل انضمام العميل إلى كل من هو في الدردشة ، بما في ذلك أنت. لحسن الحظ ، تطبيقنا لديه بالفعل كل شيء لعرض أي رسائل واردة في شكل خلايا في جدول الرسائل في ChatRoomViewController .

كل ما عليك القيام به هو استخدام inputStream "للقبض" على هذه الرسائل ، وتحويلها إلى مثيلات فئة الرسائل ، ونقلها إلى الجدول للعرض.

لتتمكن من الرد على الرسائل الواردة ، تحتاج إلى ChatRoom للامتثال لبروتوكول StreamDelegate .

للقيام بذلك ، قم بإضافة هذا الملحق في أسفل ملف ChatRoom.swift :

 extension ChatRoom: StreamDelegate { } 

أعلن الآن من سيصبح مفوضًا إلى inputStream.

أضف هذا السطر إلى أسلوب setupNetworkCommunication () قبل إجراء المكالمات لجدولة (في: forMode :):

 inputStream.delegate = self 

أضف الآن الدفق (_: handle :) method method to extension:

 func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: print("new message received") case .endEncountered: print("The end of the stream has been reached.") case .errorOccurred: print("error occurred") case .hasSpaceAvailable: print("has space available") default: print("some other event...") } } 

نحن نعالج الرسائل الواردة


لذلك ، نحن مستعدون لبدء معالجة الرسائل الواردة. الحدث الذي يهمنا هو .hasBytesAvailable ، مما يشير إلى أن رسالة واردة قد وصلت.

سنكتب طريقة تعالج هذه الرسائل. أدناه الطريقة المضافة حديثًا ، نكتب ما يلي:

 private func readAvailableBytes(stream: InputStream) { //1 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength) //2 while stream.hasBytesAvailable { //3 let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength) //4 if numberOfBytesRead < 0, let error = stream.streamError { print(error) break } // Construct the Message object } } 

  1. نضع المخزن المؤقت الذي سنقرأ فيه البايتات الواردة.
  2. نحن ندور في حلقة ، بينما في تدفق المدخلات هناك شيء للقراءة.
  3. نسمي قراءة (_: maxLength :) ، الذي يقرأ البايتات من الدفق ويضعها في المخزن المؤقت.
  4. إذا أعادت المكالمة قيمة سالبة ، فإننا نرجع خطأ وإنهاء الحلقة.

نحتاج إلى استدعاء هذه الطريقة بمجرد توفر البيانات في الدفق الوارد ، لذلك انتقل إلى بيان التبديل داخل الدفق (_: handle :) ، ابحث عن رمز التبديل .hasBytesAvailable واتصل بهذه الطريقة فور بيان الطباعة:

 readAvailableBytes(stream: aStream as! InputStream) 

في هذا المكان لدينا مخزن مؤقت أعد للبيانات المستلمة!

لكننا ما زلنا بحاجة لتحويل هذا المخزن المؤقت إلى محتويات جدول الرسائل.

ضع هذه الطريقة على readAvailableBytes (دفق :) .

 private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>, length: Int) -> Message? { //1 guard let stringArray = String( bytesNoCopy: buffer, length: length, encoding: .utf8, freeWhenDone: true)?.components(separatedBy: ":"), let name = stringArray.first, let message = stringArray.last else { return nil } //2 let messageSender: MessageSender = (name == self.username) ? .ourself : .someoneElse //3 return Message(message: message, messageSender: messageSender, username: name) } 

أولاً ، نهيئ السلسلة باستخدام المخزن المؤقت والحجم الذي نمرره لهذه الطريقة.

سيكون النص باللغة UTF-8 ، في النهاية سنقوم بتحرير المخزن المؤقت ، ونقسم الرسالة على الرمز ":" لفصل اسم المرسل والرسالة نفسها.

الآن نحن نحلل ما إذا كانت هذه الرسالة جاءت من مشارك آخر. على المنتج ، يمكنك إنشاء شيء مثل رمز مميز فريد ، وهذا يكفي للعرض التجريبي.

أخيرًا ، من كل هذا الاقتصاد ، نشكل مثيلًا للرسالة ونعيدها.

لاستخدام هذه الطريقة ، أضف ما يلي في نهاية الحلقة في حالة readAvailableBytes (دفق :) ، مباشرة بعد آخر تعليق:

 if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) { // Notify interested parties } 

الآن كل شيء جاهز لنقل رسالة إلى شخص ما ... ولكن إلى من؟

إنشاء بروتوكول ChatRoomDelegate


لذلك ، نحتاج إلى إبلاغ ChatRoomViewController.swift بالرسالة الجديدة ، لكن ليس لدينا رابط إليها. نظرًا لأنه يحتوي على ارتباط ChatRoom قوي ، يمكننا الوقوع في فخ دورة ارتباط قوي.

هذا هو المكان المثالي لإنشاء بروتوكول المفوض. لا يهتم ChatRoom بمن يحتاج إلى معرفة المشاركات الجديدة.

في الجزء العلوي من ChatRoom.swift ، أضف تعريف بروتوكول جديد:

 protocol ChatRoomDelegate: class { func received(message: Message) } 

الآن داخل فئة ChatRoom ، أضف رابطًا ضعيفًا إلى المتجر الذي سيصبح المفوض:

 weak var delegate: ChatRoomDelegate? 

الآن ، دعونا نضيف طريقة readAvailableBytes (دفق :) ، مع إضافة السطر التالي داخل بنية if-let ، تحت التعليق الأخير في الطريقة:

 delegate?.received(message: message) 

ارجع إلى ChatRoomViewController.swift وأضف ملحق الفئة التالي ، والذي يضمن الامتثال لبروتوكول ChatRoomDelegate ، مباشرةً بعد MessageInputDelegate:

 extension ChatRoomViewController: ChatRoomDelegate { func received(message: Message) { insertNewMessageCell(message) } } 

يحتوي المشروع الأصلي بالفعل على ما يلزم ، لذا فإن insertNewMessageCell (_ :) سيقبل رسالتك ويعرض الخلية الصحيحة في عرض الجدول.

الآن قم بتعيين وحدة التحكم في طريقة العرض كمفوض عن طريق إضافة هذا إلى viewWillAppear (_ :) مباشرة بعد استدعاء super.viewWillAppear ()

 chatRoom.delegate = self 

الآن تجميع وتشغيل التطبيق. أدخل اسمًا واضغط على return.



سترى خلية حول اتصالك بالدردشة. هوراي ، لقد أرسلت رسالة بنجاح إلى الخادم وتلقيت ردًا منها!

نشر الرسائل


الآن وبعد أن أصبح بإمكان ChatRoom إرسال واستقبال الرسائل ، فقد حان الوقت لتزويد المستخدم بالقدرة على إرسال عباراته الخاصة.

في ChatRoom.swift ، أضف الطريقة التالية في نهاية تعريف الفئة:

 func send(message: String) { let data = "msg:\(message)".data(using: .utf8)! _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } outputStream.write(pointer, maxLength: data.count) } } 

تشبه هذه الطريقة joinChat (اسم المستخدم :) ، الذي كتبناه سابقًا ، باستثناء أنه يحتوي على بادئة msg أمام النص (للإشارة إلى أن هذه رسالة دردشة حقيقية).

نظرًا لأننا نرغب في إرسال الرسائل بواسطة الزر " إرسال" ، فإننا نعود إلى ChatRoomViewController.swift وابحث عن MessageInputDelegate هناك.

هنا نرى sendWasTapped فارغة (رسالة :) الأسلوب. لإرسال رسالة ، أرسلها إلى chatRoom:

 chatRoom.send(message: message) 

في الواقع ، هذا كل شيء! نظرًا لأن الخادم سيتلقى الرسالة ويعيد توجيهها إلى الجميع ، فسيتم إخطار ChatRoom بالرسالة الجديدة بنفس طريقة الانضمام إلى الدردشة.

ترجمة وتشغيل التطبيق.



إذا لم يكن لديك أي شخص للدردشة معه الآن ، فقم بتشغيل نافذة طرفية جديدة وأدخل:

nc localhost 80

هذا سوف يوصلك إلى الخادم. يمكنك الآن الاتصال بالدردشة باستخدام نفس "البروتوكول":

iam:gregg

وهكذا - أرسل رسالة:

msg:Ay mang, wut's good?



تهانينا ، لقد كتبت عميلًا للدردشة!

نحن ننظف أنفسنا


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

للقيام بذلك ، أضف الطريقة التالية إلى ChatRoom.swift بعد تعريف send (message :) :

 func stopChatSession() { inputStream.close() outputStream.close() } 

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

أضف مكالمة إلى هذه الطريقة في القسم .endEncountered في بيان التبديل داخل الدفق (_: handle :) :

 stopChatSession() 

ثم ارجع إلى ChatRoomViewController.swift وقم بنفس الشيء في viewWillDisappear (_ :) :

 chatRoom.stopChatSession() 

هذا كل شئ! الآن بالتأكيد!

استنتاج


الآن وقد أتقنت أساسيات التواصل مع المقابس ، يمكنك تعميق معرفتك.

مآخذ UDP


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

ومع ذلك ، يمكنك استخدام مآخذ UDP. هذا النوع من الاتصال لا يضمن تسليم الطرود إلى الغرض المقصود منها ، لكنه أسرع بكثير.

هذا مفيد بشكل خاص في الألعاب. من أي وقت مضى شهدت تأخر؟ هذا يعني أن لديك اتصالًا سيئًا وفقدت العديد من حزم UDP.

مآخذ الويب


بديل آخر HTTP في التطبيقات هي تقنية تسمى مآخذ الويب.

على عكس مآخذ TCP العادية ، تستخدم مآخذ الويب HTTP لتأسيس الاتصال. من خلال مساعدتهم ، يمكنك تحقيق نفس الشيء مع المقابس العادية ، ولكن مع توفير الراحة والأمان ، كما في المتصفح.

Source: https://habr.com/ru/post/ar467909/


All Articles