Saat saya menulis utusan saya

Suatu malam, setelah hari yang frustasi lainnya, penuh dengan upaya untuk menyeimbangkan permainan, saya memutuskan bahwa saya sangat membutuhkan istirahat. Saya akan beralih ke proyek lain, lakukan dengan cepat, kembalikan harga diri yang telah bergulir selama pengembangan game dan akan mengambil game dengan badai dengan semangat baru! Yang utama adalah memilih proyek yang menyenangkan dan santai ... Tulis pesan Anda sendiri? Ha! Seberapa sulitkah itu?

Kode dapat ditemukan di sini .


Latar Belakang Singkat


Selama hampir setahun sebelum mulai bekerja pada messenger, ia telah mengerjakan game Line Tower Wars multiplayer online. Pemrograman berjalan dengan baik, segalanya (keseimbangan dan visual khususnya) tidak terlalu baik. Tiba-tiba ternyata membuat permainan dan membuat permainan yang menyenangkan (kesenangan untuk orang lain selain dirinya sendiri) adalah dua hal yang berbeda. Setelah satu tahun cobaan, saya perlu terganggu, jadi saya memutuskan untuk mencoba sesuatu yang lain. Pilihan jatuh pada pengembangan ponsel, yaitu Flutter. Saya mendengar banyak hal baik tentang Flutter, dan saya menyukai panah setelah percobaan singkat. Saya memutuskan untuk menulis utusan saya sendiri. Pertama, praktik yang baik untuk mengimplementasikan klien dan server. Kedua, akan ada sesuatu yang signifikan untuk dimasukkan ke dalam portofolio untuk mencari pekerjaan, saya hanya dalam proses.

Fungsi Terjadwal


  • Obrolan pribadi dan grup
  • Mengirim teks, gambar, dan video
  • Panggilan audio dan video
  • Konfirmasi tanda terima dan bacaan (kutu dari Votsap)
  • "Cetakan ..."
  • Notifikasi
  • Cari berdasarkan kode QR dan geolokasi

Ke depan, saya dapat dengan bangga (dan dengan lega) mengatakan bahwa hampir semua yang direncanakan telah dilaksanakan, dan yang belum dilaksanakan - akan dilaksanakan dalam waktu dekat.



Pilihan bahasa


Saya tidak berpikir lama dengan pilihan bahasa. Pada awalnya, tergoda untuk menggunakan panah untuk klien dan server, tetapi pemeriksaan yang lebih rinci menunjukkan bahwa tidak ada banyak driver untuk panah yang tersedia, dan mereka yang tidak menginspirasi banyak kepercayaan diri. Meskipun saya tidak akan menjamin untuk berbicara tentang momen saat ini, situasinya mungkin telah membaik. Jadi pilihan saya jatuh pada C #, dengan mana saya bekerja di Unity.

Arsitektur


Dia mulai dengan memikirkan arsitektur. Tentu saja, mengingat bahwa 3 setengah orang kemungkinan besar akan menggunakan messenger saya, orang tidak perlu repot dengan arsitektur secara umum. Anda mengambil dan melakukan seperti dalam tutorial yang tak terhitung jumlahnya. Inilah simpulnya, inilah mongo, di sini adalah soket web. Selesai Dan Firebase ada di sekitar sini. Tapi itu tidak menarik. Saya memutuskan untuk membuat messenger yang dapat dengan mudah skala horizontal, seolah-olah saya mengharapkan jutaan klien simultan. Namun, karena saya tidak punya pengalaman di bidang ini, saya harus mempelajari semuanya dalam praktik dengan metode kesalahan dan kesalahan lagi.

Arsitektur terakhir terlihat seperti ini


Saya tidak mengklaim bahwa arsitektur seperti itu sangat keren dan dapat diandalkan, tetapi itu layak dan secara teori harus menahan beban berat dan skala secara horizontal, tetapi saya tidak benar-benar mengerti cara memeriksa. Dan saya harap saya tidak melewatkan momen yang sudah diketahui semua orang kecuali saya.

Di bawah ini adalah penjelasan rinci tentang masing-masing komponen.

Server frontend


Bahkan sebelum saya mulai membuat game, saya terpesona oleh konsep server single-threaded asynchronous. Secara efektif dan tanpa potensi race'ov - apa lagi yang bisa Anda minta. Untuk memahami bagaimana server tersebut diatur, saya mulai mempelajari modul asynciobahasa python. Solusi yang saya lihat tampak sangat elegan. Singkatnya, solusi pseudo-code terlihat seperti ini.
//  ,      ,    
//       .      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,      ,
//        , ,     .
//     ,       .

Dengan menggunakan teknik sederhana ini, kami dapat melayani sejumlah besar soket dalam satu utas. Kami tidak pernah memblokir aliran sembari menunggu byte diterima atau dikirim. Aliran selalu sibuk dengan pekerjaan yang bermanfaat. Concurrency, singkatnya.

Frontend server diimplementasikan dengan cara itu. Semuanya single-threaded dan asinkron. Oleh karena itu, untuk kinerja maksimum, Anda perlu menjalankan server sebanyak pada satu mesin karena memiliki core (4 dalam gambar).

Server Frontend membaca pesan dari klien dan, berdasarkan pada kode pesan, mengirimkannya ke salah satu topik di Kafka.

Catatan kaki kecil untuk mereka yang tidak terbiasa dengan kafa
, RabbitMQ. . , ( authentication backend authentication, ). ? - , (partition). , . , , . , ( , , , (headers)).

