كما كتبت رسالتي

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

يمكن العثور على الرمز هنا .


خلفية موجزة


لمدة عام تقريبًا قبل بدء العمل في برنامج المراسلة ، كان يعمل في لعبة Line Tower Wars متعددة اللاعبين عبر الإنترنت. سارت البرمجة بشكل جيد ، كل شيء آخر (التوازن والمرئي على وجه الخصوص) لم يكن جيدًا جدًا. فجأة اتضح أن صنع لعبة وصنع لعبة ممتعة (ممتعة لشخص آخر غيره) شيئان مختلفان. بعد عام من المحنة ، كنت بحاجة إلى التشتت ، لذلك قررت أن أجرب يدي على شيء آخر. وقع الاختيار على تطوير الأجهزة المحمولة ، وهي Flutter. سمعت الكثير من الأشياء الجيدة عن Flutter ، وأعجبت بالسهام بعد تجربة قصيرة. قررت أن أكتب رسالتي. أولاً ، من الأفضل تنفيذ كل من العميل والخادم. ثانيًا ، سيكون هناك شيء مهم لوضعه في المحفظة للبحث عن عمل ، أنا فقط في هذه العملية.

الوظيفة المجدولة


  • الدردشات الخاصة والجماعية
  • إرسال النص والصور ومقاطع الفيديو
  • مكالمات الصوت والفيديو
  • تأكيد الاستلام والقراءة (علامات من Votsap)
  • "مطبوعات ..."
  • إشعارات
  • البحث عن طريق رمز الاستجابة السريعة وتحديد الموقع الجغرافي

بالنظر إلى المستقبل ، يمكنني أن أفخر بفخر (وبارتياح) بأن كل شيء تم التخطيط له تقريبًا تم تنفيذه ، ولم يتم تنفيذه بعد - سيتم تنفيذه في المستقبل القريب.



اختيار اللغة


لم أفكر لفترة طويلة في اختيار اللغة. في البداية ، كان من المغري استخدام السهام لكل من العميل والخادم ، ولكن أظهر فحص أكثر تفصيلاً أنه لا يوجد العديد من برامج تشغيل السهام المتاحة ، وأن تلك التي لا تلهم الكثير من الثقة. على الرغم من أنني لن أتحدث عن اللحظة الحالية ، فقد يكون الوضع قد تحسن. لذلك وقع اختياري على C # ، الذي عملت معه في Unity.

هندسة معمارية


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

تبدو الهندسة النهائية مثل هذا


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

فيما يلي وصف تفصيلي للمكونات الفردية.

خادم الواجهة الأمامية


حتى قبل أن أبدأ في صنع اللعبة ، كنت مفتونًا بمفهوم الخادم غير المتزامن أحادي الخيوط. بشكل فعال وبدون سباق محتمل - ماذا يمكنك أن تسأل عنه. من أجل فهم كيفية ترتيب هذه الخوادم ، بدأت في الخوض في وحدة asyncioلغة الثعبان. الحل الذي رأيته بدا أنيقًا جدًا. باختصار ، يبدو حل الكود الزائف هكذا.
//  ,      ,    
//       .      socket.Receive
//     , :
var bytesReceived = Completer<object>();
selector.Register(
    socket,
    SocketEvent.Receive,
    () => bytesReceived.Complete(null)
);

await bytesReceived.Future;

int n = socket.Receive(...); //   

// selector -     poll.   
//        (Receive 
//  ), ,    ,  .
//   completer,      ,
//        , ,     .
//     ,       .

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

يتم تنفيذ خوادم الواجهة الأمامية بهذه الطريقة. كلها كلها مترابطة وغير متزامنة. لذلك ، للحصول على أقصى أداء ، تحتاج إلى تشغيل أكبر عدد ممكن من الخوادم على جهاز واحد لأنه يحتوي على نوى (4 في الصورة).

يقرأ خادم Frontend الرسالة من العميل ويرسلها ، بناءً على رمز الرسالة ، إلى أحد الموضوعات في Kafka.

حاشية صغيرة لمن لا يعرف الكافا
, RabbitMQ. . , ( authentication backend authentication, ). ? - , (partition). , . , , . , ( , , , (headers)).

? ? . (consumer) ( consumer'), ( ) . , , , 2 , . 3 — 2. .

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

في الحالة الأكثر شيوعًا ، تحدث دورة استجابة الطلب. على سبيل المثال ، بالنسبة لطلب التسجيل ، نحتاج فقط لإعطاء العميل إجابة ( Success،EmailAlreadyInUse، إلخ). ولكن بالنسبة لرسالة تحتوي على دعوة إلى دردشة حالية للأعضاء الجدد (Vasya و Emil و Julia) ، نحتاج إلى الرد فورًا بثلاثة أنواع مختلفة من الرسائل. النوع الأول - تحتاج إلى إخطار الداعي بنتيجة العملية (فجأة حدث خطأ في الخادم). النوع الثاني - تحتاج إلى إخطار جميع الأعضاء الحاليين في الدردشة بوجود الآن مثل هؤلاء الأعضاء الجدد في الدردشة. والثالث هو إرسال دعوات إلى فازيا وإميل ويوليا.

حسنًا ، هذا لا يبدو صعبًا للغاية ، ولكن من أجل إرسال رسالة إلى أي عميل نحتاج إلى: 1) معرفة أي خادم أمامي متصل به هذا العميل (لا نختار الخادم الذي سيتصل به العميل ، يقرر الموازن بالنسبة لنا) ؛ 2) إرسال رسالة من خادم الواجهة الخلفية إلى خادم الواجهة الأمامية المطلوب ؛ 3) في الواقع ، أرسل رسالة إلى العميل.

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

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

حسنًا ، الآن عندما ينضم العميل ، تحتاج فقط إلى حفظ زوج من UserId - Frontend Server Index في مكان ما. في حالة قطع الاتصال - حذف. لهذه الأغراض ، ستفعل أي من قواعد بيانات قيمة مفتاح عديدة في الذاكرة. اخترت الفجل.

