نصنع رسولا * يعمل حتى في المصعد

* في الواقع ، سنكتب فقط النموذج الأولي للبروتوكول.

ربما واجهت موقفًا مشابهًا - الجلوس في الرسول المفضل لديك ، والدردشة مع الأصدقاء ، والدخول في المصعد / النفق / النقل ، ويبدو أن الإنترنت ما زال يصطاد ، ولكن لا يمكن إرسال شيء؟ أو في بعض الأحيان يقوم موفر الاتصال الخاص بك بتهيئة الشبكة بشكل غير صحيح ويختفي 50٪ من الحزم ولا يعمل أي شيء أيضًا. ربما كنت تفكر في هذه اللحظة - حسنًا ، ربما يمكنك القيام بشيء ما بطريقة ما بحيث لا يزال بإمكانك إرسال جزء النص الذي تريده مع اتصال سيء؟ أنت لست وحدك.

مصدر الصورة

في هذه المقالة سأتحدث عن فكرتي لتطبيق بروتوكول يستند إلى UDP ، والذي يمكن أن يساعد في هذا الموقف.

مشكلات TCP / IP


عندما يكون لدينا اتصال رديء (محمول) ، تبدأ نسبة كبيرة من الحزم في الضياع (أو تذهب مع تأخير طويل جدًا) ، وقد ينظر بروتوكول TCP / IP إلى هذا كإشارة على أن الشبكة مزدحمة ، وكل شيء يبدأ في العمل sooooooooooooooobly بشكل عام. لا تضفي المزيد من السعادة على أن إنشاء اتصال (خاصة TLS) يتطلب إرسال واستقبال عدة حزم ، وحتى الخسائر الصغيرة تؤثر على تشغيلها بشكل سيء للغاية. كما يتطلب غالبًا الوصول إلى DNS قبل إنشاء اتصال - بضع حزم إضافية.

الكل في الكل ، مشاكل REST API نموذجي TCP / IP المستندة إلى اتصال رديئة:

  • استجابة سيئة لفقدان الحزمة (تقليل حاد للسرعة ، مهلات كبيرة)
  • يتطلب إنشاء اتصال تبادل حزم (+3 حزم)
  • غالبًا ما تحتاج إلى استعلام DNS "إضافي" لمعرفة خادم IP (+2 حزم)
  • غالبًا ما تحتاج إلى TLS (+2 حزم كحد أدنى)

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

فكرة التنفيذ


الفكرة هي: نحتاج فقط إلى إرسال حزمة UDP واحدة إلى عنوان IP الخاص بالسلك مسبقًا للخادم مع بيانات الترخيص ونص الرسالة الضرورية ، والحصول على إجابة عليه. يمكن تشفير جميع البيانات بالإضافة إلى ذلك (هذا ليس في النموذج الأولي). إذا لم تصل الإجابة خلال ثانية ، فإننا نعتقد أن الطلب قد فقد ونحاول إرساله مرة أخرى. يجب أن يكون الخادم قادراً على إزالة الرسائل المكررة ، لذا يجب ألا تؤدي إعادة الإرسال إلى إنشاء مشاكل.

المزالق المحتملة للتنفيذ جاهزة للإنتاج


فيما يلي (بأي حال من الأحوال) أشياء تحتاج إلى التفكير فيها قبل استخدام شيء مثل هذا في ظروف "القتال":

  1. يمكن "قطع" UDP بواسطة الموفر - يجب أن تكون قادرًا على العمل عبر TCP / IP
  2. UDP ليست سهلة مع NAT - عادة ما يكون هناك القليل من الوقت (~ 30 ثانية) للرد على طلب العميل
  3. يجب أن يكون الخادم مقاومًا لهجمات - يجب عليك التأكد من أن حزمة الاستجابة ليست أكثر من حزمة الطلب
  4. يكون التشفير صعبًا ، وإذا لم تكن خبيرًا في الأمن ، فلديك فرصة ضئيلة في تنفيذه بشكل صحيح
  5. إذا قمت بتعيين الفاصل الزمني لإعادة الإرسال بشكل غير صحيح (على سبيل المثال ، بدلاً من المحاولة مرة أخرى كل ثانية ، والمحاولة مرة أخرى دون توقف) ، فيمكنك فعل ما هو أسوأ بكثير من TCP / IP
  6. قد يبدأ عدد أكبر من الزيارات في الوصول إلى الخادم الخاص بك بسبب نقص الملاحظات في UDP وإعادة المحاولات التي لا تنتهي
  7. يمكن أن يحتوي الخادم على عدة عناوين IP ، ويمكن أن يتغير مع مرور الوقت ، لذلك يجب أن تكون قادرًا على تحديث ذاكرة التخزين المؤقت (Telegram يعمل جيدًا :))

التنفيذ


سنكتب خادمًا يرسل ردًا عبر UDP وسنرسل في الرد رقم الطلب الذي وصل إليه (يشبه الطلب "نص رسالة الطلب") ، بالإضافة إلى الطابع الزمني لتلقي الاستجابة:

//  Go. //      buf := make([]byte, maxUDPPacketSize) //   UDP addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("0.0.0.0:%d", serverPort)) conn, _ := net.ListenUDP("udp", addr) for { //   UDP,      n, uaddr, _ := conn.ReadFromUDP(buf) req := string(buf[0:n]) parts := strings.SplitN(req, " ", 2) //          curTs := time.Now().UnixNano() clientTs, _ := strconv.Atoi(parts[0]) //       -      //   conn.WriteToUDP([]byte(fmt.Sprintf("%d %d", curTs, clientTs)), uaddr) } 