? ? . (consumer) ( consumer'), ( ) . , , , 2 , . 3 β€” 2. .

Server frontend mengirim pesan ke kafka tanpa kunci (ketika tidak ada kunci, kafka hanya mengirim pesan ke pesta secara bergantian). Pesan ditarik dari topik oleh salah satu server backend yang sesuai. Server memproses pesan dan ... selanjutnya apa? Dan apa yang selanjutnya tergantung pada jenis pesan.

Dalam kasus yang paling umum, siklus permintaan-respons terjadi. Misalnya, untuk permintaan registrasi, kami hanya perlu memberikan jawaban kepada klien ( Success,EmailAlreadyInUse, dll). Tetapi untuk pesan yang berisi undangan ke obrolan anggota baru yang sudah ada (Vasya, Emil dan Julia), kita perlu segera merespons dengan tiga jenis pesan yang berbeda. Jenis pertama - Anda perlu memberi tahu pengguna undangan tentang hasil operasi (tiba-tiba terjadi kesalahan server). Jenis kedua - Anda perlu memberi tahu semua anggota obrolan saat ini bahwa ada anggota baru ini dan itu di dalam obrolan. Yang ketiga adalah mengirim undangan ke Vasya, Emil dan Yulia.

Oke, itu kedengarannya tidak sulit, tetapi untuk mengirim pesan ke klien mana pun kita perlu: 1) mencari tahu server ujung mana yang terhubung dengan klien ini (kami tidak memilih server mana yang akan disambungkan klien, penyeimbang memutuskan untuk kami); 2) mengirim pesan dari server backend ke server frontend yang diinginkan; 3) sebenarnya, mengirim pesan ke klien.

Untuk menerapkan poin 1 dan 2, saya memutuskan untuk menggunakan topik yang terpisah (topik "server frontend"). Pemisahan otentikasi, sesi, dan topik panggilan ke dalam partisi berfungsi sebagai mekanisme paralelisasi. Kami melihat bahwa server sesi banyak dimuat? Kami hanya menambahkan beberapa server partisi dan sesi baru, dan Kafka akan mendistribusikan kembali beban untuk kami, menurunkan server sesi yang ada. Pemisahan topik "server frontend" ke dalam partisi berfungsi sebagai mekanisme perutean .

Setiap server frontend berhubungan dengan satu bagian dari topik "server frontend" (dengan indeks yang sama dengan server itu sendiri). Yaitu, server 0 - partisi 0, dan seterusnya. Kafka memungkinkan untuk berlangganan tidak hanya ke topik tertentu, tetapi juga ke bagian tertentu dari topik tertentu. Semua server frontend saat start-up berlangganan partisi yang sesuai. Dengan demikian, server backend dapat mengirim pesan ke server frontend tertentu dengan mengirim pesan ke partisi tertentu.

Oke, sekarang ketika klien bergabung, Anda hanya perlu menyimpan sepasang UserId - Frontend Server Index. Dalam hal putuskan - hapus. Untuk tujuan ini, salah satu dari banyak basis data nilai kunci dalam memori akan melakukannya. Saya memilih lobak.

Bagaimana tampilannya dalam praktik. Pertama-tama, setelah koneksi dibuat, klien Andrey mengirim pesan ke server Join. Server Frontend menerima pesan dan meneruskannya ke topik sesi, sebelum menambahkan header "Server Frontend": {index}. Salah satu server sesi backend akan menerima pesan, membaca token otorisasi, menentukan jenis pengguna yang telah bergabung, membaca indeks yang ditambahkan oleh server frontend dan menulis UserId - Index ke lobak. Dari saat ini, klien dianggap online, dan sekarang kita tahu melalui server frontend mana (dan, dengan demikian, melalui bagian mana dari topik "server frontend") kita dapat "menjangkau" ketika klien lain mengirim pesan ke Andrey.

* Sebenarnya, prosesnya sedikit lebih rumit dari yang saya jelaskan. Anda dapat menemukannya di kode sumber.

Kode pseudo server frontend


// 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);
    }
}


Ada beberapa trik di sini.
1) relayMessageToClient. Ini akan menjadi kesalahan untuk hanya mengambil soket yang Anda inginkan dan segera mulai mengirim pesan ke sana, karena mungkin kami sudah mengirim beberapa pesan lain ke klien. Jika kita mulai mengirim byte tanpa memeriksa apakah soket sedang sibuk, pesan akan tercampur. Seperti di banyak tempat lain di mana pemrosesan data yang tertib diperlukan, triknya adalah menggunakan antrian, yaitu antrian dari Completers ( TaskCompletionSourcedalam 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);
    }
}

Jika antrian tidak kosong, maka soket sudah terisi saat ini. Buat yang baru completer, tambahkan ke antrian dan awaityang sebelumnya completer . Jadi, ketika pesan sebelumnya dikirim, itu CompleteSendakan selesai completer, yang akan menyebabkan server mulai mengirim pesan berikutnya. Antrian seperti itu juga memungkinkan perkembangbiakan pengecualian dengan lancar. Misalkan terjadi kesalahan saat mengirim pesan ke klien. Dalam hal ini, kita perlu menyelesaikan, dengan pengecualian mengirim tidak hanya pesan ini, tetapi juga semua pesan yang sedang menunggu dalam antrian (tunggu await'ah). Jika tidak, maka mereka akan terus hang, dan kami akan menerima kebocoran memori. Untuk singkatnya, kode yang melakukan ini tidak ditampilkan di sini.

2)selector.Poll. Sebenarnya, ini bahkan bukan tipuan, tetapi hanya upaya untuk memuluskan kekurangan dari implementasi metode Socket.Select( selector- hanya penutup metode ini). Tergantung pada OS di bawah tenda, metode ini menggunakan salah satu selectatau poll. Tapi ini tidak penting di sini. Yang penting adalah bagaimana metode ini bekerja dengan daftar yang kami masukkan ke input (daftar soket untuk membaca, menulis, memeriksa kesalahan). Metode ini mengambil daftar, polling soket dan hanya menyisakan soket dalam daftar yang siap untuk melakukan operasi yang diperlukan. Semua soket lainnya dikeluarkan dari daftar. "Tendangan" terjadiRemoveAt(yaitu, semua elemen berikutnya digeser, yang tidak efisien). Plus, karena kita perlu mensurvei semua soket terdaftar setiap iterasi siklus, "pembersihan" seperti itu umumnya berbahaya, kita harus mengisi ulang daftar setiap waktu. Kami dapat mengatasi semua masalah ini menggunakan masalah khusus List, RemoveAtyang metodenya tidak menghapus item dari daftar, tetapi cukup menandainya sebagai dihapus. Kelas ListForPollingadalah implementasi saya dari daftar semacam itu. ListForPollinghanya bekerja dengan metode ini Socket.Selectdan tidak cocok untuk hal lain.