كيف تبدو في الممارسة. بادئ ذي بدء ، بعد إنشاء الاتصال ، يرسل العميل أندريه رسالة إلى الخادم Join. يتلقى خادم Frontend الرسالة ويعيد توجيهها إلى موضوع الجلسة ، مع إضافة رأس "Frontend Server" مبدئيًا: {index}. سيتلقى أحد خوادم جلسة الواجهة الخلفية رسالة ، ويقرأ رمز التفويض المميز ، ويحدد نوع المستخدم الذي انضم إليه ، ويقرأ الفهرس المُضاف بواسطة خادم الواجهة الأمامية ويكتب UserId - الفهرس إلى الفجل. اعتبارًا من هذه اللحظة ، يتم اعتبار العميل عبر الإنترنت ، والآن نعرف من خلال أي خادم أمامي (وبالتالي ، من خلال أي جزء من موضوع "خوادم الواجهة الأمامية") يمكننا "الوصول إليه" عندما يرسل عملاء آخرون رسائل إلى أندري.

* في الواقع ، العملية أكثر تعقيدًا قليلاً مما وصفته. يمكنك العثور عليه في التعليمات البرمجية المصدر.

الكود الزائف لخادم الواجهة الأمامية


// Frontend Server 6
while (true) {
    // Consume from "Frontend Servers" topic, partition 6
    var messageToClient = consumer.Consume();
    if (message != null) {
        relayMessageToClient(messageToClient);
    }

    var callbacks = selector.Poll();
    while (callbacks.TryDequeue(out callback)) {
        callback();
    }

    long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    while (!callAtQueue.IsEmpty && callAtQueue.PeekPriority() <= now) {
        callAtQueue.Dequeue()();
    }

    while (messagesToRelayToBackendServers.TryDequeue(out messageFromClient)) {
        // choose topic
        producer.Produce(topic, messageFromClient);
    }
}


هناك بعض الحيل هنا.
1) relayMessageToClient. سيكون من الخطأ أخذ المقبس الذي تريده والبدء فورًا في إرسال رسالة إليه ، لأننا ربما نرسل بالفعل بعض الرسائل الأخرى إلى العميل. إذا بدأنا في إرسال وحدات البايت دون التحقق مما إذا كان المقبس مشغولًا حاليًا ، فسيتم خلط الرسائل. كما هو الحال في العديد من الأماكن الأخرى التي تتطلب معالجة بيانات منظمة ، فإن الحيلة هي استخدام قائمة انتظار ، أي قائمة انتظار من المكمل ( TaskCompletionSourceفي C #).
void async relayMessageToClient(message) {
    // find client
    await client.ReadyToSend();
    await sendMessage(client, message);
    client.CompleteSend();
}

class Client {
    // ...
    sendMessageQueue = new LinkedList<Completer<object>>();

    async Future ReadyToSend() {
        var sendMessage = Completer<object>();
	if (sendMessageQueue.IsEmpty) {
	    sendMessageQueue.AddLast(sendMessage);
	} else {
	    var prevSendMessage = sendMessageQueue.Last;
	    sendMessageQueue.AddLast(sendMessage);
	    await prevSendMessage.Future;
	}
    }

    void CompleteSend() {
        var sendMessage = sendMessageQueue.RemoveFirst();
	sendMessage.Complete(null);
    }
}

إذا لم تكن قائمة الانتظار فارغة ، فهذا يعني أن المقبس مشغول بالفعل في الوقت الحالي. إنشاء واحدة جديدة completer، إضافته إلى قائمة الانتظار و awaitفي السابق completer . وبالتالي ، عند إرسال الرسالة السابقة ، CompleteSendستكتمل completer، مما سيتسبب في بدء الخادم في إرسال الرسالة التالية. تسمح قائمة الانتظار هذه أيضًا بنشر الاستثناءات بسلاسة. افترض حدوث خطأ أثناء إرسال رسالة إلى عميل. في هذه الحالة ، نحتاج إلى إكمال ، باستثناء إرسال هذه الرسالة ليس فقط ، ولكن أيضًا جميع الرسائل التي تنتظر حاليًا في قائمة الانتظار (انتظر await'ah). إذا لم نفعل ذلك ، فسوف تستمر في تعليق ، وسوف نتلقى تسرب للذاكرة. للإيجاز ، لا يظهر الرمز الذي يفعل ذلك هنا.

2)selector.Poll. في الواقع ، إنها ليست خدعة ، ولكنها مجرد محاولة لتذليل أوجه القصور في تنفيذ الطريقة Socket.Select( selector- مجرد التفاف على هذه الطريقة). اعتمادًا على نظام التشغيل تحت غطاء المحرك ، تستخدم هذه الطريقة إما selectأو poll. لكن هذا ليس مهما هنا. الشيء المهم هو كيف تعمل هذه الطريقة مع القوائم التي نطعمها للمدخلات (قائمة المقابس للقراءة والكتابة والتحقق من الأخطاء). تأخذ هذه الطريقة القوائم واستطلاعات الرأي وتترك فقط تلك المآخذ في القوائم الجاهزة لأداء العملية المطلوبة. يتم طرح كافة المقابس الأخرى من القوائم. "الركل" يحدث من خلالRemoveAt(أي ، يتم تحويل جميع العناصر اللاحقة ، وهو غير فعال). بالإضافة إلى ذلك ، نظرًا لأننا نحتاج إلى استطلاع جميع المقابس المسجلة في كل تكرار للدورة ، فإن هذا "التطهير" ضار بشكل عام ، يتعين علينا إعادة ملء القوائم في كل مرة. يمكننا التغلب على كل هذه المشاكل باستخدام مشكلة مخصصة List، RemoveAtلا تؤدي طريقته إلى إزالة العنصر من القائمة ، ولكن ببساطة تمييزه على أنه محذوف. الفئة ListForPollingهي تنفيذي لهذه القائمة. ListForPollingيعمل فقط مع الطريقة Socket.Selectولا يناسب أي شيء آخر.

3)callAtQueue. في معظم الحالات ، يتوقع خادم الواجهة الأمامية ، بعد إرسال رسالة العميل إلى خادم الواجهة الخلفية ، استجابة (تأكيد نجاح العملية ، أو خطأ إذا حدث خطأ ما). إذا لم ينتظر إجابة خلال فترة زمنية قابلة للتكوين ، يرسل خطأ إلى العميل حتى لا ينتظر إجابة لن تأتي أبدًا. callAtQueueهو قائمة انتظار ذات أولوية. مباشرة بعد أن يرسل الخادم الرسالة إلى Kafka ، يفعل شيئًا كهذا:
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
callAtQueue.Enqueue(callback, now + config.WaitForReplyMSec);

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

