V první části tohoto článku jsme si popsali princip realtime komunikace mezi serverem a klientem (Java applet) přes firewall. V tomto si ukážeme konkrétní implementaci jedné takové aplikace. Bude se jednat o jednoduchý applet pro chatování, který bude mít okamžitou odezvu i za firewallem. Server bude implementován pomocí PHP+MySQL, klient jako Java applet.

Princip realtime komunikace přes firewall je podrobně popsán v minulém článku, proto ho zde nebudeme dále rozebírat, zaměříme se pouze na problematické části.

Popis programu

Naše aplikace je jednoduchý program pro chatování (on–line psaný rozhovor po internetu). Podotýkám, že se jedná o ukázkové řešení, proto je zjednodušeno tak, aby byl kód co nejčitelnější a neobsahuje standardní mechanismy. Např. nebudeme používat uživatelské přihlašování, takže nebude zřejmé, kdo který text napsal. Rozšíření o možnost přihlašování by se řešilo klasicky a vzhledem k našemu problému není relevantní.

Funkce programu bude tedy následující: klienti přihlášení k chatovacímu serveru na něj mohou poslat text. Každý poslaný text (od libovolného uživatele) se zobrazí všem přihlášeným klientům (včetně autora) v okně.

Pro komunikaci jsou k dispozici 2 zprávy – jedna směrem od klienta k serveru (psaní zprávy), jedna směrem od serveru ke klientům (čtení zprávy).

Server

Server je řešen jako tři PHP skripty a databáze MySQL, do které jsou ukládány texty. Pro každý typ zprávy je jeden PHP skript, jeden je společný, definující přístup k databázi.

Datový model

Databáze MySQL obsahuje jednu tabulku realchat. Ta může být uložena v libovolné databázi, buď si založíte pro náš účel speciální, nebo ji uložíte do existující databáze (např. test). Tabulka obsahuje sloupce ID a TEXT. Tabulku můžete vytvořit pomocí následujícího skriptu.

Soubor dbinit.sql:

#
# Struktura tabulky `realchat`
# pro real time komunikaci (i) přes firewall z Java appletu
#
DROP TABLE IF EXISTS realchat;
CREATE TABLE realchat (
  ID int(11) NOT NULL auto_increment,
  TEXT varchar(255) NOT NULL default “,
  PRIMARY KEY  (ID)
) TYPE=MyISAM;

Přístup k databázi zajišťuje skript databaze.php, ve své vlastní implementaci je nutné si nastavit adresu MySQL serveru, uživatele, heslo a jméno databáze. V tomto příkladu jsou nastaveny takové údaje, které můžete použít, pokud spouštíte server lokálně.

Soubor databaze.php:

<?
$db_server = „localhost“;  // adresa MySQL serveru
$db_user = „“;             // uživatel MySQL
$db_passwd = „“;           // heslo uživatele
$db_name = „test“;         // jméno databáze
mysql_connect($db_server, $db_user, $db_passwd) or die(„Nelze se připojit k databázi!“);
mysql_select_db($db_name) or die(„Nelze se připojit k databázi!“);
?>

Server – psaní zpráv

Komunikace od klienta k serveru je jednodušší. Je řešena skriptem pisZpravu.php. Text je předán parametrem text a skript ho pouze uloží do databáze.

Soubor pisZpravu.php:

<?
header(„Content-type: text/plain“);
include „databaze.php“;
mysql_query („INSERT INTO realchat (TEXT) VALUES (\“$text\“);“);
echo „OK“;
?>

Server – čtení zpráv

Komunikace od serveru ke klientovi je řešena skriptem ctiZpravu.php. Pokud ho klient vyvolá bez parametru posledniZprava, je to chápáno jako první přihlášení a server pošle jako obsah dokumentu číslo poslední uložené zprávy-textu.

Pak již klient volá skript s parametrem posledniZprava (číslo, které obdržel od serveru). Pokud je tento parametr definovaný, posílá server všechny zprávy, které jsou k dispozici a mají vyšší ID než je parametr. Zpráva je vždy na dvou řádcích, první obsahuje její číslo, druhý její text. Pokud není k dispozici žádná zpráva, server čeká a každou sekundu zjišťuje, jestli nějaká zpráva nepřibyla. Princip této komunikace je podrobně rozebrán v prvním díle tohoto článku.