3)callAtQueue. Dalam kebanyakan kasus, server frontend, setelah mengirim pesan klien ke server backend, mengharapkan respons (konfirmasi bahwa operasi berhasil, atau kesalahan jika terjadi kesalahan). Jika dia tidak menunggu jawaban dalam periode waktu yang dapat dikonfigurasi, dia mengirim kesalahan kepada klien sehingga dia tidak menunggu jawaban yang tidak akan pernah datang. callAtQueueMerupakan antrian prioritas. Segera setelah server mengirim pesan ke Kafka, ia melakukan sesuatu seperti ini:
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
callAtQueue.Enqueue(callback, now + config.WaitForReplyMSec);

Dalam panggilan balik, menunggu respons dibatalkan dan pengiriman kesalahan server dimulai. Jika respons dari server backend diterima, panggilan balik tidak melakukan apa pun. Tidak ada cara untuk

menggunakannya await Task.WhenAny(answerReceivedTask, Task.Delay(x)), karena kode setelah Task.Delaydijalankan pada utas dari kolam.

Di sini, pada kenyataannya, segala sesuatu tentang server frontend. Diperlukan sedikit koreksi di sini. Padahal, server tidak sepenuhnyaberulir tunggal. Tentu saja, kafka di bawah tenda menggunakan utas, tapi maksud saya kode aplikasi. Faktanya adalah bahwa mengirim pesan ke topik kafka (hasil) mungkin tidak berhasil. Jika terjadi kegagalan, Kafka mengulangi pengiriman beberapa kali yang dapat dikonfigurasi, tetapi, jika keberangkatan berulang gagal, Kafka meninggalkan bisnis ini tanpa harapan. Anda dapat memeriksa apakah pesan telah berhasil dikirim atau tidak di deliveryHandlermana kami meneruskan ke metode Produce. Kafka menyebut penangan ini di utas I / O produsen (utas yang mengirim pesan). Kami harus memastikan bahwa pesan telah berhasil dikirim, dan jika tidak, batalkan menunggu jawaban dari server backend (respons tidak akan datang karena permintaan tidak terkirim) dan mengirim kesalahan ke klien. Artinya, kami tidak dapat menghindari berinteraksi dengan utas lainnya.

* Ketika menulis artikel, tiba-tiba saya menyadari bahwa kita tidak dapat meneruskan deliveryHandlerke metode Produceatau mengabaikan semua kesalahan kafka (kesalahan masih akan dikirim ke klien dengan batas waktu yang saya jelaskan sebelumnya) - maka semua kode kita akan berurutan tunggal. Sekarang saya berpikir bagaimana melakukannya dengan lebih baik.

Sebenarnya, kafka, bukan kelinci?
, , , , , RabbitMQ? . , , . ? , frontend . , backend , , . , , . , error-prone. , basicGet , , , . . basicGet, , . .


Server backend


Dibandingkan dengan server frontend, praktis tidak ada poin menarik di sini. Semua server backend bekerja dengan cara yang sama. Pada saat startup, server berlangganan ke topik (otentikasi, sesi atau panggilan tergantung pada peran), dan kafka menetapkan satu atau lebih partisi untuknya. Server menerima pesan dari Kafka, memproses dan biasanya mengirim satu atau lebih pesan sebagai tanggapan. Kode yang hampir nyata:
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();
	}
    }
}

Jenis offset apa yang harus dikomit?
. β€” (offset) (0, 1 ). 0. TopicPartitionOffset. (consume) , ConsumeResult, , , TopicPartitionOffset. ?

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

Saya menonaktifkan komitmen otomatis dan mengikat diri saya sendiri. Ini diperlukan karena handleWorkUnit, di mana pemrosesan pesan sebenarnya dilakukan, ini adalah async voidmetode, oleh karena itu tidak ada jaminan bahwa pesan 5 akan diproses sebelum pesan 6. Kafka menyimpan masing-masing satu offset yang dilakukan (dan bukan satu set offset), masing-masing, sebelum melakukan offset 6, kita perlu memastikan bahwa semua pesan sebelumnya telah diproses juga. Selain itu, satu server backend dapat menggunakan pesan dari beberapa partisi pada saat yang bersamaan, dan, karenanya, harus memastikan bahwa ia melakukan offset yang benar ke partisi yang sesuai. Untuk ini, kami menggunakan peta hash dari partisi bentuk: unit kerja. Seperti apa kode ini commitOffsets(kode sebenarnya saat ini):
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();
    }
}