لاستخدامه await Task.WhenAny(answerReceivedTask, Task.Delay(x))، حيث أن الكود بعد Task.Delayتنفيذه على الخيط من التجمع.

هنا ، في الواقع ، كل شيء عن خوادم الواجهة الأمامية. مطلوب تصحيح طفيف هنا. في الواقع ، الخادم ليس بالكاملواحد مترابطة. بالطبع الكافكا تحت غطاء محرك السيارة تستخدم خيوط ، ولكن أعني رمز التطبيق. والحقيقة أن إرسال رسالة إلى موضوع الكافكة (إنتاج) قد لا ينجح. في حالة الفشل ، يكرر كافكا إرسال عدد معين قابل للتكوين من المرات ، ولكن إذا فشلت المغادرة المتكررة ، يتخلى كافكا عن هذا العمل باعتباره ميئوسًا منه. يمكنك التحقق مما إذا كانت الرسالة قد تم إرسالها بنجاح أم لا deliveryHandlerوالتي نمر فيها إلى الطريقة Produce. يستدعي Kafka هذا المعالج في مؤشر ترابط الإدخال / الإخراج الخاص بالمنتج (الخيط الذي يرسل الرسائل). يجب أن نتأكد من إرسال الرسالة بنجاح ، وإذا لم يكن الأمر كذلك ، فقم بإلغاء انتظار الرد من خادم الواجهة الخلفية (لن تأتي الاستجابة لأن الطلب لم يتم إرساله) وأرسل خطأ إلى العميل. أي أنه لا يمكننا تجنب التفاعل مع موضوع آخر.

* عند كتابة مقال ، أدركت فجأة أنه لا يمكننا المرور deliveryHandlerإلى الطريقة Produceأو ببساطة تجاهل جميع أخطاء الكافكا (سيستمر إرسال الخطأ إلى العميل بحلول المهلة التي وصفتها سابقًا) - عندئذٍ ستكون جميع شفراتنا مترابطة. أفكر الآن في كيفية القيام بذلك بشكل أفضل.

لماذا ، في الواقع ، كافكا ، وليس أرنب؟
, , , , , RabbitMQ? . , , . ? , frontend . , backend , , . , , . , error-prone. , basicGet , , , . . basicGet, , . .


خادم الخلفية


مقارنة بخادم الواجهة الأمامية ، لا توجد نقاط مثيرة للاهتمام هنا. تعمل جميع خوادم الواجهة الخلفية بنفس الطريقة. عند بدء التشغيل ، يشترك الخادم في الموضوع (المصادقة أو الجلسة أو المكالمة بناءً على الدور) ، وتعين الكافكا قسمًا واحدًا أو أكثر إليه. يستقبل الخادم الرسالة من كافكا ، ويقوم بمعالجة رسالة واحدة أو أكثر كرد على ذلك. كود حقيقي تقريبا:
void Run() {
    long lastCommitTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    while (true) {
        var consumeResult = consumer.Consume(
            TimeSpan.FromMilliseconds(config.Consumer.PollTimeoutMSec)
        );

        if (consumeResult != null) {
            var workUnit = new WorkUnit() {
                ConsumeResult = consumeResult,
            };

            LinkedList<WorkUnit> workUnits;
            if (partitionToWorkUnits.ContainsKey(consumeResult.Partition)) {
                workUnits = partitionToWorkUnits[consumeResult.Partition];
            } else {
                workUnits = partitionToWorkUnits[consumeResult.Partition] =
                    new LinkedList<WorkUnit>();
            }

            workUnits.AddLast(workUnit);

            handleWorkUnit(workUnit);
        }

	if (
            DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - lastCommitTime >=
            config.Consumer.CommitIntervalMSec
        ) {
            commitOffsets();
	    lastCommitTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
	}
    }
}

أي نوع من التعويضات لارتكابها؟
. — (offset) (0, 1 ). 0. TopicPartitionOffset. (consume) , ConsumeResult, , , TopicPartitionOffset. ?

at least once delivery, , ( ). , (commited) . , consumer 16, , 16 , , , . - consumer' consumer' , 16 + 1 ( + 1). 17 . N , .

لقد عطلت الالتزام التلقائي وألزم نفسي. هذا ضروري لأنه handleWorkUnit، حيث يتم تنفيذ معالجة الرسالة بالفعل ، فهذه async voidطريقة ، لذلك ليس هناك ما يضمن معالجة الرسالة 5 قبل الرسالة 6. يخزن كافكا إزاحة واحدة فقط (وليس مجموعة إزاحة) ، على التوالي ، قبل تنفيذ الإزاحة 6 ، نحتاج إلى التأكد من معالجة جميع الرسائل السابقة أيضًا. بالإضافة إلى ذلك ، يمكن لخادم الواجهة الخلفية أن يستهلك رسائل من عدة أقسام في نفس الوقت ، وبالتالي ، يجب التأكد من أنه يقوم بإزاحة الإزاحة الصحيحة إلى القسم المقابل. لهذا ، نستخدم خريطة تجزئة لقسم النموذج: وحدات العمل. إليك شكل الرمز commitOffsets(الرمز الحقيقي هذه المرة):
private void commitOffsets() {
    foreach (LinkedList<WorkUnit> workUnits in partitionToWorkUnits.Values) {
        WorkUnit lastFinishedWorkUnit = null;
        LinkedListNode<WorkUnit> workUnit;
        while ((workUnit = workUnits.First) != null && workUnit.Value.IsFinished) {
            lastFinishedWorkUnit = workUnit.Value;
            workUnits.RemoveFirst();
        }

        if (lastFinishedWorkUnit != null) {
            offsets.Add(lastFinishedWorkUnit.ConsumeResult.TopicPartitionOffset);
        }
    }

    if (offsets.Count > 0) {
        consumer.Commit(offsets);
        foreach (var offset in offsets) {
            logger.Debug(
                "{Identifier}: Commited offset {TopicPartitionOffset}",
                identifier,
                offset
            );
        }
        offsets.Clear();
    }
}