الآن الجزء الصعب هو العميل. سنرسل رسائل واحدة تلو الأخرى وننتظر حتى يرد الخادم قبل إرسال التالي. سوف نرسل الطابع الزمني الحالي وقطعة من النص - الطابع الزمني سيكون بمثابة معرف الطلب.

 //   addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", serverIP, serverPort)) conn, _ := net.DialUDP("udp", nil, addr) //  UDP      ,     . resCh := make(chan udpResult, 10) go readResponse(conn, resCh) for i := 0; i < numMessages; i++ { requestID := time.Now().UnixNano() send(conn, requestID, resCh) } 

رمز الميزات:

 func send(conn *net.UDPConn, requestID int64, resCh chan udpResult) { for { //     ,       . conn.Write([]byte(fmt.Sprintf("%d %s", requestID, testMessageText))) if waitReply(requestID, time.After(time.Second), resCh) { return } } } //   ,  . //      ,   ,   // ,        , //   . func waitReply(requestID int64, timeout <-chan time.Time, resCh chan udpResult) (ok bool) { for { select { case res := <-resCh: if res.requestTs == requestID { return true } case <-timeout: return false } } } //    type udpResult struct { serverTs int64 requestTs int64 } //           . func readResp(conn *net.UDPConn, resCh chan udpResult) { buf := make([]byte, maxUDPPacketSize) for { n, _, _ := conn.ReadFromUDP(buf) respStr := string(buf[0:n]) parts := strings.SplitN(respStr, " ", 2) var res udpResult res.serverTs, _ = strconv.ParseInt(parts[0], 10, 64) res.requestTs, _ = strconv.ParseInt(parts[1], 10, 64) resCh <- res } } 

قمت أيضًا بتنفيذ نفس الشيء على أساس REST القياسي (أكثر أو أقل): باستخدام HTTP POST ، نرسل نفس requestTs ونص الرسالة وننتظر الرد ، ثم ننتقل إلى الرد التالي. تم تقديم الطعن من خلال اسم النطاق ، ولم يتم حظر تخزين DNS المؤقت في النظام. لم يتم استخدام HTTPS لجعل المقارنة أكثر صدقًا (لا يوجد تشفير في النموذج الأولي). تم تعيين المهلة في 15 ثانية: TCP / IP لديه بالفعل إعادة توجيه الحزم المفقودة ، وعلى الأرجح لن ينتظر المستخدم أكثر من 15 ثانية.

اختبار ، النتائج


عند اختبار النموذج الأولي ، تم قياس الأشياء التالية (جميعها بالمللي ثانية ):

  1. وقت الاستجابة الأول (الأول)
  2. متوسط ​​زمن الاستجابة (متوسط)
  3. وقت الاستجابة الأقصى (الحد الأقصى)
  4. H / U - نسبة "وقت HTTP" / "وقت UDP" - كم مرة أقل تأخير عند استخدام UDP

تم تقديم 100 سلسلة من 10 طلبات - نحن نحاكي موقفًا عندما تحتاج إلى إرسال بضع رسائل فقط وبعد توفر الإنترنت العادي (على سبيل المثال ، شبكة Wi-Fi في المترو أو 3G / LTE في الشارع).

أنواع الاتصالات التي تم اختبارها:


  1. ملف تعريف "الشبكة السيئة للغاية" (خسارة 10٪ ، زمن وصول 500 مللي ثانية ، 1 ميجابت في الثانية) في مكيف ارتباط الشبكة - "سيء جدًا"
  2. EDGE ، هاتف في الثلاجة ("مصعد") - ثلاجة
  3. إيدج
  4. 3G
  5. LTE
  6. واي فاي

النتائج (الوقت بالميلي ثانية):




( نفس الشيء بتنسيق CSV )

الاستنتاجات


فيما يلي الاستنتاجات التي يمكن استخلاصها من النتائج:

  1. بصرف النظر عن الحالة الشاذة LTE ، يكون الفرق في إرسال الرسالة الأولى أكبر ، والأسوأ من ذلك (في المتوسط ​​، أسرع بمعدل 2-3 مرات)
  2. الإرسال اللاحق للرسائل في HTTP ليس أبطأ بكثير - في المتوسط ​​1.3 مرة أبطأ ، ولكن على شبكة Wi-Fi مستقرة لا يوجد فرق على الإطلاق
  3. يكون زمن الاستجابة المستند إلى UDP أكثر استقرارًا بكثير ، والذي يتم رؤيته بشكل غير مباشر من خلال الكمون الأقصى - كما أنه أقل من 1.4 إلى 1.8 مرة

بمعنى آخر ، في ظل الظروف ("السيئة") المناسبة ، سيعمل بروتوكولنا بشكل أفضل ، خاصة عند إرسال الرسالة الأولى (غالبًا ما يكون هذا هو كل ما يلزم إرساله).

تنفيذ النموذج الأولي


تم نشر النموذج الأولي على جيثب . لا تستخدمه في الإنتاج!

أمر بدء تشغيل العميل على الهاتف أو الكمبيوتر:
 instant-im -client -num 10 
. الخادم لا يزال قيد التشغيل :). من الضروري أن ننظر أولاً وقبل كل شيء في وقت الإجابة الأولى ، وكذلك في أقصى تأخير. تتم طباعة كل هذه البيانات في النهاية.

مثال على إطلاق المصعد


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


All Articles