Seperti yang Anda lihat, kami beralih pada unit, menemukan unit terakhir selesai saat ini, setelah itu tidak ada yang tidak lengkap , dan melakukan offset yang sesuai. Perulangan semacam itu memungkinkan kita untuk menghindari komit "berlubang". Misalnya, jika saat ini kami memiliki 4 unit ( 0: Finished, 1: Not Finished, 2: Finished, 3: Finished), kami hanya dapat melakukan unit ke-0, karena jika kami langsung berkomitmen ke-3, ini dapat menyebabkan potensi hilangnya unit ke-1 jika server mati saat ini.
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);
    }
}


handleWorkUnitseperti yang dikatakan, async voidmetode, dan karenanya, sepenuhnya dibungkus try-catch-finally. Dalam tryia memanggil layanan yang diperlukan, dan di finally- workUnit.Finish().

Layanannya cukup sepele. Di sini, misalnya, kode apa yang dieksekusi ketika pengguna mengirim pesan baru:
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
    );
}


Basis data


Sebagian besar fungsionalitas layanan yang disebut oleh server backend hanya menambahkan data baru ke database dan memproses yang sudah ada. Jelas, bagaimana database diorganisasikan dan bagaimana kami beroperasi sangat penting bagi messenger, dan di sini saya ingin mengatakan bahwa saya mendekati masalah memilih database dengan sangat hati-hati setelah mempelajari semua opsi dengan hati-hati, tetapi tidak demikian halnya. Saya baru saja memilih CockroachDb karena menjanjikan banyak dengan usaha minimal dan memiliki sintaks yang kompatibel postgres (saya pernah bekerja dengan postgres sebelumnya). Ada pemikiran untuk menggunakan Cassandra, tetapi pada akhirnya saya memutuskan untuk memikirkan sesuatu yang akrab. Saya belum pernah bekerja dengan Kafka, atau dengan Kelinci, atau dengan Flutter dan Dart, atau dengan WebRtc, jadi saya memutuskan untuk tidak menyeret Cassandra juga, karena saya takut menenggelamkan sejumlah besar teknologi baru untuk saya.

Dari semua bagian proyek saya, desain basis data adalah hal yang paling saya ragukan. Saya tidak yakin bahwa keputusan yang saya buat adalah keputusan yang benar-benar baik . Semuanya berfungsi, tetapi bisa dilakukan dengan lebih baik. Misalnya, ada tabel ShareRooms (seperti yang saya sebut obrolan) dan ShareItems (seperti yang saya sebut pesan). Jadi semua pengguna yang memasuki ruangan dicatat di bidang jsonb ruangan ini. Ini nyaman, tapi jelas sangat lambat, jadi saya mungkin akan mengulanginya menggunakan kunci asing. Atau, misalnya, tabel ShareItems menyimpan semua pesan. Yang juga nyaman, tetapi karena ShareItems adalah salah satu tabel paling banyak dimuat (persisten selectdaninsert), mungkin ada baiknya membuat tabel baru untuk setiap kamar atau sesuatu seperti itu. Kokroach menyebarkan rekaman pada node yang berbeda, oleh karena itu, Anda harus hati-hati memikirkan catatan mana yang akan digunakan untuk mencapai kinerja maksimum, tetapi saya tidak melakukannya. Secara umum, seperti dapat dipahami dari semua hal di atas, basis data bukan titik terkuat saya. Saat ini saya umumnya menguji segala sesuatu untuk postgres, dan bukan kokroach, karena ada lebih sedikit beban pada mesin saya, sudah sangat buruk dari beban sehingga akan segera lepas landas. Untungnya, kode untuk postgres dan kokroach sedikit berbeda, jadi beralih tidak sulit.

Sekarang saya sedang dalam proses mempelajari bagaimana cocroach sebenarnya bekerja (bagaimana pemetaan terjadi antara SQL dan nilai kunci (cocroach menggunakan RocksDb di bawah tenda), bagaimana ia mendistribusikan data antara node, ulangan, dll.). Tentu saja bermanfaat untuk mempelajari cocroach sebelum menggunakannya, tetapi lebih baik terlambat daripada tidak sama sekali.

Saya pikir basis akan mengalami perubahan besar ketika saya menjadi lebih baik dalam memahami masalah ini. Saat ini, meja Acks menghantui saya. Dalam tabel ini, saya menyimpan data tentang siapa yang belum menerima dan siapa yang belum membaca pesan (untuk menunjukkan tanda centang pengguna). Mudah untuk memberi tahu pengguna bahwa pesannya telah dibaca jika pengguna sedang online sekarang, tetapi jika tidak, kita perlu menyimpan informasi ini untuk memberi tahu pengguna nanti. Dan karena obrolan grup tersedia, tidak cukup hanya untuk menyimpan benderanya, Anda memerlukan data tentang pengguna perorangan. Jadi di sini kami langsung meminta penggunaan string bit (satu baris untuk pengguna yang belum menerima, yang kedua - bagi mereka yang belum membaca). Terutama dukungan kokroach bitdanbit varying. Namun, saya tidak pernah menemukan cara untuk mengimplementasikan bisnis ini, mengingat bahwa komposisi kamar dapat terus berubah. Agar string bit tetap mempertahankan maknanya, pengguna di dalam ruangan harus tetap dalam urutan yang sama, yang cukup sulit dilakukan ketika, misalnya, beberapa pengguna meninggalkan ruangan. Ada opsi di sini. Mungkin ada baiknya menulis -1 daripada menghapus pengguna dari bidang jsonb sehingga pesanan dipertahankan, atau menggunakan beberapa metode versi, sehingga kami tahu bahwa string bit ini mengacu pada urutan pengguna, yang saat itu, dan tidak pada urutan pengguna saat ini. Saya masih dalam proses berpikir tentang bagaimana menerapkan bisnis ini dengan lebih baik, tetapi untuk saat ini, mereka yang belum menerima dan belum membaca pengguna juga hanya bidang jsonb. Mengingat bahwa tabel Acks ditulis dengan setiap pesan, jumlah datanya besar.Meskipun catatan, tentu saja, dihapus ketika pesan diterima dan dibaca oleh semua orang.

Berdebar


Untuk waktu yang lama saya bekerja di sisi server dan menggunakan klien konsol sederhana untuk pengujian, jadi saya bahkan tidak membuat proyek Flutter. Dan ketika saya membuatnya, saya berpikir bahwa bagian server adalah bagian yang kompleks , dan aplikasinya seperti itu, sampah, saya akan mengetahuinya dalam beberapa hari. Saat bekerja di server, saya membuat Hello Worlds untuk bergetar beberapa kali untuk merasakan kerangka, dan karena kurir itu tidak memerlukan UI yang rumit, saya pikir itu benar-benar siap. Jadi UI, benar-benar, adalah sampah, tetapi penerapan fungsi memberi saya masalah (dan itu masih akan menghasilkan, karena tidak semuanya siap).

Manajemen negara


Topik paling populer. Ada ribuan cara untuk mengelola kondisi Anda, dan pendekatan yang disarankan diubah setiap enam bulan. Sekarang arus utama adalah penyedia. Secara pribadi, saya memilih 2 cara untuk diri saya sendiri: blok dan redux. Blok (Komponen Logika Bisnis) untuk mengelola negara bagian dan redux untuk mengelola global.

Blok bukan semacam perpustakaan (walaupun, tentu saja, ada juga perpustakaan yang mengurangi boilerplate, tapi saya tidak menggunakannya). Blok adalah pendekatan berbasis aliran. Secara umum, panah adalah bahasa yang cukup bagus, dan aliran pada umumnya sangat manis. Inti dari pendekatan ini adalah kami mendorong seluruh logika bisnis ke dalam layanan, dan kami berkomunikasi antara UI dan layanan melalui pengontrol yang memberi kami berbagai aliran. Apakah pengguna mengklik tombol β€œtemukan kontak”? Menggunakansink(ujung lain dari aliran) kami mengirim acara ke controller SearchContactsEvent, controller akan memanggil layanan yang diinginkan, menunggu hasilnya dan mengembalikan daftar pengguna kembali ke UI melalui stream juga. UI menunggu hasil menggunakan StreamBuilder(widget yang dibangun kembali setiap kali data baru tiba di aliran tempat berlangganannya). Faktanya, itu saja. Dalam beberapa kasus, kami perlu memperbarui UI tanpa keterlibatan pengguna (misalnya, ketika pesan baru tiba), tetapi ini juga mudah dilakukan melalui streaming. Bahkan, MVC sederhana dengan aliran, tanpa sihir.

Dibandingkan dengan beberapa pendekatan lain, blok membutuhkan lebih banyak boilerplate, tetapi, menurut saya, lebih baik menggunakan solusi asli tanpa partisipasi perpustakaan pihak ketiga, kecuali menggunakan solusi pihak ketiga memberikan beberapa signifikankeuntungan. Semakin banyak abstraksi di atas, semakin sulit untuk memahami apa kesalahannya ketika kesalahan terjadi. Saya tidak menganggap kelebihan penyedia cukup signifikan untuk beralih ke itu. Tetapi saya memiliki sedikit pengalaman di bidang ini, sehingga kemungkinan saya akan mengubah kamp di masa depan.

Ya, tentang redux, dan semua orang tahu segalanya, jadi tidak ada yang perlu dikatakan. Selain itu, saya hentikan itu dari aplikasi :) Saya menggunakannya untuk mengelola akun saya, tetapi kemudian, menyadari bahwa dalam kasus ini tidak ada keuntungan khusus atas blokir, saya hentikan agar tidak terlalu banyak menarik. Tetapi secara umum saya menganggap redux hal yang berguna untuk mengelola negara global.

Bagian yang paling menyiksa


Apa yang harus saya lakukan jika pengguna mengirim pesan, tetapi sebelum dikirim, koneksi Internet terputus? Apa yang harus saya lakukan jika pengguna menerima konfirmasi baca, tetapi ia menutup aplikasi sebelum catatan terkait dalam database diperbarui? Apa yang harus saya lakukan jika pengguna mengundang temannya ke kamar, tetapi sebelum undangan dikirim, baterainya mati? Pernahkah Anda menanyakan pertanyaan serupa? Saya disini. Sebelum. Tetapi dalam proses pengembangan saya mulai bertanya-tanya. Karena koneksi dapat menghilang kapan saja, dan telepon mati kapan saja, semuanya harus dikonfirmasi . Tidak menyenangkan. Oleh karena itu, pesan pertama yang dikirim klien ke server ( Joinjika Anda ingat) bukan hanya "Halo saya sedang online" , itu adalah"Halo, saya sedang online dan ini kamar yang belum dikonfirmasi, ini acks yang belum dikonfirmasi, ini operasi keanggotaan kamar yang belum dikonfirmasi, dan ini pesan terakhir yang diterima per kamar . " Dan server merespons dengan lembar serupa: "Ketika Anda sedang offline, pesan ini dan itu dibaca oleh pengguna ini dan itu, dan mereka juga mengundang Petya ke ruangan ini, dan Sveta meninggalkan ruangan ini, dan Anda diundang ke ruangan ini, tetapi untuk dua kamar ini memiliki 40 pos baru . " Saya benar-benar ingin tahu bagaimana hal serupa dilakukan pada utusan lain, karena implementasi saya tidak bersinar dengan anggun.

Gambar-gambar


Saat ini, Anda dapat mengirim teks, teks + gambar dan hanya gambar. Upload video belum diimplementasikan. Gambar dikompresi sedikit dan disimpan dalam penyimpanan Firebase. Pesan itu sendiri berisi tautan. Setelah menerima pesan, klien mengunduh gambar, menghasilkan thumbnail dan menyimpan semuanya ke sistem file. Jalur file ditulis ke database. Omong-omong, pembuatan thumbnail adalah satu-satunya kode yang dieksekusi pada utas terpisah, karena ini adalah operasi yang sangat berat. Saya baru memulai satu stream pekerja, berikan gambar dan sebagai imbalannya saya mendapatkan thumbnail. Kode ini sangat sederhana, karena panah memberikan abstraksi yang nyaman untuk bekerja dengan stream.

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 authase juga digunakan, tetapi hanya untuk otorisasi akses ke penyimpanan Firebase (sehingga pengguna tidak dapat, misalnya, mengisi gambar profil dengan orang lain ). Semua otorisasi lain dilakukan melalui server saya.

Format pesan


Anda mungkin ngeri di sini, karena saya menggunakan array byte biasa. Json menghilang karena efisiensi diperlukan, dan saya tidak tahu tentang protobuf ketika saya mulai. Menggunakan array memerlukan banyak perhatian karena satu indeks salah dan semuanya serba salah.

4 byte pertama adalah panjang pesan.
Byte berikutnya adalah kode pesan.
16 byte berikutnya adalah pengidentifikasi permintaan (uuid).
40 byte berikutnya adalah token otorisasi.
Sisa pesan .

Panjang Pesandiperlukan, karena saya tidak menggunakan http atau soket web, atau protokol lain yang menyediakan pemisahan satu pesan dari yang lain. Server frontend saya hanya melihat stream byte, dan mereka perlu tahu di mana satu pesan berakhir dan yang lainnya dimulai. Ada beberapa cara untuk memisahkan pesan (misalnya, menggunakan beberapa jenis karakter yang tidak pernah ditemukan dalam pesan sebagai pemisah), tetapi saya lebih suka menentukan panjangnya, karena metode ini paling mudah, meskipun memerlukan overhead, karena sebagian besar pesan hilang dan satu byte untuk menunjukkan panjangnya.

Kode pesan hanyalah salah satu anggota enumMessageCode. Routing dilakukan sesuai dengan kode, dan karena kita dapat mengekstrak kode dari array tanpa deserialization awal, server frontend memutuskan di mana topik kafka untuk mengirim pesan alih-alih mendelegasikan tanggung jawab ini kepada orang lain.

ID Permintaanhadir di sebagian besar pos, tetapi tidak di semua. Itu melakukan 2 fungsi: oleh pengidentifikasi ini, klien membuat korespondensi antara permintaan yang dikirim dan respons yang diterima (jika klien mengirim pesan A, B, C dalam urutan ini, ini tidak berarti bahwa jawaban juga akan datang secara berurutan). Fungsi kedua adalah untuk menghindari duplikat. Seperti disebutkan sebelumnya, kafka menjamin setidaknya satu kali pengiriman. Artinya, dalam kasus yang jarang terjadi, pesan masih dapat diduplikasi. Dengan menambahkan kolom RequestIdentifier dengan batasan unik ke tabel database yang diinginkan, kita dapat menghindari memasukkan duplikat.

Token OtorisasiAdalah tanda tangan UserId (8 byte) + 32 byte HmacSha256. Saya tidak berpikir itu layak menggunakan Jwt di sini. Jwt sekitar 7-8 kali lebih besar untuk apa? Pengguna saya tidak memiliki klaim, jadi tanda tangan hmac sederhana tidak masalah. Otorisasi melalui layanan lain tidak dan tidak direncanakan.

Panggilan audio dan video


Lucu bahwa saya sengaja menunda implementasi panggilan audio dan video, karena saya yakin saya tidak akan bisa menyelesaikan masalah, tetapi ternyata itu menjadi salah satu fitur yang paling mudah untuk diterapkan. Setidaknya fungsionalitas dasar. Secara umum, hanya menambahkan WebRtc ke aplikasi dan mendapatkan sesi video pertama hanya butuh beberapa jam, dan, secara ajaib, tes pertama berhasil. Sebelum itu, saya berpikir bahwa kode yang berfungsi pertama kali adalah mitos. Biasanya pengujian pertama dari fitur baru selalu gagal karena beberapa jenis kesalahan bodoh seperti "menambahkan layanan, tetapi tidak mendaftarkannya dalam wadah DI".

Tidak terlalu singkat tentang WebRtc untuk yang belum tahu
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, . , , , , . ( ) , .


Teknologi WebRtc sendiri membangun koneksi dan terlibat dalam mentransfer arus bolak-balik, tetapi ini bukan kerangka kerja untuk membuat panggilan penuh. Melalui panggilan, maksud saya sesi komunikasi dengan kemampuan untuk membatalkan, menolak dan menerima panggilan, serta menutup telepon. Selain itu, Anda harus memberi tahu penelepon jika pihak lain sudah diambil. Dan juga untuk mengimplementasikan hal-hal kecil seperti "tunggu jawaban panggilan N detik, lalu reset." Jika Anda hanya mengimplementasikan WebRtc dalam aplikasi dalam bentuk kosong, maka dengan panggilan masuk, kamera dan video akan menyala secara spontan, yang tentu saja tidak dapat diterima.