كما ترون ، فإننا نكرر على الوحدات ، ونعثر على آخر وحدة تم إكمالها في هذه اللحظة ، وبعد ذلك لا توجد وحدات غير مكتملة ، ونقوم بتنفيذ الإزاحة المقابلة. تسمح لنا هذه الحلقة بتجنب ارتكاب "الهولي". على سبيل المثال ، إذا كان لدينا حاليًا 4 وحدات ( 0: Finished, 1: Not Finished, 2: Finished, 3: Finished) ، يمكننا تنفيذ الوحدة رقم 0 فقط ، نظرًا لأننا إذا ارتكبنا الوحدة الثالثة على الفور ، فقد يؤدي ذلك إلى فقد الوحدة الأولى إذا مات الخادم في الوقت الحالي.
class WorkUnit {
    public ConsumeResult<Null, byte[]> ConsumeResult { get; set; }
    private int finished = 0;

    public bool IsFinished => finished == 1;

    public void Finish() {
        Interlocked.Increment(ref finished);
    }
}


handleWorkUnitكما قيل ، async voidالطريقة ، وبالتالي ، ملفوفة بالكامل try-catch-finally. في tryيدعو الخدمة اللازمة ، finallyو- workUnit.Finish().

الخدمات تافهة جدا. هنا ، على سبيل المثال ، ما هو الرمز الذي يتم تنفيذه عندما يرسل المستخدم رسالة جديدة:
private async Task<ServiceResult> createShareItem(CreateShareItemMessage msg) {
    byte[] message;
    byte[] messageToPals1 = null;
    int?[] partitions1 = null;

    //  UserId  .
    long? userId = hashService.ValidateSessionIdentifier(msg.SessionIdentifier);
    if (userId != null) {
        var shareItem = new ShareItemModel(
            requestIdentifier: msg.RequestIdentifier,
            roomIdentifier: msg.RoomIdentifier,
            creatorId: userId,
            timeOfCreation: null,
            type: msg.ShareItemType,
            content: msg.Content
        );

        //      null,
        //     .
        long? timeOfCreation = await storageService.CreateShareItem(shareItem);
        if (timeOfCreation != null) {
            //      .
            List<long> pals = await inMemoryStorageService.GetRoomPals(
                msg.RoomIdentifier
            );
            if (pals == null) {
            	//     -       .
                pals = await storageService.GetRoomPals(msg.RoomIdentifier);
                await inMemoryStorageService.SaveRoomPals(msg.RoomIdentifier, pals);
            }

            //    ,  .
            pals.Remove(userId.Value);

            if (pals.Count > 0) {
            	//  ack,  ,    
                //    .
                await storageService.CreateAck(
                    msg.RequestIdentifier, userId.Value, msg.RoomIdentifier,
                    timeOfCreation.Value, pals
                );

                // in -  UserId, out -   frontend ,
                //    .  -   -
                //   null.
                partitions1 = await inMemoryStorageService.GetUserPartitions(pals);

                List<long> onlinePals = getOnlinePals(pals, partitions1);

                //    ,       .
                //         .
                if (onlinePals.Count > 0) {
                    messageToPals1 = converterService.EncodeNewShareItemMessage(
                        userId.Value, timeOfCreation.Value, onlinePals, shareItem
                    );
                    nullRepeatedPartitions(partitions1);
                    // -         
                    // frontend ,    null' .
                }
            }

            message = converterService.EncodeSuccessfulShareItemCreationMessage(
                msg.RequestIdentifier, timeOfCreation.Value
            );
        } else {
            message = converterService.EncodeMessage(
                MessageCode.RoomNotFound, msg.RequestIdentifier
            );
        }
    } else {
        message = converterService.EncodeMessage(
            MessageCode.UserNotFound, msg.RequestIdentifier
        );
    }

    return new ServiceResult(
        message: message, //    .
        messageToPals1: messageToPals1, //  -    .
        partitions1: partitions1
    );
}


قاعدة البيانات


معظم وظائف الخدمات التي تطلبها خوادم الواجهة الخلفية هي ببساطة إضافة بيانات جديدة إلى قاعدة البيانات ومعالجة البيانات الموجودة. من الواضح أن كيفية تنظيم قاعدة البيانات وكيف نعمل عليها أمر مهم للغاية بالنسبة للرسول ، وهنا أود أن أقول أنني تعاملت مع مسألة اختيار قاعدة البيانات بعناية فائقة بعد دراسة جميع الخيارات بعناية ، ولكن هذا ليس كذلك. لقد اخترت CockroachDb للتو لأنه يعد بالكثير بأقل جهد ولديه بناء جملة متوافق مع postgres (لقد عملت مع postgres من قبل). كانت هناك أفكار حول استخدام كاساندرا ، ولكن في النهاية قررت أن أتحدث عن شيء مألوف. لم أعمل أبدًا مع كافكا ، أو مع أرنب ، أو مع Flutter و Dart ، أو مع WebRtc ، لذلك قررت عدم سحب كاساندرا أيضًا ، لأنني كنت خائفة من الغرق في مجموعة كاملة من التقنيات الجديدة بالنسبة لي.