Metodou set_time_limit() je nutné zvýšit maximální časový limit pro běh skriptu, standardní hodnota 30 je pro náš účel nedostatečná (skript by skončil chybou).

Soubor ctiZpravu.php:

<?
include „databaze.php“;
$TIMEOUT=30; // nastavení časového limitu
if ($posledniZprava==““) {    // pokud klient klade první dotaz
  $row = mysql_fetch_array (mysql_query („select max(ID) from RealChat;“));
  echo $row[0];
  return;
  }
$res = mysql_query („select * from RealChat where ID>“.$posledniZprava.“ ORDER BY ID ASC;“);
if ($res==0) { echo „Error! (res=0)“; return; }         // problém se čtením z databáze
set_time_limit (100);       // maximální doba na zpracování skriptu (poté PHP skript ukončí)
$i = 0;
while (mysql_num_rows($res) == 0) {
  if ($i==$TIMEOUT) { echo „NONE“; return; }
  sleep(1) // čekání 1 sekundu, aby se nepřetěžoval DB server stálými dotazy
  $res = mysql_query („select * from RealChat where ID>“.$posledniZprava.“ ORDER BY ID ASC;“);
  if ($res==0) { echo „Error! (res=0)“; return; }
  $i++;
  }
while ($row = mysql_fetch_row($res)) { // výpis výsledků – texty, které jsou k dispozici (nemusí být k dispozici žádný)
  echo $row[0].“\n“;
  echo $row[1].“\n“;
  }
?>

Klient

Klient je implementován jako Java Applet napsaný v prosředí Borland JBuilder. Používá standardní uživatelské rozhraní AWT a skládá se ze dvou tříd. Třída ApplerRC reprezentuje applet a zajišťuje reakci na činnost uživatele. Odeslání zprávy s napsaným textem zajišťuje metoda button1_actionPerformed(). Třída také spouští vlákno zpravyDovnitr (reprezentované v souboru zpravyDovnitr.java), které zajišťuje komunikaci směrem od serveru ke klientu.

Při posílání HTTP dotazu je třeba založit objekt java.net.URL, poté se metodou openConnection() vytvoří třída java.net.URLConnection. Vlastní spojení se naváže metodou java.net.URLConnection.connect(). Pokud chceme psát do otevřeného spojení, metodou URLConnection.getInputStream() získáme proud, do kterého lze zapisovat. Data ze spojení čteme pomocí proudu URLConnection.getInputStream().

Soubor ApplerRC.java:

package realchat;
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.io.*;
import java.net.*;
/**
* Title:        Klient programu RealChat pro real-time komunikaci serveru a klienta přes firewall
* Description:  Ukázkový program k článku Real-time komunikace přes firewall
* Copyright:    Copyright (c) 2002
* Company:      Interval
* @author       Jiří Semecký (jiri.semecky@interval.cz)
* @version      1.0
*/
public class AppletRC extends Applet {
  String host;
  BorderLayout borderLayout1 = new BorderLayout();
  Panel panel1 = new Panel();
  Label label1 = new Label();
  TextField textField1 = new TextField();
  Button button1 = new Button();
  TextArea textArea1 = new TextArea();
  BorderLayout borderLayout2 = new BorderLayout();
  /**Construct the applet*/
  public AppletRC() {
  }
  /**Initialize the applet*/
  public void init() {
    try {
      host = this.getParameter(„HOST“);
      if (host==null) {
        System.out.println(„Nastavte parametr HOST na serverk, ke kteremu se pripojujete“);
        }
    }
    catch(Exception e) {
      e.printStackTrace();
    }
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }
  /**Component initialization*/
  private void jbInit() throws Exception {
    this.setLayout(borderLayout1);
    label1.setText(“  text:  „);
    textField1.setText(„“);
    textField1.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(ActionEvent e) {
        textField1_actionPerformed(e);
      }
    });
    button1.setLabel(„piš“);
    button1.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(ActionEvent e) {
        button1_actionPerformed(e);
      }
    });
    panel1.setLayout(borderLayout2);
    this.add(panel1, BorderLayout.NORTH);
    panel1.add(label1, BorderLayout.WEST);
    panel1.add(textField1, BorderLayout.CENTER);
    panel1.add(button1, BorderLayout.EAST);
    this.add(textArea1, BorderLayout.CENTER);
  }
  void button1_actionPerformed(ActionEvent e) {
    try {
      URL url = new URL(host+“pisZpravu.php?text=“+URLEncoder.encode(textField1.getText()));
      URLConnection connection = url.openConnection();
      connection.connect();
      connection.getInputStream();
      textField1.setText(„“);
      } catch (IOException ioe) { System.out.println(„Došlo k chybě při spojování se serverem“); }
    }
  void textField1_actionPerformed(ActionEvent e) {
    button1_actionPerformed(e);
  }
  public void start() {
    zpravyDovnitr prijem = new zpravyDovnitr(this);
    prijem.run();
    //Thread vlakno = new Thread(prijem);
    //vlakno.run();
}
  public void main(String par[]) {
  }
}

