[ river @ 14.01.2004. 22:55 ] @
Posto sam pokrenuo pitanje zivosti ovog foruma evo sada cu probati da budem
i malo konstruktivan, pa cu u ovoj temi probati da prenesem neka iskustva iz
projekata na kojima radim.

Bilo je tesko odabrati temu za prvi post, nekako ne mogu da se
skoncentrisem, i svaka tema mi nekako bude mnogo opsirna, ali evo probacu sa
jednim zanimljivim network programing problemom koji sam imao.

ZASTO KORISTITI ALIVE (PING) PAKETE U IMPLEMENTACIJI PROTOKOLA PREKO TCP/IP
KONEKCIJE

Projekat na kome sam tada radio je bio server aplikacija koja je
komunicirala sa nekim telekom sistemima putem TCP/IP konekcije. E sad svi iz
skole znamo da je UDP nestabilan protokol, a TCP stabilan. Svi se secate
onog uporedjivanja sa radio prenosom i telefonskim razgovorom. Znaci sa time
i dobro potkovani threading-om i java.net.* paketom krenemo u posao
kodiranja proprieti protokola.

Posle nekoliko nedelja svi protokoli (a bilo ih je 4 ako se dobro secam) su
bili implementirani i server je posle nedelju dana stress testing-a pusten u
probni rad u production okruzenju.
Deployment je radjen na 3 masine (3 servera, nisu bili u nikakvoj vezi). Sve
radi lepo, ali posle nedelju dana vidimo da se jedna od implementacija
(konektora) zaglupljuje u nekim nedefinisanim uslovima. Jednostavno proces
nam ode na 100% utilizacije CPU vremena. Prvo probamo onu staru
programersku - pokreni ponovo. I on stvarno gle cuda pocne da radi ok, ali
samo nekih 3 dana i opet isto. Krenemo mi onda u potragu za greskom, i
nadjemo na liniju gde je neko koristio BufferedInputStream.readLine() za
citanje podataka. Sad tamo pise da bi metod trebalo da vrati String ili da
ispali Exception ukoliko je EOF. Pogledamo kod u petlji i vidimo da tamo ima
nesto kao