من بين جميع أجزاء مشروعي ، فإن تصميم قاعدة البيانات هو الشيء الذي أشك فيه أكثر. لست متأكدًا من أن القرارات التي اتخذتها هي قرارات جيدة حقًا . كل شيء يعمل ، ولكن يمكن القيام به بشكل أفضل. على سبيل المثال ، هناك جداول ShareRooms (كما أسمي الدردشات) و ShareItems (كما أسمي الرسائل). لذلك يتم تسجيل جميع المستخدمين الذين يدخلون الغرفة في حقل jsonb لهذه الغرفة. هذا مريح ، ولكن من الواضح أنه بطيء جدًا ، لذلك ربما سأعيده باستخدام مفاتيح أجنبية. أو ، على سبيل المثال ، يقوم جدول ShareItems بتخزين جميع الرسائل. وهو أيضا ملائم، ولكن بما أن ShareItems هي واحدة من أكثر الجداول تحميل (ثابتة selectوinsert) ، قد يكون من المفيد إنشاء جدول جديد لكل غرفة أو شيء من هذا القبيل. يقوم Kokroach بتوزيع السجلات على العقد المختلفة ، وبناءً على ذلك ، تحتاج إلى التفكير بعناية في أي سجل سيذهب لتحقيق أقصى أداء ، لكنني لم أفعل ذلك. بشكل عام ، كما يمكن فهمه من كل ما سبق ، فإن قواعد البيانات ليست أقوى نقطة لدي. الآن أنا بشكل عام أختبر كل شيء من أجل postgres ، وليس kokroach ، نظرًا لأن الحمل أقل على جهاز العمل الخاص بي ، فهو بالفعل ضعيف جدًا من الأحمال التي ستنطلق قريبًا. لحسن الحظ ، يختلف رمز postgres و kokroach قليلاً ، لذا فإن التبديل ليس صعبًا.

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

أعتقد أن القاعدة ستخضع لتغييرات كبيرة عندما أصبح أفضل في فهم هذه المشكلة. الآن ، جدول Acks يلاحقني. في هذا الجدول ، أقوم بتخزين بيانات حول من لم يستلم بعد ومن لم يقرأ الرسالة بعد (لإظهار علامات اختيار المستخدم). من السهل إخطار المستخدم بأن رسالته قد تمت قراءتها إذا كان المستخدم متصلاً الآن ، ولكن إذا لم يكن كذلك ، فنحن بحاجة إلى حفظ هذه المعلومات لإخطار المستخدم لاحقًا. وبما أن الدردشات الجماعية متاحة ، فلا يكفي فقط تخزين العلم ، فأنت بحاجة إلى بيانات حول المستخدمين الفرديين. لذا نطلب هنا بشكل مباشر استخدام سلاسل البت (سطر واحد للمستخدمين الذين لم يتلقوا بعد ، والثاني - لأولئك الذين لم يقرؤوا بعد). وخاصة دعم kokroach bitوbit varying. ومع ذلك ، لم أحسب أبدًا كيفية تنفيذ هذا العمل ، نظرًا لأن تكوين الغرف يمكن أن يتغير باستمرار. لكي تحتفظ سلاسل البت بمعناها ، يجب أن يبقى المستخدمون في الغرفة بنفس الترتيب ، وهو أمر صعب للغاية عندما يغادر بعض المستخدمين الغرفة على سبيل المثال. هناك خيارات هنا. ربما يجدر كتابة -1 بدلاً من حذف المستخدم من حقل jsonb بحيث يتم حفظ الطلب ، أو باستخدام بعض طرق الإصدار ، حتى نعلم أن سلسلة البت هذه تشير إلى ترتيب المستخدمين ، والذي كان حينها ، وليس على الترتيب الحالي للمستخدمين. ما زلت في طور التفكير في كيفية تنفيذ هذا العمل بشكل أفضل ، ولكن في الوقت الحالي ، أولئك الذين لم يتلقوا ولم يقرأوا المستخدمين بعد هم أيضًا حقول jsonb. بالنظر إلى أن جدول Acks مكتوب مع كل رسالة ، فإن كمية البيانات كبيرة.على الرغم من أن السجل ، بالطبع ، يتم حذفه عند تلقي الرسالة وقراءتها من قبل الجميع.

رفرفة


لفترة طويلة عملت على جانب الخادم واستخدمت عملاء وحدة تحكم بسيطة للاختبار ، لذلك لم أقم حتى بإنشاء مشروع Flutter. وعندما قمت بإنشائه ، اعتقدت أن جزء الخادم كان جزءًا معقدًا ، والتطبيق على هذا النحو ، القمامة ، سأكتشف ذلك في غضون يومين. أثناء العمل على الخادم ، قمت بإنشاء Hello Worlds للرفرفة عدة مرات للتعرّف على إطار العمل ، وبما أن برنامج المراسلة لا يحتاج إلى أي واجهة مستخدم معقدة ، اعتقدت أنه جاهز تمامًا. لذا ، فإن واجهة المستخدم هي حقًا القمامة ، لكن تنفيذ الوظيفة أعطاني مشاكل (وستستمر في تقديمها ، نظرًا لأن كل شيء ليس جاهزًا).

إدارة الدولة


الموضوع الأكثر شعبية. هناك ألف طريقة لإدارة حالتك ، ويتم تغيير النهج الموصى به كل ستة أشهر. الآن التيار هو مزود. أنا شخصياً اخترت طريقتين لنفسي: كتلة و redux. Bloc (Business Logic Component) لإدارة الحالة المحلية وإعادة الاختزال لإدارة العالمية.

إن الكتلة ليست نوعًا من المكتبات (على الرغم من وجود مكتبة تقلل أيضًا من الصفيحة الورقية ، لكنني لا أستخدمها). الكتلة هي نهج قائم على التدفق. بشكل عام ، السهام لغة جميلة جدًا ، والجداول جميلة جدًا بشكل عام. جوهر هذا النهج هو أننا ندفع منطق الأعمال بأكمله إلى الخدمات ، ونتواصل بين واجهة المستخدم والخدمات من خلال وحدة تحكم توفر لنا مختلف التدفقات. هل قام المستخدم بالنقر فوق الزر "العثور على جهة اتصال"؟ باستخدامsink(الطرف الآخر من الدفق) نرسل حدثًا إلى وحدة التحكم SearchContactsEvent، وستتصل وحدة التحكم بالخدمة المطلوبة ، وتنتظر النتيجة وترجع قائمة المستخدمين مرة أخرى إلى واجهة المستخدم من خلال الدفق أيضًا. تنتظر واجهة المستخدم النتائج باستخدام StreamBuilder(القطعة التي أعيد بناؤها في كل مرة تصل بيانات جديدة إلى الدفق المشترك فيها). هذا ، في الواقع ، كل شيء. في بعض الحالات ، نحتاج إلى تحديث واجهة المستخدم دون أي تدخل من المستخدم (على سبيل المثال ، عند وصول رسالة جديدة) ، ولكن يتم ذلك أيضًا بسهولة من خلال التدفقات. في الواقع ، MVC بسيط مع تيارات ، لا سحر.

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

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

الجزء الأكثر إيلاما


ماذا علي أن أفعل إذا أرسل المستخدم رسالة ، ولكن قبل إرسالها ، فقد الاتصال بالإنترنت؟ ماذا علي أن أفعل إذا تلقى المستخدم تأكيد قراءة ، لكنه أغلق التطبيق قبل تحديث السجل المطابق في قاعدة البيانات؟ ماذا أفعل إذا دعا المستخدم صديقه إلى الغرفة ، ولكن قبل إرسال الدعوة ، نفدت بطاريته؟ هل سبق لك أن سألت أسئلة مماثلة؟ ها أنا. قبل. ولكن في عملية التنمية بدأت أتساءل. نظرًا لأن الاتصال يمكن أن يختفي في أي وقت ، ويتم إيقاف تشغيل الهاتف في أي وقت ، يجب تأكيد كل شيء . غير مسلي. لذلك ، فإن أول رسالة يرسلها العميل إلى الخادم ( Joinإذا كنت تتذكر) ليست مجرد "مرحبًا أنا متصل بالإنترنت" ،"مرحبًا ، أنا متصل بالإنترنت وهنا غرف غير مؤكدة ، وهنا أكيكس غير مؤكدة ، وهنا عمليات عضوية غرفة غير مؤكدة ، وهنا آخر رسائل مستلمة لكل غرفة . " ويستجيب الخادم بورقة مماثلة: "عندما كنت في وضع عدم الاتصال ، تم قراءة مثل هذه الرسائل الخاصة بك من قبل هؤلاء المستخدمين وهؤلاء ، ودعوا أيضًا Petya إلى هذه الغرفة ، وغادرت Sveta هذه الغرفة ، وتمت دعوتك إلى هذه الغرفة ، ولكن تحتوي هاتان الغرفتان على 40 مشاركة جديدة . " أود حقًا أن أعرف كيف تتم الأشياء المماثلة في رسل آخرين ، لأن تنفيذي لا يتألق بالنعمة.

صور


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

ThumbnailGeneratorService
class ThumbnailGeneratorService {
  SendPort _sendPort;
  final Queue<Completer<Uint8List>> _completerQueue =
      Queue<Completer<Uint8List>>();

  ThumbnailGeneratorService() {
    var receivePort = ReceivePort();
    Isolate.spawn(startWorker, receivePort.sendPort);

    receivePort.listen((data) {
      if (data is SendPort) {
        _sendPort = data;
      } else {
        var completer = _completerQueue.removeFirst();
        completer.complete(data);
      }
    });
  }

  static void startWorker(SendPort sendPort) async {
    var receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);

    receivePort.listen((imageBytes) {
      Image image = decodeImage(imageBytes);
      Image thumbnail = copyResize(image, width: min(image.width, 200));

      sendPort.send(Uint8List.fromList(encodePng(thumbnail)));
    });
  }

  Future<Uint8List> generate(Uint8List imageBytes) {
    var completer = Completer<Uint8List>();
    _completerQueue.add(completer);
    
    _sendPort.send(imageBytes);

    return completer.future;
  }
}


يتم استخدام مصادقة Firebase أيضًا ، ولكن فقط من أجل تفويض الوصول إلى تخزين Firebase (بحيث لا يمكن للمستخدم ، على سبيل المثال ، ملء صورة الملف الشخصي لشخص آخر ). يتم إجراء جميع التفويضات الأخرى من خلال خوادمي.

تنسيق الرسالة


ربما تشعر بالرعب هنا ، حيث أستخدم صفائف البايت العادية. يختفي Json لأن الكفاءة مطلوبة ، ولم أكن أعرف عن protobuf عندما بدأت. يتطلب استخدام المصفوفات الكثير من العناية لأن الفهرس خاطئ والأشياء تسوء.

أول 4 بايت هي طول الرسالة.
البايت التالي هو رمز الرسالة.
16 بايت التالية هي معرّف الطلب (uuid).
الـ 40 بايت التالية هي رمز التفويض.
ما تبقى من الرسالة .

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

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

طلب معرفموجودة في معظم الوظائف ، ولكن ليس في جميع. يقوم بوظيفتين: من خلال هذا المعرف ، يحدد العميل المراسلات بين الطلب المرسل والاستجابة المستلمة (إذا أرسل العميل الرسائل A ، B ، C بهذا الترتيب ، فهذا لا يعني أن الإجابات ستأتي أيضًا بالترتيب). الوظيفة الثانية هي تجنب التكرارات. كما ذكرنا سابقًا ، تضمن كافكا التوصيل مرة واحدة على الأقل. أي أنه في حالات نادرة ، لا يزال من الممكن تكرار الرسائل. بإضافة عمود RequestIdentifier مع قيود فريدة إلى جدول قاعدة البيانات المطلوب ، يمكننا تجنب إدراج نسخة مكررة.

رمز التفويضهو UserId (8 بايت) + 32 بايت HmacSha256 التوقيع. لا أعتقد أن الأمر يستحق استخدام Jwt هنا. Jwt أكبر بحوالي 7-8 مرات لماذا؟ ليس لدى المستخدمين أية مطالبات ، لذا فإن توقيع hmac البسيط جيد. الترخيص من خلال خدمات أخرى غير مخطط له.

مكالمات الصوت والفيديو


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

ليست موجزة للغاية حول WebRtc للمبتدئين
WebRtc — , peer-to-peer , , peer-to-peer , . - , , . , .

(peer-to-peer), , 3 ( , 3 . 3 ).

— stun . stun , — Source IP Source Port , . ? - . IP . - , , , Source IP Source Port IP - NAT [ Source IP | Source Port | Router External IP | Router Port ]. - , Dest IP Dest Port Router External IP Router Port NAT, , Source IP — Source Port , . , , , , , , NAT . stun NAT . stun Router External IP — Router Port. — . , «» NAT (NAT traversal) , NAT , stun .
* NAT , . , , WebRtc .