Soubor zpravyDovnitr.java:

package realchat;
import java.net.*;
import java.io.*;
/**
* Title:        Klient programu RealChat pro real-time komunikaci serveru a klienta přes firewall
* Description:  Ukázkový program k článku Real-time komunikace přes firewall
* Copyright:    Copyright (c) 2002
* Company:      Interval
* @author       Jiří Semecký (jiri.semecky@interval.cz)
* @version      1.0
*/
/**
* Toto vlákno se cyklicky dotazuje, zda není k dispozici nějaká zpráva od serveru.
* Když se mu vrátí odpověď (ať kladná nebo záporná), okamžitě posílá další dotaz.
* Je na serveru (PHP), aby zajistil, aby se data po síti neposílala příliš často a odpověď
* patřičně zdržel (pokud nemá k dispozici zprávu, přirozeně).
*/
public class zpravyDovnitr extends Thread {
  private AppletRC appletRC;
  public zpravyDovnitr(AppletRC appletRC) {
    this.appletRC = appletRC;
  }
  public void run() {
    int posledniZprava = -1;
    URL url;
    URLConnection connection;
    DataInputStream vstup;
    String radka;
    while (true) {
      try {
        if (posledniZprava<0) url = new URL(appletRC.host+“ctiZpravu.php“);
                         else url = new URL(appletRC.host+“ctiZpravu.php?posledniZprava=“+String.valueOf(posledniZprava));
        } catch (java.net.MalformedURLException e) {System.out.println(„Sparna adresa, applet konci!“); appletRC.stop(); break;}
      try {
        connection = url.openConnection();
        connection.setDoInput(true);
        connection.connect();
        vstup =  new DataInputStream (connection.getInputStream());
        } catch (java.io.IOException e) {System.out.println(„Chyba v sitove komunikaci, applet konci!“); appletRC.stop(); break;}
      if (posledniZprava<0) {
        try {
          connection.connect();
          radka = vstup.readLine();
          } catch (java.io.IOException e) {System.out.println(„Chyba v sitove komunikaci, applet konci!“); appletRC.stop(); break;}
        try {
          posledniZprava = Integer.valueOf(radka).intValue();
          } catch (java.lang.NumberFormatException e) {System.out.println(„Spatny format cisla „+radka); appletRC.stop(); break;}
        System.out.println(„posledni zprava nastavena na „+posledniZprava+ „(„+radka+“)“);
        } else {
        try {
          connection.connect();
          while (vstup.available()>0) {
            radka = vstup.readLine();
            if (radka.equals(„NONE“)) {
              System.out.println(„Doslo k timeoutu, pokracuje se…“);
              continue;
              }
            posledniZprava = Integer.valueOf(radka).intValue();
            System.out.println(„posledni zprava nastavena na „+posledniZprava+ „(„+radka+“)“);
            radka = vstup.readLine();
            appletRC.textArea1.setText(radka+“\n“+appletRC.textArea1.getText());
            System.out.println(„vypsan text „+radka);
            }
          } catch (java.io.IOException e) {System.out.println(„Chyba v sitove komunikaci, applet konci!“); appletRC.stop(); break;}
        }
      }
  }
}

Zdrojové kódy si můžete stáhnout.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět