Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Algorytmika Inżynieria oprogramowania Linux Programowanie Programowanie webowe Utrzymanie oprogramowania

Jabber jako protokół wymiany wiadomości

Cześć! Cieszę się, że mnie odwiedziłeś/aś. Zanim przejdziesz do artykułu chciałbym zwrocić Ci uwagę na to, że ten artykuł był pisany kilka lat temu (2015-10-26) miej więc proszę na uwadzę że rozwiązania i przemyślenia które tu znajdziesz nie muszą być aktualne. Niemniej jednak zachęcam do przeczytania.

Oj dawno tu nic nie pisałem.. Niestety praca, uczelnia, dziewczyna i życie zabierają mi ponad 100% doby co nie wróży najlepiej dla bloga ;) aczkolwiek, dzieląc pisanie na kilka dni – udało się stworzyć coś nowego!

W pracy (nowej pracy ;) ) jednym z głównych narzędzi jest protokół SIP. Służy on do inicjalizacji sesji np. głosowej czy video. Gdy taka sesja zostanie zainicjowana to konkretne ramki przesyłające strumień wideo i/lub dźwięku lecą już protokołem RTP. Używane przy zastosowaniach VOIPowych.

Tak w skrócie wygląda zestawianie sesji SIP:

Inicjalizacja to wysyłanie i otrzymywanie pakietów o konkretnej treści w konkretnej kolejności. Przykładowo INVITE wygląda tak:

1
2
3
4
5
6
7
8
9
INVITE sip:[email protected] SIP/2.0
Via: SIP/2.0/UDP pc33.server1.com;branch=z9hG4bK776asdhds Max-Forwards: 70
To: user2 <sip:[email protected]>
From: user1 <sip:[email protected]>;tag=1928301774
Call-ID: [email protected]
CSeq: 314159 INVITE
Contact: <sip:[email protected]>
Content-Type: application/sdp
Content-Length: 142

Jest to pierwszy pakiet który wysyła osoba inicjalizująca sesję. I oczekuje na konkretną odpowiedź (wg. obrazka wyżej).

Dla SIPa/RTP serwerem może być Asterisk czy np. FreeSwitch.

Dane mi było napisać kilka linijek obsługujących webową implementację SIPa (over WebSocket) i działa to całkiem przyzwoicie – używając tylko przeglądarki mogę wykonać połączenie na jakiś telefon fizyczny, np. na swoją komórkę. Implementacja webowa używa oczywiście głośników i mikrofonu ;)

Sesja SIP jest inicjalizowana przez WebSocket (ramki lecą przez TCP zamiast UDP jak jest w np. softphone’ach) – docierając do Asteriska który pozwala zestawić kanał a głos leci przez WebRTC.

Jako że ten temat jest całkiem ciekawy, poszedłem nieco dalej i zainteresowałem się Jabberem. W odróżnieniu od SIPa, Jabber jest nieco bardziej „standardowym” protokołem, gdyż jako format danych używa XMLa.

Jabber jest typowo tekstowym protokołem, tj. umożliwia wysyłanie wiadomości natychmiastowych. Według wikipedii implementację tego protokołu posiada np. Facebook czy Google w swoich czatach. Gdy zobaczyłem że formatem danych jest XML to nieco zbladłem. Jestem „pokoleniem” JSONa, więc wypieram XMLa jakoś tak instynktownie, co w sumie nie jest słuszne. Toż to nawet lekka dyskryminacja.. ;)

Używając Vagranta postawiłem CentOSa 6.5 i na nim Jabberowy serwer (Javowy) – OpenFire. Nie obyło się bez lekkich problemów – jak to na Linuxie. Ale po jakiś 30 minutach serwer działał.

Zabrałem się więc za szukanie biblioteki do obsługi tego protokołu. Znalazłem Strophe i zacząłem pisać. Zacząłem od abstrakcji „ConnectionManager”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var ConnectionManager = function (serviceUrl) {
    this.url = serviceUrl;
    this.emptyListener = function (status) {
        console.log("Event " + ConnectionManager.events[status] + " has been triggered without defined listener!");
    };
    this.listeners = {};
    this.sessionListeners = {};
};
ConnectionManager.events = ["onError", "onConnecting", "onConnFail", "onAuthenticating",
    "onAuthFail", "onConnected", "onDisconnected", "onDisconnecting", "onAttached"];

ConnectionManager.sessionEvents = ["message"];

ConnectionManager.prototype.addEventListener = function (eventName, func) {
    var index = ConnectionManager.events.indexOf(eventName);
    if (index > -1) {
        this.listeners[index] = func;
    }
    var sessionIndex = ConnectionManager.sessionEvents.indexOf(eventName);
    if (sessionIndex > -1)
        this.sessionListeners[sessionIndex] = func;
};

ConnectionManager.prototype.connect = function (user, server, password) {
    this.connectionObj = new Strophe.Connection(this.url, {protocol: "ws"});
    var that = this;
    this.connectionObj.connect(user + '@' + server, password, function (status, opts) {
        if (status === Strophe.Status.CONNECTED) {
            var keys = Object.keys(that.sessionListeners);
            for (var i = 0; i < keys.length; i++) {
                var key = keys[i];
                that.connectionObj.addHandler(that.sessionListeners[key], null, ConnectionManager.sessionEvents [key], null, null, null);
            }
        }
        (that.listeners[status] || that.emptyListener)(status, opts);
    });
};