— turn. , , peer-to-peer . Fallback . , , , , , peer-to-peer . turn — coturn, .

— . , . , . — . . , , , :) — .

WebRtc 3 : offer, answer candidate. offer , answer, . , , , , . ( ) , .


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

عادة ما يتضمن WebRtc في شكله الخالص إرسال المرشحين إلى الطرف الآخر في أقرب وقت ممكن حتى تبدأ المفاوضات في أسرع وقت ممكن ، وهو أمر منطقي. في اختباراتي ، دائمًا ما يكون المرشحون إلى الطرف المتلقي دائمًابدأ يأتي حتى قبل أن يأتي العرض. لا يمكن تجاهل هؤلاء المرشحين "الأوائل" ، يجب تذكرهم ، لذلك عندما يصل العرض RTCPeerConnectionويتم إنشاؤه ، قم بإضافتهم إلى الاتصال. حقيقة أن المرشحين قد يبدأون في الوصول حتى قبل العرض ، بالإضافة إلى بعض الأسباب الأخرى ، تجعل تنفيذ المكالمات الكاملة مهمة غير تافهة. ماذا تفعل إذا اتصل بنا عدة مستخدمين في وقت واحد؟ سوف نتلقى مرشحين من الجميع ، وعلى الرغم من أنه يمكننا فصل المرشحين عن مستخدم من مستخدم آخر ، فإنه من غير الواضح أي المرشحين سيرفضون ، لأننا لا نعرف من سيأتي عرضه في وقت سابق. ستكون هناك أيضًا مشاكل إذا بدأ المرشحون بالقدوم إلينا ثم عرضًا في اللحظة التي نسمي أنفسنا فيها شخصًا ما.

بعد اختبار العديد من الخيارات باستخدام WebRtc عارية ، توصلت إلى استنتاج مفاده أن محاولة إجراء مكالمات في هذا النموذج ستكون مشكلة ومليئة بتسريبات الذاكرة ، لذلك قررت إضافة مرحلة أخرى إلى عملية تفاوض WebRtc. أسمي هذه المرحلة Inquire - Grant/Refuse.

الفكرة بسيطة للغاية ، لكنني استغرقت بعض الوقت للوصول إليها. يرسل المتصل حتى قبل إنشاء الدفق RTCPeerConnection(وبشكل عام قبل تنفيذ أي رمز مرتبط بـ WebRtc) رسالة عبر خادم الإشارة إلى الجانب الآخر Inquire. على الجانب المتلقي ، يتم التحقق مما إذا كان المستخدم في جلسة اتصال أخرى في الوقت الحالي ( boolحقل بسيط ). إذا كان الأمر كذلك ، فسيتم إعادة الرسالة.Refuseوبهذه الطريقة ندع المتصل يعرف أن المستخدم مشغول ، والمتلقي - أن مثل هذا الهاتف اتصل به بينما كان مشغولاً بمحادثة أخرى. إذا كان المستخدم مجانيًا حاليًا ، فهو محجوز . Inquireيتم إرسال معرف الجلسة في الرسالة ، ويتم تعيين هذا المعرّف كمعرف للجلسة الحالية . إذا كان المستخدم محجوزًا ، فسيرفض جميع Inquire/Offer/Candidateالرسائل ذات معرفات الجلسة بخلاف الحالية. بعد الحجز ، يرسل المتلقي رسالة عبر خادم الإشارة إلى المتصل Grant. تجدر الإشارة إلى أن هذه العملية غير مرئية للمستخدم المتلقي ، نظرًا لعدم وجود مكالمة حتى الآن. والشيء الرئيسي هنا هو ألا ننسى تعليق الوقت المستقطع على الجانب المتلقي. فجأة سوف نحجز جلسة ، ولن يتبع ذلك أي عرض.

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

الوضع الحالي والخطط المستقبلية


  • الدردشات الخاصة والجماعية
  • إرسال النص والصور ومقاطع الفيديو
  • مكالمات الصوت والفيديو
  • تأكيد الاستلام والقراءة
  • "مطبوعات ..."
  • إشعارات
  • البحث عن طريق رمز الاستجابة السريعة وتحديد الموقع الجغرافي


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

الإخطارات قيد التقدم ، وكذلك إرسال مقاطع الفيديو.

ما الذي يجب فعله أيضًا؟


