Loopin messenjerining yaratilishi

Loopin messenjerining yaratilishi

1. Kirish (Introduction)

1.1 Loyihalar tarixi

Qadimgi eski xabar xizmatlari tuzilishi o'zgaruvchan veb muhitga moslashishga qiyin bo'lib, texnik xizmat ko'rsatish jihatidan ko'p tashvishlarni keltirib chiqardi. Ushbu loyiha 'Loopin' bunday Smalltalk tizimini olib tashlash va zamonaviy Vizend platformasiga qayta tashkil etishni maqsad qilib oldi.

Xabar xizmatlarining asosiy qiymati 'foydalanuvchilar o'rtasida uzluksiz real vaqtli muloqot tajribasidir'. Bunga erishish uchun eng muhim texnik vazifalar ikkita edi. Birinchidan, mavjud HTTP protokolining bir tomonlama so'rov-javob modelidan chiqib, mijoz va server o'rtasida doimiy ulanishni o'rnatish va kechikishlarsiz xabarlarni jo'natish va qabul qilish imkonini beradigan xabarlashuv kanali yaratish. Ikkinchidan, hamkorlik va muloqot samaradorligini maksimal darajada oshirish uchun foydalanuvchilarning hozirgi ulanish holatini real vaqtda aniqlab, ekranlarga chiqazuvchi ulanish holatini boshqarish (Presence) mexanizmini amalga oshirish edi.

1.2 Loyihaning maqsadi

Texnologiyani tanlaganda eng ehtiyot bo'lish kerak bo'lgan jihat esa 'ortiqcha muhandislik (Overengineering)'dir. Katta global trafikni ko'tara oladigan arxitektura bo'lmasa-da, modaga ergashib Redis, Kafka kabi ko'plab murakkab tarqatilgan yechimlarni infrastruktura orqali undov berish operatsion yukni va tarqatish qiyinchiliklarini oshiradi, shuningdek, bulut narxining ortiqcha oshishiga olib keladi. Loopin loyihasi 'soddalik va samaradorlik (KISS - Keep It Simple, Stupid)'ni asosiy qiymat sifatida belgilangan. Tashqi infrasturani maksimal darajada chiqarib tashlab, xizmatning asosiy saqlash manbai bo'lgan munosabatlar bazasini RDB bir necha imkoniyatlardan foydalangan holda real vaqtli Presence va xabarlarning umr bo'yi siklini to'liq boshqarishni o'z ichiga olgan maqsad qildi.

2. Tizim arxitekturasi umumiy ko'rinishi (System Architecture)

Loopin xabarchisining ma'lumotlar oqimi keraksiz qatlamlarni o'chirib tashlash va aniq mas'uliyat bo'linishi asosida tashkil etilgan. Real vaqtli aloqa qatlamini ta'minlash va ma'lumotning yaxlitligini kafolatlovchi mustaqillik qatlamlari mukammal ravishda sinxron holda ishlaydi.

image1.png

[Rasm 1] Loopin tizim arxitekturasi va web soket asosidagi ma'lumotlar oqimi

  • React Frontend: STOMP protokolidan foydalanib, server bilan bitta ulanish o'rnatilganidan so'ng, brauzer fon holatida doimiy tayyor turadi. Foydalanuvchi tomonidan kiritilgan ma'lumotlar asinxron ravishda xabar jo'natiladi va qabul qilingan voqealar komponent darajasi bo'yicha qayta chizilib, foydalanuvchilarga darhol javob beriladi.

  • Spring Boot Backend: web soket endpointlarini qabul qilish, JWT tokenlari orqali bog'lanish vaar shartli tekshirish (Interceptor), STOMP manzil tizimini boshqarish, ichki umr sikl voqealarini (Event Listener) qayta ishlashni amalga oshirib beradi.

  • Munozara bazasi: barqaror xabarlar bilan birga, real vaqtli ulanishni nazorat qilish sxemasini ichida saqlaydi, sessiya holati va sessiya tarixini ishonchli saqlab qoladi.

3. WebSocket va STOMP orqali real vaqtli xabarlashuv

3.1 STOMP sub-protokolining joriy etilish tarixi

HTML5 standart spetsifikatsiyasi bo'lgan Raw WebSocket faqat TCP soketiga oʻxshash ikki tomonlama aloqa tunnelini ochadi, lekin ma'lumotning aniq formatini yoki maqsadini belgilovchi qoidalar mavjud emas. Ya'ni, mijoz va server o'rtasida almashilgan qatorlar xabar ma'lumotimi, xonaga kirishga tayyorlanish yoki xato xabari ekanligini ajratish uchun dasturchi o'ziga xos matn qoidalarini belgilab, maxsus ishlov berishda tayyorlash kerak. Bu qiyinchiliklardan kelib chiqib, arxitektura murakkablashadi.
Loopin ushbu sarf-xarajatlarni kamaytirish uchun vebsocket freymi ustida ishlovchi STOMP (Simple Text Oriented Messaging Protocol) yordamchi protokolini to'liq joriy etdi. STOMP buyruq (COMMAND), sarlavha (Headers), tanalar (Body) shaklida shakllangan freym tuzilmasiga ega bo'lib, xabar asosida tizimning yo'nalish xaritasini aql bovar qilmay darajada intuitiv qiladi.

