Programarea în reţea – Socket-uri
3. Aplicaţie client-server cu server monofir
4. Aplicatie client-server cu server multifir
6. Trimiterea obiectelor prin socket-uri
Scopul acestei lucrări este insuşirea
tehnicilor de programare în reţea în limbajul Java utilizând socket-uri.
Calculatoarele conectate in reţea comunică între ele utilizând protocoalele TCP
(Transport Control Protocol) şi UDP (User Datagram Protocol) conform
diagramei:
Figura 1. Nivelele de omunicare în reţea
Pentru realizarea unor programe care comunică in reţea în
java, se utilizează clasele din pachetul java.net . Acest pachet
oferă clasele necesare pentru realizarea unor programe de reţea
independente de sistemul de operare.
In tabelul următor sunt prezentate principalele clase care sunt
utilizate pentru construirea unor programe de reţea.
Class |
Scop |
URL |
Reprezintă
un URL |
URLConnection |
Returnează
continutul adresat de obiectele URL |
Socket |
Crează un
socket TCP |
ServerSocket |
Crează un
socket server TCP |
DatagramSocket |
Crează un
socket UDP |
DatagramPacket |
Reprezintă o
datagrama trimisă printr-un obiect DatagramSocket |
InetAddress |
Reprezintă
numele unui pc din reţea, respectiv IP-ul corespunzător |
Java oferă două abordări diferite pentru realizarea de
programe de reţea. Cele două abordări sunt asociate cu clasele:
-
Socket, DatagramSocket şi ServerSocket
-
URL, URLEncoder şi URLConnection
Programarea prin socket-uri reprezintă o abordare
de nivel jos, prin care, două calculatoare pot fi
conectate pentru a realiza schimb de date. Ca principiu de
baza, programarea prin socketuri face posibilă comunicarea în mod
full-duplex între client şi server. Comunicarea se face prin
fluxuri de octeţi.
Pentru
ca comunicarea să se desfăşoare
corespunzător, programatorul va trebui să implementeze un protocol de
comunicaţie (reguli de dialog), pe care clientul şi serverul îl vor
urma.
Identificare unui calculator
în reţea
Orice calculator conectat la Internet este identificat in mod unic de
adresa sa IP (IP este acronimul de la Internet Protocol). Aceasta
reprezinta un numar reprezentat pe 32 de biti, uzual sub forma a 4 octeti, cum
ar fi de exemplu: 193.226.5.33 si este numit adresa IP numerică.
Corespunzătoare unei adrese numerice exista si o adresa IP simbolica,
cum ar fi utcluj.ro.
De asemenea fiecare calculator aflat într-o reţea locala
are un nume unic ce poate fi folosit la identificarea locala a acestuia.
Clasa Java care reprezinta
notiunea de adresa IP este InetAddress. Pentru a construi un obiect se foloseşte
comanda:
InetAddress address
=InetAddress.getByName("121.3.1.2");
Pentru
a vedea toate modurile în care
pot fi construite obiecte de tip InetAddress studiaţi documentaţia
acestei clase.
Un
calculator are în general o singura legătura fizica la reţea. Orice
informaţie destinata unei anumite maşini trebuie deci sa specifice
obligatoriu adresa IP a acelei maşini. Insa pe un calculator pot exista
concurent mai multe procese care au stabilite conexiuni în reţea, asteptând diverse informaţii. Prin
urmare datele trimise către o destinaţie trebuie sa specifice pe lângă
adresa IP a calculatorului si procesul catre care se îndreaptă informaţiile
respective. Identificarea proceselor se realizează prin intermediul porturilor.
Orice
aplicaţie care comunică în reţea este identificată în mod
unic printr-un port, astfel încât pachetele sosite pe calculatorul gazdă
să poată fi corect rutate către aplicaţia destinaţie.
Valorile
pe care le poate lua un număr de port sunt cuprinse între 0 si 65535 (deoarece sunt numere
reprezentate pe 16 biţi), numerele cuprinse între 0 si 1023 fiind însă
rezervate unor servicii sistem, si, din acest motiv, nu se recomandă
folosirea acestora.
Definitia socket-ului: Un socket reprezintă un punct de
conexiune într-o reţea TCP\IP. Când două programe aflate pe două
calculatoare în reţea doresc să comunice, fiecare dintre ele
utilizează un socket. Unul dintre programe (serverul) va deschide un
socket si va aştepta conexiuni, iar celălalt program (clientul), se
va conecta la server şi astfel schimbul de informaţii poate începe.
Pentru a stabili o conexiune, clientul va trebui să cunoască adresa
destinaţiei ( a pc-ului pe care este deschis socket-ul) şi portul pe
care socketul este deschis.
Principalele operaţii care sunt facute de socket-uri sunt:
- conectare la un alt socket
- trimitere date
- recepţionare date
- inchidere conexiune
- acceptare conexiuni
Pentru realizarea unui program client-server se utilizează clasele
ServerSocket şi Socket.
Programul server va trebui să deschidă un port şi să
aştepte conexiuni. In acest scop este utilizată clasă
ServerSocket. In momentul în care se crează un obiect ServerSocket se
specifică portul pe care se va iniţia aşteptarea. Inceperea
ascultării portuli se face apelând metoda accept(). In momentul în care un
client s-a conectat, metoda accept() va returna un obiect Socket.
La rândul său clientul pentru a se conecta la un server, va trebui
să creeze un obiect de tip Socket, care va primi ca parametri adresa
serverului şi portul pe care acesta aşteaptă conexiuni.
Atât la nivelul serverului cât şi la nivelul clientului, odată
create obiectele de tip Socket, se vor obţine fluxurile de citire şi
de scriere. In acest scop se utilizeaza metodele getInputStream() şi
getOuptuStream().
In listingul următor este prezentat programul server.
import java.net.*;
import java.io.*;
public class ServerSimplu {
public static
void main(String[] args) throws IOException{
ServerSocket ss=null;
Socket
socket=null;
try{
String
line="";
ss = new
ServerSocket(1900); //creaza obiectul serversocket
socket =
ss.accept(); //incepe asteptarea peportul 1900
//in
momentul in care un client s-a conectat
ss.accept() returneaza
//un
socket care identifica conexiunea
//creaza
fluxurile de intrare iesire
BufferedReader in = new BufferedReader(
new
InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(
new
BufferedWriter(new OutputStreamWriter(
socket.getOutputStream())),true);
while(!line.equals("END")){
line =
in.readLine(); //citeste datele de la client
out.println("ECHO "+line); //trimite date la client
}
}catch(Exception
e){e.printStackTrace();}
finally{
ss.close();
if(socket!=null) socket.close();
}
}
}
Programul client este prezentat în listingul următor:
import java.net.*;
import java.io.*;
public class ClientSimplu {
public static
void main(String[] args)throws Exception{
Socket
socket=null;
try {
//creare
obiect address care identifica adresa serverului
InetAddress address =InetAddress.getByName("localhost");
//se putea
utiliza varianta alternativa: InetAddress.getByName("127.0.0.1")
socket = new
Socket(address,1900);
BufferedReader in =
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
// Output
is automatically flushed
// by
PrintWriter:
PrintWriter out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
for(int i
= 0; i < 10; i ++) {
out.println("mesaj " + i);
String
str = in.readLine(); //trimite mesaj
System.out.println(str); //asteapta raspuns
}
out.println("END"); //trimite mesaj care determina serverul sa
inchida conexiunea
}
catch
(Exception ex) {ex.printStackTrace();}
finally{
socket.close();
}
}
}
Pentru verificare se va starta serverul, după care se va starta
clientul.
Analizând programul server prezentat în secţiunea anterioarăm
se observă că acesta poate servi doar un singur client la un moment
dat. Pentru ca serverul să poată servi mai mulţi clienţi
simultan, se va utiliza programarea multifir.
Ideea de bază este simplă, şi anume, serverul va
aştepta conexiuni prin apelarea metodei accept(). In momentul in care un
client s-a conectat şi metoda accept() a returnat un Socket, se va crea un
fir de execuţie care va servi respectivul clientul, iar severul va reveni
în aşteptare.
In programul următor este prezentat un server multifir – capabil de
a servi mai multi clienţi simultan.
import java.io.*;
import java.net.*;
public class ServerMultifir
{
public static final int PORT = 1900;
void startServer()
{
ServerSocket ss=null;
try
{
ss = new ServerSocket(PORT);
while (true)
{
Socket socket = ss.accept();
new TratareClient(socket).start();
}
}catch(IOException ex)
{
System.err.println("Eroare :"+ex.getMessage());
}
finally
{
try{ss.close();}catch(IOException ex2){}
}
}
public static void main(String args[])
{
ServerMultifir smf = new ServerMultifir();
smf.startServer();
}
}
class TratareClient extends Thread
{
private Socket socket;
private BufferedReader in;
private PrintWriter out;
TratareClient(Socket socket)throws IOException
{
this.socket = socket;
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter( socket.getOutputStream())));
}
public void run()
{
try {
while (true)
{
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}//.while
System.out.println("closing...");
}
catch(IOException e) {System.err.println("IO Exception");}
finally {
try {
socket.close();
}catch(IOException e) {System.err.println("Socket not closed");}
}
}//.run
}
In această sectiune este creat un server HTTP care poate
răspunde la cereri GET. Functia main() din cadrul clasei HttpServer
startaează un fir de execuţie. In cadrul acestui fir se
instanţiază un obiect ServerSocket şi se incepe ascultarea
portului 80, care este portul standard pentru protocolul HTTP.
In momentul în care apare o cerere (un client se conectează pe
portul 80) metoda accept() va returna un obiect Socket. In continuare se
crează un obiect PrecesRequest (care este de tip fir de excutie), care va
primi ca parametru, obiectul Socket returnat de metoda accept(). După
crearea obiectului ProcesRequest, serverul revine în aşteptare şi va
putea servi alţi clienţi.
Clasa ProcesRequest implementează o versiune simplificată a
protocolului HTTP. In cadrul constructorului clasei ProcesRequest se
crează fluxurile de intrare \ ieşire, după care este startat
firul de execuţie. In cadrul firului de execuţie este analizată
cererea primită de la client , şi în cazul în care aceasta este o
cerere validă de tip GET, atunci se va transmite către client resursa
solicitată.
import java.io.*;
import java.net.*;
class HttpServer extends Thread
{
//portul
standard
private
final static int PORT = 80;
private
final String iniContext="c:/temp/ServerHTTP/webdocs";
private
boolean alive;
private
ServerSocket ss;
//constructor
HttpServer()throws
Exception{
System.out.println("Start
server http.");
ss
= new ServerSocket(PORT);
alive=true;
start();
}
public
void run(){
while(alive){
//asteapta
conexiuni
try{
System.out.println("Server
asteapta...");
new
ProcesRequest(ss.accept(),iniContext);
}catch(IOException
e){System.err.println("EROARE CONECTARE:"+e.getMessage());}
//..reia
bucla de asteptare dupa ce am creat un fir pentru client
}
System.out.println("STOP
SERVER");
}
public
static void main(String[] args)throws Exception
{
try{
new
HttpServer();
}catch(Exception
e){e.printStackTrace();}
}
}
import java.net.*;
import java.io.*;
import java.util.*;
class ProcesRequest extends Thread
{
private
PrintWriter outStr;
private
BufferedReader inStr;
private
Socket s;
private
DataOutputStream dout;
private
String iniContext;
ProcesRequest(Socket
s, String iContext){
try{
outStr
= new PrintWriter(new OutputStreamWriter(s.getOutputStream()),true);
inStr
= new BufferedReader(new InputStreamReader(s.getInputStream()));
dout
= new DataOutputStream(s.getOutputStream());
iniContext
= iContext;
this.s
= s;
start();
}catch(IOException
e)
{System.err.println("EROARE
CONECTARE: "+e.getMessage());}
}
public
void run(){
try{
String
fileName=null;
String
request = inStr.readLine();
System.out.print(request);
if(request.lastIndexOf("GET")==0)
fileName = interpretGET(request);
else
throw new Exception("BAU");
byte[]
data = readFile(fileName);
dout.write(data);
dout.flush();
}
catch(IOException
e){outStr.println("<HTML><BODY><P>403
Forbidden<P></BODY></HTML>");}
catch(Exception
e2){outStr.println("<HTML><BODY><P>"+e2.getMessage()+"<P></BODY></HTML>");}
finally{
try{s.close();}catch(Exception
e){}
}
}
private
String interpretGET(String rqst) throws Exception{
StringTokenizer
strT = new StringTokenizer(rqst);
String
tmp="";
String
fileN=iniContext;
tmp=strT.nextToken();
if(!tmp.equals("GET"))
throw new Exception("Comanda GET invalida .");
tmp=strT.nextToken();
if((tmp.equals("/"))
|| (tmp.endsWith("/"))) {
fileN
= fileN+tmp+"index.htm";
System.err.println("CERERE:"+fileN);
return
fileN;
}
fileN
= fileN+ tmp;
System.err.println("CERERE:"+fileN);
return
fileN;
}
private
byte[] readFile(String fileN) throws Exception{
fileN.replace('/','\\');
File
f = new File(fileN);
if(!f.canRead())
throw new Exception("Fisierul "+fileN+" nu poate fi
citit");
FileInputStream
fstr = new FileInputStream(f);
byte[]
data = new byte[fstr.available()];
fstr.read(data);
return
data;
}
}
Pentru verificarea
programului anterior se va modifica variabila iniContextm din cadrul clasei
HTTPServer, astfel încât aceasta să indice calea corectă catre contextul
iniţial (directorul unde se află toate resursele pe care clientul le
poate accesa).
Mecanismul de serializare pune la dispoziţia programatorului o
metodă prin care un obiect poate fi salvat pe disc şi restaurat
atunci cand este nevoie. Tot prin acelaşi mecanism un obiect poate fi
transmis la distanta catre o altă maşină utilizând socketurile.
Pentru a putea serializa un obiect acesta va trebui să implementeze
interfaţa Serializable.
Pentru scrierea şi citirea obiectelor serializate se
utilizează fluxurile de intrare / ieşire : ObjectInputStream si ObjectOutputStream.
Listingul următor prezintă modul in care se poate serializa /
deserializa un obiect.
import java.io.*;
import java.net.*;
public class SerialTest extends Thread{
public
void run(){
try{
ServerSocket ss = new ServerSocket(1977);
Socket
s = ss.accept();
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Pers p
= (Pers)ois.readObject();
System.out.println(p);
s.close();
ss.close();
}catch(Exception e){e.printStackTrace();}
}
public static
void main(String[] args) throws Exception{
//trimite obiect prin socket
(new
SerialTest()).start();
Socket
s = new Socket(InetAddress.getByName("localhost"),1977);
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
Pers p
= new Pers("Alin",14);
oos.writeObject(p);
s.close();
}
}
class Pers implements Serializable{
String nume;
int varsta;
Pers(String
n, int v){
nume = n;
varsta = v;
}
public String
toString(){
return
"Persoana: "+nume+" vasrta: "+varsta;
}
}
Există situaţii în care în momentul in care se selveaza starea
unui obiect prin serializare, nu se doreşte salvare tuturor starilor
obiectului, respectiv nu se doreşte salvarea sau transmiterea anumitor
parametri ai obiectului. Pentru a bloca serializarea unui atribut al unui
obiect serializabil se utilizează cuvantul cheie transient.
Versiunea Java 2 Standad Edition 1.4 introduce un nou mecanism de
comunicare în reţea prin interemediul socket-urilor neblocante – acestea
permit comunicarea între aplicaţii fără a bloca procesele în
apeluri de metode destinate deschiderii unei conexiuni, citirii sau scrierii de
date.
Soluţia clasică pentru a construi o aplicaţie server care
deserveşte mai mulţi clienţi este de a utiliza tehnologia
firelor de execuţie şi de a aloca câte un fir de execuţie pentru
fiecare client deservit. Folosind tehnologia socket-urilor neblocante
programatorul va putea implementa o aplicaţie server pentru deservirea clienţilor
fără a fi nevoit să apeleze în mod explicit la fire de
execuţie pentru tratarea cererilor clienţilor. În continuare v-or fi
prezentate principiile de bază ale acestei tehnologii şi modul în
care pot fi construite aplicaţii client şi aplicaţii server pe
tehnologia socket-urilor neblocante.
Arhitectura unui sistem ce utilizează socketuri neblocante pentru
comunicare este ilustraţă în figura următoare.
Arhitectură cu socketu-uri neblocante.
Principalele operaţii ce au loc în cadrul unei aplicaţii
bazată pe această tehnologie sunt:
-
Clientul:
trimite cereri către server
-
Serverul:
recepţionează cereri
-
SocketChannel:
permite transmiterea de date între client şi server
-
Selector:
reprezintă un obiect de tip multiplexor şi este punctul central al
acestei tehnologii. Acesta monitorizează socket-urile înregistrate şi
serializează cererile sosite de la acestea, transmiţându-le
către aplicaţia server.
-
Cheile
reprezintă obiectele ce încapsulează cererile sosite de la
clienţi.
Un algoritm general pentru a construi un server neblocant arată
astfel:
create
SocketChannel;
create
Selector
associate
the SocketChannel to the Selector
for(
;;) {
waiting events from the Selector;
event arrived; create keys;
for each key created by Selector {
check the type of request;
isAcceptable:
get the client SocketChannel;
associate that SocketChannel to the Selector;
record it for read/write operations
continue;
isReadable:
get the client SocketChannel;
read from the socket;
continue;
isWriteable:
get the client SocketChannel;
write on the socket;
continue;
}
}
Implementarea serverului constă într-o buclă infinită în
cadrul căreia selectorul aşteaptă producerea de evenimente. În
momentul în care un eveniment s-a produs si o cheie a fost generată se
verifică tipul acestei chei. Tipurile de chei posibile sunt:
-
Acceptable
– este asociată evenimentului de cerere de conexiune de la un client
-
Connectable
– este asociată evenimentului de acceptare de conexiune de către client
-
Readable
– citire de date
-
Writable
– scriere de date
Clasa Selector este responsabilă pentru menţinerea unui set de
chei care pot fi active în timpul rulării programului server. În momentul
în care un evenimet este generat de către un client, o cheie este
construită.
Selector selector = Selector.open();
Pentru a demultiplexa datele şi a avea acces la evenimente trebuie
construit un canal de comunicaţie care va trebui înregistrat în cadrul
obiectului de tip selector. Fiecare canal de comunicaţie înregistrat va
trebui să specifice tipul de evenimente de care este inetersat.
ServerSocketChannel
channel = ServerSocketChannel.open();
channel.configureBlocking(false);
InetAddress
lh = InetAddress.getLocalHost();
InetSocketAddress
isa = new InetSocketAddress(lh, port );
channel.socket().bind(isa);
SelectionKey
acceptKey = channel.register( selector, SelectionKey.OP_ACCEPT );
Un canal care citeşte şi scrie date va fi înregistrat în felul
următor:
SelectionKey
readWriteKey = channel.register( selector,
SelectionKey. OP_READ| SelectionKey. OP_WRITE );
Codul aplicaţiei server ce implementează comunicarea prin
socket-uri nonblocante este listat mai jos:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.channels.spi.*;
import java.nio.charset.*;
import java.net.*;
import java.util.*;
public class NonBlockingServer2 {
public
static void main(String[] args) throws Exception{
//
Create the server socket channel
ServerSocketChannel
server = ServerSocketChannel.open();
//
nonblocking I/O
server.configureBlocking(false);
//
host-port 8000
server.socket().bind(new
java.net.InetSocketAddress("localhost",8000));
System.out.println("Server
waiting on port 8000");
//
Create the selector
Selector
selector = Selector.open();
//
Recording server to selector (type OP_ACCEPT)
server.register(selector,SelectionKey.OP_ACCEPT);
//
Infinite server loop
for(;;)
{
Thread.sleep(1000);
// Waiting for events
System.err.println("wait for
event...");
selector.select();
// Get keys
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
System.err.println("keys
size="+keys.size());
// For each keys...
while(i.hasNext()) {
//
Obtain the interest of the key
SelectionKey key = (SelectionKey) i.next();
// Remove the current key
i.remove();
// if isAccetable = true
// then a client required a connection
if (key.isAcceptable()) {
System.err.println("Key is of type
acceptable");
// get client socket channel
SocketChannel client = server.accept();
// Non Blocking I/O
client.configureBlocking(false);
// recording to the selector (reading)
client.register(selector,
SelectionKey.OP_READ);
continue;
}
// if isReadable = true
// then the server is ready to read
if (key.isReadable()) {
System.err.println("Key is of type
readable");
SocketChannel client = (SocketChannel)
key.channel();
// Read byte coming from the client
int BUFFER_SIZE = 32;
ByteBuffer buffer =
ByteBuffer.allocate(BUFFER_SIZE);
try {
client.read(buffer);
}
catch (Exception e) {
// client is no longer active
client.close();
e.printStackTrace();
continue;
}
// Show bytes on the console
buffer.flip();
Charset
charset=Charset.forName("ISO-8859-1");
CharsetDecoder decoder =
charset.newDecoder();
CharBuffer charBuffer =
decoder.decode(buffer);
System.out.println(charBuffer.toString());
continue;
}
}
System.err.println("after while keys
size="+keys.size());
}
}
}
Condul aplicaţiei client ce foloseşte socketu-uri neblocante
pentru comunicarea cu un server este listat mai jos:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.channels.spi.*;
import java.nio.charset.*;
import java.net.*;
import java.util.*;
public class NonBlockingClient {
public
static void main(String[] args) throws IOException {
//
Create client SocketChannel
SocketChannel
client = SocketChannel.open();
//
nonblocking I/O
client.configureBlocking(false);
//
Connection to host port 8000
client.connect(new
java.net.InetSocketAddress("localhost",8000));
//
Create selector
Selector
selector = Selector.open();
//
Record to selector (OP_CONNECT type)
SelectionKey
clientKey = client.register(selector, SelectionKey.OP_CONNECT);
//
Waiting for the connection
while
(selector.select(500)> 0) {
System.err.println("Start
communication...");
// Get keys
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
// For each key...
while (i.hasNext()) {
SelectionKey key = (SelectionKey)i.next();
// Remove the current key
i.remove();
// Get the socket channel held by the key
SocketChannel channel =
(SocketChannel)key.channel();
// Attempt a connection
if (key.isConnectable()) {
// Connection OK
System.out.println("Server
Found");
// Close pendent connections
if (channel.isConnectionPending())
channel.finishConnect();
// Write continuously on the buffer
ByteBuffer buffer = null;
int x=0;
for (;x<7;) {
x++;
buffer =
ByteBuffer.wrap(
new String(" Client " + x
+ " "+x).getBytes());
channel.write(buffer);
buffer.clear();
try {Thread.sleep(2000);} catch
(InterruptedException e) {e.printStackTrace();}
}
channel.finishConnect();
client.close();
}
}
}
System.err.println("Client
terminated.");
}
}
Importaţi în
mediul Eclipse proiectul ce exemplifică noţiunile prezentate în acest
laborator (link proiect).
Pachetul lab.scd.socket exemplifica construirea unor aplicatii server si client ce comunica utilizand protocolul TCP.
1)
2)
Pachetul lab.scd.datagrame exemplifica constuirea unor aplicatii server si client ce comunica utilizand protocolul UDP.
Pachetul lab.scd.broadcast exemplifica constuirea unor aplicatii server si client ce comunica utilizand protocolul UDP.
Pachetul lab.scd.serializare exemplifică mecanismul de serializare şi
deserializare a obiectelor.
Pachetul lab.scd.net.httpexample prezinta modul in care poate fi construit in
java un server HTTP. Aplicatia exemplu implementeaza partial protocolul HTTP,
raspunzand la cereri de tip GET. Analizati modul de construire al aplicatiei,
executati si testati aplicatia.
Pachetul lab.scd.net.url_http pezinta clasele din java.net ce pot fi
utilizate pentru lucrul cu URL-uri.
Pachetul lab.scd.net.browser prezintă modul în care poate fi construit în java un browser de pagini html.