الكثير.
أولاً ، لا توجد اختبارات. اعتاد الزملاء على كتابة الاختبارات ، لذلك ارتحت تمامًا.
ثانيًا ، لا يمكن حاليًا دعوة المستخدمين إلى دردشة حالية وترك الدردشة. رمز الخادم جاهز لذلك ، رمز العميل ليس كذلك.
ثالثًا ، إذا كانت معالجة الخطأ على الخادم أكثر أو أقل ، فلا يوجد خطأ في التعامل مع العميل. لا يكفي فقط إدخال إدخال سجل ؛ يجب إعادة محاولة العملية. الآن ، على سبيل المثال ، لم يتم تنفيذ آلية إعادة إرسال الرسائل.
رابعاً ، لا يقوم الخادم بتنفيذ الأمر ping على العميل ، لذلك لا يتم الكشف عن قطع الاتصال إذا ، على سبيل المثال ، فقد العميل الإنترنت. يتم الكشف عن قطع الاتصال فقط عندما يقوم العميل بإغلاق التطبيق.
خامساً ، لا يتم استخدام الفهارس في قاعدة البيانات.
سادسا ، التحسين. يحتوي الرمز على عدد كبير من الأماكن حيث تتم كتابة شيء مثل // @@TODO: Pool. معظم المصفوفات هي newتلك فقط . يقوم خادم الواجهة الخلفية بإنشاء العديد من الصفائف ذات الطول الثابت ، لذلك يمكنك هنا استخدام التجمع.
سابعًا ، هناك العديد من الأماكن على العميل حيث awaitينتهي الرمز ، على الرغم من أن هذا ليس ضروريًا. إرسال الصور ، على سبيل المثال ، يبدو بطيئًا لأن الشفرةawaitيحفظ الصور في نظام الملفات وينشئ صورًا مصغرة قبل عرض الرسالة ، على الرغم من أنه لا يلزم القيام بأي من ذلك. أو ، على سبيل المثال ، إذا فتحت التطبيق وأرسلت لك صورًا أثناء غيابك ، فسيكون بدء التشغيل بطيئًا ، لأنه مرة أخرى يتم تنزيل جميع هذه الصور وحفظها في النظام ، ويتم إنشاء الصور المصغرة ، وبعد ذلك فقط ينتهي بدء التشغيل ويتم طرحك من شاشة البداية على الشاشة الرئيسية. تم عمل كل هذه التكرارات awaitلتسهيل عملية التصحيح ، ولكن ، بالطبع ، تحتاج إلى التخلص من الانتظار غير الضروري قبل الإصدار.
ثامنأصبحت واجهة المستخدم الآن نصف جاهزة ، لأنني لم أقرر كيف أريد رؤيتها. لذلك ، الآن كل شيء ليس بديهيًا ، نصف الأزرار غير واضحة ما يفعلونه. وغالبًا ما لا يتم الضغط على الأزرار في المرة الأولى ، لأنها الآن مجرد رموز مع GestureDetectorحشو أو بدون حشو ، لذلك ليس من الممكن دائمًا الدخول إليها. بالإضافة إلى أنه في بعض الأماكن لا يتم إصلاح تجاوز وحدات البكسل.
تاسعاً ، أصبح من المستحيل الآن تسجيل الدخول إلى حساب ، فقط قم بالتسجيل. لذلك ، إذا قمت بإلغاء تثبيت التطبيق وإعادة تثبيته ، فلن تتمكن من تسجيل الدخول إلى حسابك :)
العاشر ، لا يتم إرسال رمز التحقق إلى البريد. الآن الرمز دائمًا هو نفسه دائمًا ، مرة أخرى لأنه من الأسهل تصحيحه.
الحاديه عشريتم انتهاك مبدأ المسؤولية الفردية في العديد من الأماكن. بحاجة إلى إعادة بناء. عادةً ما تكون الفئات المسؤولة عن التفاعل مع قاعدة البيانات (على كل من العميل والخادم) منتفخة للغاية ، لأنها تشارك في جميع عمليات قاعدة البيانات.
ثاني عشر ، يتوقع خادم الواجهة الأمامية دائمًا استجابة من خادم الواجهة الخلفية ، حتى إذا كانت الرسالة لا تعني إرسال استجابة (على سبيل المثال ، رسالة تحتوي على رمز IsTypingوبعض الرسائل المتعلقة بـ WebRtc). لذلك ، دون انتظار إجابة ، يكتب خطأ إلى وحدة التحكم ، على الرغم من أن هذا ليس خطأ.
ثالث عشر ، لا تفتح الصور الكاملة عند النقر.
مائة مليون أخماسيتم إرسال بعض الرسائل التي تحتاج إلى إرسالها على دفعات بشكل منفصل. الأمر نفسه ينطبق على بعض عمليات قاعدة البيانات. بدلاً من تنفيذ أمر واحد ، يتم تنفيذ الأوامر في حلقة باستخدام await(brr ..).
مائة مليون سدس ، بعض القيم مشفرة ، بدلاً من كونها قابلة للتكوين.
مائة وواحد مليون سابعتسجيل الدخول على الخادم هو الآن فقط على وحدة التحكم ، وعلى العميل بشكل عام ، مباشرة إلى القطعة. يوجد على الشاشة الرئيسية علامة تبويب "سجلات" ، حيث يتم إسقاط جميع السجلات الموجودة على الشاشة. الحقيقة هي أن آلة العمل الخاصة بي ترفض تشغيل كل من المحاكي وكل ما هو ضروري للخادم (kafka ، قاعدة البيانات ، الفجل وجميع الخوادم). لم ينجح الخصم مع جهاز متصل ، كل شيء تم تعليقه بإحكام في نصف الحالات ، لأن الكمبيوتر لم يتمكن من التعامل مع الأحمال. لذلك ، يجب عليك إنشاء إصدار في كل مرة ، وإسقاطه على الجهاز ، وتثبيته واختباره مثل هذا. لرؤية السجلات ، أسقطها مباشرة في الأداة. أعلم أن الانحراف ليس هناك خيار. للسبب نفسه ، العديد من الأساليب تعود Futureوawaitهم (للقبض على الاستثناء ورمي في القطعة) ، على الرغم من أنه لا ينبغي لهم. إذا نظرت إلى الشفرة ، سترى _logErrorطريقة قبيحة في العديد من الفئات التي تفعل ذلك. هذا ، بالطبع ، سيذهب أيضًا إلى سلة المهملات.
مائة مليون وثمانية ، لا أصوات.
مائة مليون وتسعة ، تحتاج إلى استخدام التخزين المؤقت أكثر.
مائة مليون عُشر ، الكثير من الشفرات المتكررة. على سبيل المثال ، تقوم العديد من الإجراءات أولاً بالتحقق من صحة الرمز المميز ، وإذا لم تكن صالحة ، فإنها ترسل خطأً. أعتقد أنك بحاجة إلى تنفيذ خط أنابيب وسيط بسيط.

والكثير من الأشياء الصغيرة ، مثل سلاسل متسلسلة بدلاً من استخدام StringBuilder"a ،Disposeليس كل مكان يسمى حيث يجب ، وهكذا دواليك. بشكل عام ، الحالة الطبيعية للمشروع قيد التطوير. كل ما سبق قابل للحل ، ولكن هناك مشكلة أساسية واحدة لم أفكر بها حتى اللحظة الأخيرة ، لأنها خرجت من رأسي - يجب أن يعمل الرسول حتى عندما لا يكون التطبيق مفتوحًا ، ولا يعمل مشكلتي. لنكون صادقين ، فإن حل هذه المشكلة لم يخطر ببالي بعد. هنا ، على ما يبدو ، لا يمكنك الاستغناء عن الكود الأصلي.

أقدر جاهزية المشروع بنسبة 70٪.

ملخص


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

إذا كان لديك أي أسئلة ، اكتب. البريد موجود على جيثب.

All Articles