3.2 Spring Boot vebsocket infratuzilmasini tashkil etish kod manbasi

Spring Boot'da STOMP asosidagi ichki xabar brokerini faollashtirish va endpointlarni xaritaga joylash uchun haqiqiy implementatsiya kodi quyidagi kabi.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 프론트엔드가 핸드셰이크를 요청할 종단점 주소 매핑
        // SockJS 폴백을 적용하여 웹소켓이 차단된 프록시나 구형 브라우저 환경 지원
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS()
                .setSessionCookieNeeded(false);
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/user");
        registry.setUserDestinationPrefix("/user");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

4. Ma'lumotlar bazasi (DB) asosidagi Presence (ulanish holati) funksiyasi loyihasi

4.1 Qo'shimcha infratuzilmasiz (Redis) chiqarish va RDB tanlashning arxitektura maqsadi

Ko'p rivojlantirish qo'llanmalari real vaqtdagi sessiya ma'lumotlarini yo'qolish xususiyatiga ega bo'lganligi sababli Redis kabi o'z ichiga olgan xotira Key-Value NoSQL ma'lumotlar bazasida saqlashni tavsiya qiladi. Lekin Loopin loyihasining o'lchami va domen xususiyatlarini diqqat bilan o'rganilganida bu aniq bir resurs sarfi edi. Faqatgina munosabatlar ma'lumotlar bazasi (RDB)dan foydalanib tuzilishni loyihalashning aniq sabablari quyidagilar.

Birinchidan, oddiy tarqatish arxitekturasining saqlanish qulayligi. Yagona instansiya yoki asosiy-replika (Primary-Replica) tuzilmasidagi mavjud DB konfiguratsiyasidan foydalanish monitoring ogohlantirish tizimi, zaxira siyosatlarini birlashtirishni ta'minlaydi. Ikkinchidan, kuchli ma'lumot to'liqligi va statistik tarixiy izlanadi. Foydalanuvchi qachon kirgan va chiqqanligi haqidagi tarix oddiy holatdan ortiq ahamiyatga ega. Tizimda nosozlik yoki xavfsizlik tekshiruvida sessiya loglari tarixi zaruriydir, RDB munosabatlar sxemasi va indekslardan foydalangan holda aniq vaqt seriyasi loglarining tahlil so'rovlarini qabul qilish imkonini beradi. Uchtan, domen ma'lumotlari bilan bog'lanish qulayligi. 'Mening do'stlar ro'yxatimdagi faqat ulanishda bo'lgan a'zolarni yuqoriga joylashtirish' kabi talablarni amalga oshirganda, Redis va RDB ga ma'lumotlar ajratilsa, ilova qatlamida ikki marta IOni yuzaga keltirishi kerak va qo'lda xotira bog'lovini o'tkazishi kerak, ammo RDB yagona muhitida murakkab tuzilmasiz oddiy JOIN yoki IN subso'rov operatsiyalari orqali millisekund ichida hal etish mumkin.

4.2 Ma'lumotlar bazasi sxemasini modellashtirish

-- Presence 테이블
CREATE TABLE IF NOT EXISTS active_participant
(
    id             VARCHAR(255) NOT NULL,
    actor_id       VARCHAR(255),
    stage_id       VARCHAR(255),
    pavilion_id    VARCHAR(255),
    entity_version BIGINT       NOT NULL,
    registered_by  VARCHAR(255),
    registered_on  BIGINT       NOT NULL,
    modified_by    VARCHAR(255),
    modified_on    BIGINT       NOT NULL,
    session_id     VARCHAR(255),
    login_time     BIGINT       NOT NULL,
    CONSTRAINT pk_active_participant PRIMARY KEY (id)
);

4.3 Presence voqealarini qayd etuvchi amalga oshirish

Loopin Presence jadvalining faol hisobini real vaqt rejimida tasdiqlash usuli.

@Component
@RequiredArgsConstructor
public class PresenceEventListener {
    //
    private static final String SIMP_CONNECT_MESSAGE_HEADER = "simpConnectMessage";
    private static final String NATIVE_HEADERS = "nativeHeaders";
    private static final String SIMP_USER_HEADER = "simpUser";
    private final PresenceTask presenceTask;
    