try {
while (!done) {
String ret = bufferedStream.readLine();
if (ret == null)
continue;
...
}
catch (IOException io) {
// ...
}

Upravo se tu kod zaglupljuje. Za�to ne ispaljuje gresku??? Mi na net tamo
dosta tekstova o tom Java SDK bug-u. Kazu nekada readLine() metod kada
nastupi EOF ne vrati gresku nego samo vrati null string. Znaci to je
problem.

Svi pljuju po sun-u i po implementaciji, a mi posto nam ova petlja gore
treba resimo da se isprobamo sa obicnom inputstrem klasom.

Promenimo samo nekoliko redova i dodjemo do necega ovakvog:

try {
int c = -1;
while ((c = in.read()) != -1) {
// ovde radimo posao
// u stvari samo bufferujemo (char) c u StringBuffer
}
catch (IOException ex) {
}

Ovo odradi posao. Server radi. Posle 10 dana server jos uvek radi, ali tek
tada jedan od nasih DB administratora primeti da u nekoliko zadnjih dana
nema poruka sa tog konektiviti software-a u bazi. Sta je sad. Mi ponovo
stavljamo debuging kackete na glavu, ali nista kod nas sve radi, a u
production okruzenju zaglupljuje se. Mi onda uz pomoc remote-debuging-a (sto
je prica za sebe i za neki drugi post) otkrijemo da je socket konekcija
zapravo zatvorena, ali da Socket klasa u javi to ne primecuje. Jednostavno
ostane da visi u tom read() metodu. Pocnu onda pretrage po netu, i onda
linux frakcija pocne da hvali linux jer tamo i posle tih 10 dana server
radi, a iz JDK sourca smo videli da se zatvaranje koda ne detektuje u native
metodi. Mada im to nije pomoglo mnogo jer je i linux server stao posle nekih
3 dana.

Naravno nije dolazilo u obzir da klijentu kazemo da mora da zameni Windows
box Linuxom ili Unix-om (ne daj boze, jer njegovi administratori neznaju to
da administriraju).

Nisam spavao tri noci zbog ovoga, i onda odjednom, setim se programiranja
socket-a u C-u. Secam se nekog teksta koji kaze da su socketi u stvari
nestabilna konekcija, setim se paketa. Onda brzo u kancelariju, pa
specifikaciju u ruke, a tamo lepo stoji opis ALIVE paketa koji je bio u
protocol flow introduction sekciji. Onda, posle dva tri "#$%, na glas dodam
thread koji koristi ALIVE paket i na taj nacin otkriva laznu konekciju u
situaciji kada java to ne otkrije. Normalno posle ovoga server je proradio
besprekorno. I radi od tada bez prestanka (barem na Linux racunarima, na
windowsu su instalirana bar 2 service packa od tada).

E sada ako nekoga zanima sasto se sve ovo desavalo. Koja je to greska u
implementaciji jave, da probam da objasnim. Greska u javi je ta sto kad u
nju dodjes iz C-a budes ocaran kako ona radi 90% stvari automatski za tebe.
Uz tu lenjost dosta ljudi prihvata TCP protokol kao stabilan stream , a za
to je delimicno kriv i Tutorial na sunovom sajtu i sve knjige koje na brzinu
prelete preko socket programiranja. Niko ne baca drvlje i kamenje na tvorce
C-a ili C++-a ili MFC-a iako ovo ponasanje socketa i tamo moze da se
detektuje. To je zato sto kad radis u C-u uvek polazis od toga da ti nesto
nisi uradio dobro i trazis svoju gresku, a sa javom, vb-om i ostalim ostrim
(sharp) jezicima uvek ocekujes da je greska u kompajleru ili VM zbog
tekstova tipa Java je s*****, c# je kopija jave, ovo je sporo i td.

Elem da se vratim na temu. Problem je u slede�em. TCP protokol nije stream,
kao sto mnogi misle. Kada imate otvoren socket vi nemate stalni protok
podataka nego je to samo iluzija. E sad kada zatvarate socket to rezultuje
SOCKET_CLOSE paketom koji se �alje drugoj strani. Vas socket primi signal i
vama onda greskom ili kako god signalizira da je druga strana otisla u void.
Ali sta ako je racunar na drugoj strani resetovan, sta ako je Windows na
dugoj strani. Onda nema nikoga na drugoj strani da vam posalje obavestenje,
tako da vas socket ima iluziju da druga strana postoji jos uvek i on ceka.

OK, ali zasto onda TCP zovu stabilnim protokolom? To je zato sto je redosled
pristiglih paketa zagarantovan. Nikada vam poslednje poslati paket nece
stici pre prvog. U stvari i to je moguce, ali vi to necete primetiti. Ali
stoji sledece nikad niste sigurni da li je konekcija ziva dok ne probate da
je koristite.

Kada shvatite to svi vasi network enabled programi ce biti mnogo robusniji i
mnogo stabilniji.

P.S

Za one koji se mozda pitaju, na stuprotnoj strani od naseg servera nije bio
windows bio je UNIX, a problem je nastajao zato sto je jedan od cisco
routera u cvoristu koje klijent koristi u trenutcima velikog loada, gutao
SOCKET_CLOSE pakete. U ovo se vec ne razumem, ali znam da mi je mnogo
glavobolja zadavalo.


P.P.S

I nije bilo strasno za prvi put. Probacu jos koji, pa ako ima interesovanja
mozda ovo bude i neki Java tutorial na srpskom.

[ Java Beograd @ 15.01.2004. 08:32 ] @
Svaka cast, majstore. Samo napred.
[ tweeester @ 15.01.2004. 08:59 ] @
Hvala na podeljenom iskustvu.
[ Gojko Vujovic @ 15.01.2004. 09:25 ] @
Zanimljiva priča, bravo. ;)

Dodao bih samo nešto po ovom pitanju:

Citat:
river:
OK, ali zasto onda TCP zovu stabilnim protokolom? To je zato sto je redosled
pristiglih paketa zagarantovan. Nikada vam poslednje poslati paket nece
stici pre prvog. U stvari i to je moguce, ali vi to necete primetiti.


TCP se smatra reliable protokolom zato što pruža upravo to - pouzdanu konekciju. Osnovna funkcija koja ga čini "pouzdanim" (reliable) je ERROR RECOVERY koji je implementiran u samom TCP-u i ne zahteva od korisnika (najčešće nekog layer 7 protokola) dodatne napore po ovom pitanju. Osim običnog error checkinga koji je čest kod mnogih protokola, treba razlikovati error recovery funkcionalnost koja obezbeđuje RETRANSMISIJU segmenata koji nisu primljeni u određenom roku. (dok error checking obezbeđuje samo detektovanje greške i odbacivanje loše primljenih podataka, ali ne i ponovno slanje). Unreliable protokoli nemaju garanciju isporuke, takvi su udp ili ip na primer - oni se vode "best effort to deliver" metodom, a reliability mora da implementira neki protokol višeg sloja, bilo to tcp ili sama aplikacija.

Inače tačno je to što kažeš da TCP i segmentira podatke i numeriše segmente radi reorderinga na receiving delu, to je funkcija segment reordering i doprinosi reliability-ju koji u stvari primarno pruža ova opisana error recovery funkcija.
[ filmil @ 15.01.2004. 09:49 ] @
Citat:
TCP se smatra reliable protokolom zato što pruža upravo to -
pouzdanu konekciju. Osnovna funkcija koja ga čini "pouzdanim" (reliable)
je ERROR RECOVERY koji je implementiran u samom TCP-u i ne zahteva od
korisnika


U stilu onog crtaća „svega ovoga ne bi bilo da je Pera...“, dopunio bih
priču iz ugla koji se oslanja na teoriju informacija. Naime, nijedan
protokol za komunikaciju nije pouzdan u smislu da obezbeđuje isporuku
paketa sa 100% ispravnosti: da pošaljete paket od 100 bajtova i dobijete
100 istih tih bajtova. Takav prenos je, inače, često fizički
nemoguć, dakle sama priroda nam stoji na putu.

Komunikacije se umesto toga oslanjaju na poznavanje kapaciteta kanala i
na kodiranje poruka tako da je verovatnoća greške mala. Postoje
razni mehanizmi za obezbeđivanje ovakvog prenosa, neki su bolji neki
gori. TCP daje neke garancije na kvalitet usluge, koje recimo UDP ne
daje. Zbog toga, i samo zbog toga se u žargonu TCP naziva pouzdanim
(reliable) protokolom: jer nudi nešto što drugi protokol nema. Ali to ni
u kom slučaju ne može biti 100% garancija bilo čega u vezi sa
greškama u prenosu i atribut reliable nikako ne sme tako da se shvati. U
ovom slučaju on je samo skraćena oznaka za „isporuku po redosledu i
kontrolu protoka retransmisijom“.

Mislim da problem koji opisuje OP upravo dolazi od toga što ljudi lako
prenebregavaju ovu finesu. Možda je meni bilo jasnije o čemu se radi
pošto na TCP uglavnom gledam „odozdo“, tek ne treba se čuditi ako
program koji se gradi na pogrešnim pretpostavkama ne radi onako kako se
očekuje. Greška dakle ne leži u Javi.

f
[ river @ 15.01.2004. 12:54 ] @
Dobro je proslo. Iskreno da vam kazem ocekivao sam da ce me docekati
odgovori tipa bezi odavde, mi sve to vec znamo. A ovako super. Hvala ljudi
ohrabrili ste me, pa ako vam ne smeta, ja cu i u buduce pokusati da pisem
ovako nesto.

Ono za TCP vc UDP i nacin rada. Ma znali smo mi to, ali problem je sto smo
ocekivali da ce neko drugi da radi umesto nas.

E sad reliable ili ne? Za potrebe nase price nije bio reliable protokol.

P.S.
U buduce da ne bude zabune, vise cu se koncentrisati na ono sto je bitno za
pricu i pokusavati da pisem jezikom koji je svima razumljiv, cak i onima
koji su tek poceli da programiraju.

I jedno pitanje? Kako je najbolje da nastavim ovu seriju tekstova? Da li je
bolje postovati ovde u ovoj temi, ili praviti novu temu za svaki tekst
posebno?

[ river @ 17.01.2004. 14:44 ] @
CLASSPATH i custom ClassLoader

Svako ko je napravio iole slozeniju aplikaciju zna kakav haos moze da
postane
komanda kojom startujete vasu aplikaciju. Mislim na java komandu i -cp
switch.

Verovatno vam se zadesilo da imate neku strukturu direktorijuma gde je
"instalirana" vasa java aplikacija. Vasa struktura je verovatno vama vrlo
logicna, i mogla bi da izgleda nesto kao:

Code:

APP_HOME
+-- LIB
+-- THIRD_PARTY
+-- CONFIG
+-- LOG
+-- DB
+-- DTD
+-- RESOURCES


Verovatno u LIB i u LIB/THRID_PARTY direktorijumu imate mnogo jar fajlova.
Ko
jos distribuira .class fajlove?

E ako ste shvatili o cemu pricam onda znate kakav je haos java komanda kojom
startujete vasu aplikaciju.

U stvari ukoliko ste srecni i ne koristite third party biblioteke, ili su te
biblioteke u jar fajlovima koji imaju Class-Path: attribut u manifestu
ispravno
setovan i odgovara vasim potrebama (raspored direktorijuma i sl) onda ste
stvarno rodjeni pod srecnom zvezdom mozete koristiti nesto sto se zove
izvrsna
jar arhiva. Dovoljno je da napravite jar za vasu aplikaciju i da napravite
manifest koji ce imati dve "komande"/"atributa" postavljene. Prvi atribut je
"Class-Path", a drugi je "Main-Class:". U prvom upisujete sve foldere i jar
arhive od kojih zavisi vas program

(primer: "Class-Path: classes/ servlet.jar"),

a u drugom atributu dajete putanju do startup klase u vasoj aplikaciji,
primer:

com.mydomen.myapp.Main

Ukoliko imate srece vi cete upisati samo biblioteke od kojih ste vi zavisni,
a
biblioteke od kojih zavise vase biblioteke ce biti dodate u CLASSPATH
ukoliko
biblioteka koju koristite koristi manifest i "Class-Path:" attribut. To bi
bila
idealna situacija, a u protivnom mozete jednostavno ukucati sve sto treba da
se
nalazi u CP putanji u CP attribut u vasem manifestu. To je mukotrpan posao,
i
zahteva da menjate manifest svaki put kada se neki od zavisnih modula
promeni.
Nije to tesko samo se treba setiti.

Jedna alternativa vam je da pisete shell skripte. Ovo je dobro resenje,
ukoliko
svi vasi korisnici koriste jedan sistem, u protivnom opet pisanje skripte za
svaki OS posebno.

Medjutim postoji resenje. Napraviti jedan mali loader koji ce po startovanju
ucitati sve potrebne biblioteke koje nadje u odredjenom folderu, kreirati
odgovarajuci ClassLoader i zatim startovati vasu aplikaciju. Kod koji vam
daje
radi upravo to. Vrlo je jednostavan, i deo je veceg projekta koji se igra
ClassLoader-ima.

Posto ne znam kakav je standard ovde za upload source koda, ja cu vam dati
kod
ovde. Neznam da li je moguce poslati attachment posto ja koristim ES NEWS
server.

Code:

package org.intertele.utils.startup;

import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class BootstrapLoader extends URLClassLoader {

public BootstrapLoader() {
super(new URL[0]);
}

public void addPath(File path) {
try {
addURL(path.toURL());
} catch (MalformedURLException ex) {
}
}

public void addJars(File dir) {
addJars(dir, false);
}

public void addJars(String path) {
addJars(new File(path));
}

public void addJars(String path, boolean recursive) {
addJars(new File(path), recursive);
}

public void addJars(File dir, boolean recursive) {
if (!dir.isDirectory()) {
throw new IllegalArgumentException("argument not directory : "
+ dir.toString());
}
File[] list = dir.listFiles(new FileFilter() {
public boolean accept(File file) {
if (file.isFile()
&& file.getName().toLowerCase().endsWith(".jar")) {
return true;
}
if (file.isDirectory()) {
return true;
}
return false;
}
});

for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
if (recursive) {
addJars(list[i], true);
}
} else {
try {
//System.out.println("Adding to classpath: " + list[i]);
addURL(list[i].toURL());
} catch (MalformedURLException ex) {
}
}
}
}