ConnectionManager.prototype.send = function (obj) {
    if (obj) {
        this.connectionObj.send(obj);
    }
};

Napisałem tutaj przede wszystkim obsługę listenerów. Sama Strophe trochę ułomnie (moim zdaniem) to robi – jest jeden listener do wszystkich eventów dla połączenia – rozdzieliłem je. Natomiast listenery do obsługi wiadomości już są podpinane inną funkcją. Ujednoliciłem to. Jabber pozwala na to żeby ustawiać sobie statusy. Jak na gadu gadu. Dopisałem więc obiekt status wraz z metodą fabrykującą żądanie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Status = function (type, status) {
    var availableTypes = ["chat", "away", "dnd"];
    if (availableTypes.indexOf(type) > -1) {
        this.type = type;
        this.status = "" || status;
    } else {
        throw "Invalid type!";
    }
};

Status.prototype.prepare = function () {
    var presence = $pres().c("show").t(this.type).
            up().c("status").t(this.status).
            up().c("priority").t(127);
    return presence;
};

Prepare() buduje obiekt XML w formacie:

1
<presence xmlns='jabber:client'><show>chat</show><status>:)</status><priority>127</priority></presence>

Gdzie element show to typ statusu – tutaj jest „chat” – taki odpowiednik „chętny do rozmowy” z GG. Element status to opis, tak, taki z GG. Priority to sposób na rozwiązanie problemu z zalogowanym jednym użytkownikiem na wielu klientach – priorytet określa do którego klienta ma wiadomość zostać wysłana.

Potem, analogicznie napisałem obiekt do wiadomości:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Message = function (mess, to) {
    this.to = to;
    this.message = mess;
};

Message.prototype.prepare = function () {
    var msg = $msg({
        to: this.to+'@192.168.33.11',
        type: 'chat'
    }).cnode(Strophe.xmlElement('body', this.message)).up()
            .c('active', {xmlns: "http://jabber.org/protocol/chatstates"});
    return msg;
};

Ten tutaj generuje natomiast XMLa podobnego do:

1
<message to='[email protected]' type='chat' xmlns='jabber:client'><body>sd</body><active xmlns='http://jabber.org/protocol/chatstates'/></message>

W przykładzie wysyłam wiadomość o treści „sd” (niech żyje kreatywność!) do użytkownika „test2” zarejestrowanego na serwerze 192.168.33.11 – to moja wirtualka z CentOSem.

Zerknijmy teraz na chwilę na panel serwera..

users_of

main_of

sessions_of

Sam serwerek jest bardzo user-friendly. Udało mi się w nim dodać wtyczkę która zastępuję domyślny protokół komunikacji z HTTP (AJAX) na TCP (WebSocket). A jeszcze inna wtyczka udostępnia np. API Restowe do między innymi dodawania użytkowników, tworzenia konferencji itp.

Kolejne dwie godziny zajęło mi zaimplementowanie małego testu mojego kodu. Testem nazywam zalogowanie na tej samej stronie dwóch innych użytkowników i przeprowadzenie rozmowy. Oto wynik:

of_test

Dałem też na zrzucie podgląd WS – tak żeby nie było że JSem sobie przepisuje wartości textarea ;)

Krótki test pokazał że w miarę łatwo można wykorzystać Jabbera do własnych rozwiązań. Zarządzanie rosterami (listą kontaktów) można obsługiwać po stronie OpenFire jak i po stronie klienta, np. dociągać z innej bazy w ramach integracji z jakąś platformą. OF prosi przy instalacji o wybór bazy danych wybrałem Postgresa 9.4 – gdzie po instalacji znajdziecie ciekawą tabelę o nazwie mówiącej chyba wszystko co potrzeba.. „ofmessagearchive”:

of_archive

Co prawda API Restowe chyba nie daje dostępu do archiwum ale napisanie czegoś do SELECTowania tych wierszy nie jest niczym skomplikowanym, w dodatku że body wiadomości mamy wyłuskane.

Zdaję sobie sprawę że Jabber nie jest nową technologią i nic tym wpisem szalonego nie odkryłem, ale pokazałem że wystarczy ~100 linijek JSa żeby móc połączyć się z serwerem i stworzyć namiastkę stabilnego czatu :)

Więc chyba fajnie :)

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

4 komentarzy
Inline Feedbacks
View all comments
SpeX

A ja ostatnio myślałem, na czym podobnym jeśli chodzi o połączenie jabbera i SIP, ale w drugą stronę. By wykorzystać statusy jabbera, ale informację dla centralki czy agent jest przy komputerze, a co za tym idzie czy można mu przekazać połączeniem. Bo logiczne jeśli komputer jest wyłączony (status niedostępny) lub agent odszedł od komputera (włączył się wygaszać ekranu/ekran został zablokowany; status zaraz wracam) nie ma sensu przekazywać tam połączenia.

SpeX

W sensie w czasie aktywnego połączenia SIP (wychodzącego lub przychodzącego) jabber dostawałby DND? Ja tylko założyłem wykorzystanie Jabbera do detekcji obecności agenta (bo nie mam innego pomysłu na taką detekcje). Nie zakładałem większego wykorzystania jabbera (w tym w jego pierwotnego przeznaczenia).