Dalam bentuknya yang murni, WebRtc biasanya menyiratkan pengiriman kandidat ke pihak lain sesegera mungkin sehingga negosiasi dimulai secepat mungkin, yang logis. Dalam tes saya, kandidat untuk partai penerima umumnya selalumulai datang bahkan sebelum penawaran datang. Calon "awal" seperti itu tidak dapat dibuang, mereka harus diingat, sehingga nanti, ketika penawaran datang dan RTCPeerConnectiondibuat, tambahkan mereka ke koneksi. Fakta bahwa kandidat dapat mulai datang bahkan sebelum penawaran, serta beberapa alasan lainnya, menjadikan pelaksanaan panggilan penuh menjadi tugas yang tidak sepele. Apa yang harus dilakukan jika beberapa pengguna menghubungi kami sekaligus? Kami akan menerima kandidat dari semua, dan meskipun kami dapat memisahkan kandidat dari satu pengguna dari pengguna lain, menjadi tidak jelas kandidat mana yang akan ditolak, karena kami tidak tahu penawaran siapa yang akan datang lebih awal. Juga akan ada masalah jika kandidat mulai datang kepada kita dan kemudian tawaran pada saat ketika kita sendiri memanggil seseorang.

Setelah menguji beberapa opsi dengan WebRtc kosong, saya sampai pada kesimpulan bahwa mencoba membuat panggilan dalam bentuk ini akan bermasalah dan penuh dengan kebocoran memori, jadi saya memutuskan untuk menambahkan tahap lain ke proses negosiasi WebRtc. Saya menyebut tahap ini Inquire - Grant/Refuse.

Idenya sangat sederhana, tetapi butuh waktu cukup lama untuk mencapainya. Penelepon bahkan sebelum membuat aliran dan RTCPeerConnection(dan umumnya sebelum menjalankan kode apa pun yang terkait dengan WebRtc) mengirim pesan melalui server sinyal ke sisi lain Inquire. Di sisi penerima, diperiksa apakah pengguna sedang dalam sesi komunikasi lain saat ini ( boolbidang sederhana ). Jika ya, maka pesan dikirim kembali.Refuse, dan dengan cara ini kami memberi tahu penelepon bahwa pengguna sedang sibuk, dan penerima - bahwa telepon ini dan itu dipanggil saat ia sibuk dengan percakapan lain. Jika pengguna saat ini gratis, maka itu dicadangkan . InquirePengidentifikasi sesi dikirim dalam pesan , dan pengidentifikasi ini ditetapkan sebagai pengidentifikasi sesi saat ini . Jika pengguna dicadangkan, ia akan menolak semua Inquire/Offer/Candidatepesan dengan pengidentifikasi sesi selain yang sekarang. Setelah pemesanan, penerima mengirim pesan melalui server sinyal ke pemanggil Grant. Patut dikatakan bahwa proses ini tidak terlihat oleh pengguna penerima, karena belum ada panggilan. Dan hal utama di sini adalah jangan lupa untuk menutup waktu tunggu di sisi penerima. Tiba-tiba kami akan memesan satu sesi, dan tidak ada penawaran yang akan mengikuti.

Penelepon menerima Grant, dan di sinilah WebRtc dimulai dengan penawaran, kandidat, dan ini untuk semua orang. Penawaran terbang ke penerima, dan dia, setelah diterima, menampilkan layar dengan tombol Jawab / Tolak. Tetapi para kandidat, seperti biasa, tidak mengharapkan siapa pun. Mereka kembali mulai datang bahkan lebih awal daripada penawaran, karena tidak ada alasan untuk menunggu pengguna menjawab panggilan. Dia mungkin tidak menjawab, tetapi menolak atau menunggu sampai batas waktu berakhir - maka para kandidat akan diusir begitu saja.

Status saat ini dan rencana masa depan


  • Obrolan pribadi dan grup
  • Mengirim teks, gambar, dan video
  • Panggilan audio dan video
  • Konfirmasi tanda terima dan bacaan
  • "Cetakan ..."
  • Notifikasi
  • Cari berdasarkan kode QR dan geolokasi


Pencarian dengan kode QR, secara tak terduga, cukup bermasalah untuk diterapkan, karena hampir semua plugin untuk pemindaian kode yang saya coba tolak untuk memulai atau tidak berfungsi dengan benar. Tapi saya pikir masalahnya akan diselesaikan di sini. Dan untuk implementasi pencarian geolokasi, saya belum mengambil. Secara teori, seharusnya tidak ada masalah khusus.

Pemberitahuan sedang berlangsung, serta mengirim video.

Apa lagi yang perlu dilakukan?