public static void main(String[] args) throws Exception {

if (args.length == 0) {
System.out.println("You must specify main class and optionaly
its
arguments");
System.exit(-1);
}
BootstrapLoader loader = new BootstrapLoader();
// ovde mozete da vrsite razne izmene. Unesite drugo ime
direktorijuma
// ili pak napravite da ime direkturijuma bude system property ili
// citano iz nekog konfiguracijskog fajla.
File lib = new File("lib");

// ucitaj biblioteke iz lib direktorijuma i iz svih subdirektorija
loader.addJars(lib, true);

// ucitaj biblioteke samo iz lib dir. Bez subdir
// loader.addJars(lib, false);

Thread.currentThread().setContextClassLoader(loader);
Class clazz = loader.loadClass(args[0], true);
Method main = clazz.getMethod("main", new Class[]
{args.getClass()});
String[] mArgs = new String[args.length - 1];
System.arraycopy(args, 1, mArgs, 0, mArgs.length);
main.invoke(null, new Object[] {nArgs});
}


Sada ovu klasu iskompajlirate i spakujte u jedan mail jar fajl nazovimo ga
loader.jar Potrebno je takodje da napravite i mainifest za taj jar. Evo ga:

Code:

Manifest-Version: 1.0
Main-Class: org.intertele.utils.startup.BootstrapLoader



Ono sto smo dobili ovim je da sada kada zazelimo da startujemo onu nasu
aplikaciju sa pocetka price mozemo da koristimo vrlo jednostavnu komandu:

Code:

java -jar loader.jar com.mydomain.myapp.Main mainarg1 mainarg2 mainargn



Ono sto ce se desiti je sledece:

Prvo ce java komada postaviti u class path loader.jar fajl. Onda ce iz
manifesta
procitati entery point, pa ce zatim poceti da se izvrsava
BootClassLoader.main(String[]) metod. Ovaj metod ce napraviti klass loader,
dodace u CP loadera sve jar arhive koje se nalaze u <user.dir>/lib
direktorijumu i svim poddirektorijumima. Zatim ce probati da uz pomoc novog
ClassLoadera ucita klasu cije ime ocekuje u prvom argumentu. Pozvace main
metodu
te klase i proslediti joj preostale argumente.

Sada vam je jasno. Sve svoje slozene skripte za pravljenje classpath-a koji
je
potreban za rad vase aplikacije sada mozete zameniti loader.jar arhivom.

Moram da napomenem da je ovo samo snipet koda, i da ste pozvani da ga dalje
razvijete i prilagodite svojim potrebama. U komentarima u kodu se nalaze
neki
predlozi za poboljsanje, a ovde moram da napomenem da bi bilo dobro dodati
jos i
bolji exception handling mehanizam. U realnom projektu ovo je reseno jednim
prostim frameworkom za obradu gresaka, ali o tome neki drugi put.


P.S
Ubuduce ocekujte po jedan tekst vikendom, jer je sad ludnica ovde kod nas
radim danima a i pocele su pripreme za karneval.