    @EventListener
    public void handleSessionConnect(SessionConnectEvent event) {
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        log.info("### STOMP CONNECT Attempt ### SessionId: {}", headers.getSessionId());
    }

    @EventListener
    public void handleSessionConnected(SessionConnectedEvent event) {
        //
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        log.info("WebSocket Session Connected! SessionId: {}", headers.getSessionId());
        Map<String, ArrayList<String>> nativeHeaders = this.extractNativeHeaders(headers);
        
        if (nativeHeaders == null || nativeHeaders.isEmpty()) {
            throw new IllegalArgumentException("Native headers cannot be found in Socket header.");
        }
        List<String> actorId = nativeHeaders.get(SIMP_USER_HEADER);
        log.info("WebSocket Session Connected! SessionId: {}, ActorId: {}, Headers: {}", headers.getSessionId(), actorId, nativeHeaders);
        LoginEvent loginEvent = new LoginEvent(actorId.getFirst());
        presenceTask.addParticipant(headers.getSessionId(), loginEvent);
    }

    @EventListener
    public void handleSessionDisconnect(SessionDisconnectEvent event) {
        log.info("WebSocket Session Disconnect! sessionId : {}", event.getSessionId());
        presenceTask.removeParticipant(event.getSessionId());
    }

    @SuppressWarnings("unchecked")
    private Map<String, ArrayList<String>> extractNativeHeaders(SimpMessageHeaderAccessor headers) {
        //
        GenericMessage<?> gm = (GenericMessage<?>) headers.getMessageHeaders().get(SIMP_CONNECT_MESSAGE_HEADER);
        return (Map<String, ArrayList<String>>) gm.getHeaders().get(NATIVE_HEADERS);
    }
}

@Service
@Transactional
@RequiredArgsConstructor
public class PresenceTask {
    //
    private final ActiveParticipantLogic activeParticipantLogic;

    public void addParticipant(String sessionId, LoginEvent event) {
        // 1인 1세션 정책: 동일 사용자의 기존 세션 정보를 모두 정리한 후 새 세션 등록
        activeParticipantLogic.removeByActorId(event.getActorId());

        ActiveParticipantCdo cdo = new ActiveParticipantCdo();
        cdo.setActorId(event.getActorId());
        cdo.setSessionId(sessionId);
        cdo.setLoginTime(System.currentTimeMillis());
        activeParticipantLogic.registerActiveParticipant(cdo);
    }

    public LoginEvent getParticipant(String sessionId) {
        ActiveParticipant activeParticipant = activeParticipantLogic.findBySessionId(sessionId);
        if (activeParticipant == null) {
            return null;
        }
        return new LoginEvent(activeParticipant.getActorId());
    }

    public void removeParticipant(String sessionId) {
        ActiveParticipant activeParticipant = activeParticipantLogic.findBySessionId(sessionId);
        if (activeParticipant != null) {
            activeParticipantLogic.removeActiveParticipant(activeParticipant.getId());
        }
    }

    public Map<String, LoginEvent> getActiveSessions() {
        return activeParticipantLogic.findActiveParticipants(null).stream()
                .collect(Collectors.toMap(
                        ActiveParticipant::getSessionId,
                        ap -> new LoginEvent(ap.getActorId())
                ));
    }

    public boolean isCitizenOnline(String citizenId) {
        return activeParticipantLogic.existsByActorId(citizenId);
    }
}

5. Xulosa va tahlil (Xulosa va Retrospective)

Mavjud Smalltalk messenjer tizimini zamonaviy Vizend platformasining Loopin messenjer xizmatini qayta qurish ishlari katta va qimmatli tashqi tarqatish kesh qatlamisiz, murakkab indeks strategiyasi va aqlli tuzilgan o'zaro tasdiqlovchi sessiya loglari jadvali sxemasi dizayni bilan ishonchli va ma'lumot ko'rinishini yuqori darajada ta'minlashga muvaffaq bo'ldik.

Ushbu loyiha WebSocket va STOMP'dan foydalanib real vaqt rejimida xabar berishning asosiy oqimini o'zimiz amalga oshirib, framework ortida sessiya qanday boshqarilishini, WebSocket'da sessiya uzilishi istisnolarini qanday boshqarishni ko'rib chiqib, real vaqtli aloqa oqimlarini boshidan ohista anglay olish imkoniyatini berdi. Ajoyib zamonaviy texnologiyalarga quvib yetishdan ko'ra, hozirgi talablar va xarajatlarga mos ma'lumotlar bazasi bilan muammolarni hal qilishda, oddiy arxitekturaning loyiha saqlanishining afzalliklarini yana bir bor o'rganish imkoniyatiga ega bo'ldik.

BigJumbo

Site footer