Oh, banyak.
Pertama, tidak ada tes. Rekan kerja dulu menulis tes, jadi saya benar-benar santai.
Kedua, mengundang pengguna ke obrolan yang ada dan meninggalkan obrolan saat ini tidak memungkinkan. Kode server siap untuk ini, kode klien tidak.
Ketiga, jika penanganan kesalahan pada server lebih atau kurang, maka tidak ada penanganan kesalahan pada klien. Tidak cukup hanya dengan membuat entri log, Anda harus mencoba kembali operasi. Sekarang, misalnya, mekanisme pengiriman ulang pesan tidak diterapkan.
Keempat, server tidak melakukan ping klien, jadi putuskan sambungan tidak terdeteksi jika, misalnya, klien telah kehilangan Internet. Putuskan sambungan terdeteksi hanya ketika klien menutup aplikasi.
Kelima, indeks tidak digunakan dalam database.
Keenam, optimasi. Kode memiliki sejumlah besar tempat di mana sesuatu seperti ditulis // @@TODO: Pool. Kebanyakan array hanya newitu. Server backend menciptakan banyak array dengan panjang tetap, jadi di sini Anda dapat dan harus menggunakan kumpulan.
Ketujuh, ada banyak tempat di klien tempat kode awaitberakhir, meskipun ini tidak perlu. Mengirim gambar, misalnya, karena itu tampaknya lambat karena kodenyaawaitIni menyimpan gambar ke sistem file dan menghasilkan thumbnail sebelum menampilkan pesan, meskipun tidak ada yang perlu dilakukan. Atau, misalnya, jika Anda membuka aplikasi dan selama Anda tidak ada, mereka mengirim gambar kepada Anda, startup akan lambat, karena sekali lagi semua gambar ini diunduh, disimpan ke sistem, thumbnail dihasilkan, dan hanya setelah itu startup berakhir dan Anda terlempar dari layar splash di layar beranda. Semua ini berlebihan awaitdibuat untuk memudahkan debugging, tetapi, tentu saja, Anda harus menyingkirkan menunggu yang tidak perlu sebelum rilis.
KedelapanUI sekarang setengah siap, karena saya belum memutuskan bagaimana saya ingin melihatnya. Karena itu, sekarang semuanya tidak intuitif, setengah dari tombol tidak jelas apa yang mereka lakukan. Dan tombol sering kali tidak ditekan pertama kali, karena sekarang mereka hanya ikon dengan GestureDetectordan tanpa bantalan, jadi tidak selalu mungkin untuk masuk ke dalamnya. Ditambah di beberapa tempat, pixel overflow tidak diperbaiki.
Kesembilan, sekarang bahkan tidak mungkin untuk Masuk ke akun, hanya Mendaftar. Karenanya, jika Anda menghapus aplikasi dan menginstalnya kembali, Anda tidak akan dapat masuk ke akun Anda :)
Kesepuluh, kode verifikasi tidak dikirim ke surat. Sekarang kode umumnya selalu sama, lagi karena lebih mudah untuk di-debug.
KesebelasPrinsip tanggung jawab tunggal dilanggar di banyak tempat. Butuh refactor. Kelas-kelas yang bertanggung jawab untuk berinteraksi dengan database (baik pada klien dan di server) umumnya sangat membengkak, karena mereka terlibat dalam semua operasi database.
Keduabelas, server frontend sekarang selalu mengharapkan respons dari server backend, bahkan jika pesan tidak menyiratkan mengirim respons (misalnya, pesan dengan kode IsTypingdan beberapa pesan yang berhubungan dengan WebRtc). Karena itu, tanpa menunggu jawaban, ia menulis kesalahan ke konsol, meskipun ini bukan kesalahan.
Ketigabelas, gambar penuh tidak terbuka saat diketuk.
Seratus juta perlimabeberapa pesan yang perlu dikirim dalam batch dikirim secara terpisah. Hal yang sama berlaku untuk beberapa operasi basis data. Alih-alih mengeksekusi satu perintah, perintah dieksekusi dalam satu lingkaran dengan await(brr ..).
Seratus juta keenam, beberapa nilai di-hardcode, alih-alih dapat dikonfigurasi.
Seratus satu juta tujuhlogging di server sekarang hanya untuk konsol, dan pada klien secara umum, langsung ke widget. Di layar utama ada tab Log, di mana semua log di tekan dijatuhkan. Faktanya adalah bahwa mesin saya menolak untuk menjalankan emulator dan semua yang diperlukan untuk server (kafka, database, lobak dan semua server). Debit dengan perangkat yang terhubung juga tidak berhasil, semuanya hanya tergantung erat pada separuh kasing, karena komputer tidak dapat mengatasi bebannya. Karena itu, Anda harus membuat build setiap saat, memasukkannya ke perangkat, menginstal dan menguji seperti ini. Untuk melihat log, saya taruh di widget. Penyimpangan, saya tahu, tetapi tidak ada pilihan. Untuk alasan yang sama, banyak metode kembali FuturedanawaitMereka (untuk menangkap pengecualian dan melempar ke widget), meskipun seharusnya tidak. Jika Anda melihat kode, Anda akan melihat _logErrormetode jelek di banyak kelas yang melakukan ini. Ini, tentu saja, juga akan dibuang ke tempat sampah.
Seratus juta delapan, tidak ada suara.
Seratus juta sembilan, Anda perlu menggunakan caching lebih banyak.
Seratus juta persepuluh, banyak kode berulang. Misalnya, banyak tindakan pertama-tama memeriksa validitas token, dan jika tidak valid, mereka mengirim kesalahan. Saya pikir Anda perlu menerapkan middleware-pipeline sederhana.

Dan banyak hal-hal kecil, seperti string gabungan daripada menggunakan StringBuilder'a,Disposetidak di mana-mana disebut di mana seharusnya, dan seterusnya dan seterusnya. Secara umum, keadaan normal proyek sedang dalam pengembangan. Semua hal di atas dapat diselesaikan, tetapi ada satu masalah mendasar yang tidak saya pikirkan sampai saat terakhir, karena itu keluar dari kepala saya - messenger harus bekerja bahkan ketika aplikasi tidak terbuka, dan milik saya tidak berfungsi. Sejujurnya, solusi untuk masalah ini belum terlintas di pikiran saya. Di sini, tampaknya, Anda tidak dapat melakukannya tanpa kode asli.

Saya akan menilai kesiapan proyek di 70%.

Ringkasan


Enam bulan telah berlalu sejak awal pengerjaan proyek. Dikombinasikan dengan pekerjaan paruh waktu dan mengambil istirahat panjang, tetapi masih membutuhkan waktu dan energi yang cukup. Saya berencana untuk mengimplementasikan semua fitur yang dideklarasikan + menambahkan sesuatu yang tidak biasa seperti tic-tac-toe atau draft tepat di ruangan. Tanpa alasan, hanya karena itu menarik.

Jika Anda memiliki pertanyaan, tulis. Mail ada di github.

All Articles