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:user2@server2.com SIP/2.0
Via: SIP/2.0/UDP pc33.server1.com;branch=z9hG4bK776asdhds Max-Forwards: 70
To: user2 <sip:user2@server2.com>
From: user1 <sip:user1@server1.com>;tag=1928301774
Call-ID: a84b4c76e66710@pc33.server1.com
CSeq: 314159 INVITE
Contact: <sip:user1@pc33.server1.com>
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='test2@192.168.33.11' 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..

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:

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”:

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
Mateusz M.

Pokaż komentarze

  • 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.

    • Jeśli w dodatku status po odebraniu połączenia by się zmieniał na dnd np. np to faktycznie może to być niezły pomysł :D

      • 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).

        • Jeśli wyślesz dnd w momencie odebrania to unikniesz przydzielenia połączenia jak już będzie miał połączenie inne :D

Ostatnie wpisy

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More

2 miesiące ago

Podsumowanie: kwiecień 2024

Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More

6 miesięcy ago

Podsumowanie: luty i marzec 2024

Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More

7 miesięcy ago

Podsumowanie: styczeń 2024

Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More

9 miesięcy ago

Podsumowanie roku 2023

Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More

10 miesięcy ago

Podsumowanie: grudzień 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More

11 miesięcy ago