Laborator 06. Comunicația prin Sockeți TCP

Realizarea Conexiunii între o Mașină Fizică și Dispozitivul Mobil

Pentru a putea comunica prin intermediul unui socket TCP, o mașină fizică și dispozitivul mobil trebuie să se găsească în aceeași rețea, astfel încât adresele IP ale acestora să fie vizibile între ele.

Dispozitiv Fizic

O mașină fizică și un dispozitiv fizic pot comunica:

  1. prin plasarea lor în aceeași rețea fără fir (WiFi), având adresele IP furnizate de un server DHCP ce rulează pe un router;
  2. prin stabilirea unei legături punct la punct, folosind Bluetooth;
  3. printr-o conexiune de date realizată prin intermediul portului USB.

Pe telefon, se accesează SettingsWireless & NetworksTethering & portable hotspot și se selectează opțiunea USB Tethering

Astfel, se va activa (în mod automat) interfața rndis0, pentru care se poate determina adresa Internet:

student@eim2016:/opt/android-sdk-linux/platform-tools$ ./adb devices
List of devices attached
0019531d59461f    device
student@eim2016:/opt/android-sdk-linux/platform-tools$ ./adb -s 0019531d59461f shell
shell@n7000:/ $ su
su
shell@n7000:/ # ifconfig rndis0
ifconfig rndis0
rndis0: ip 192.168.42.129 mask 255.255.255.0 flags [up broadcast running multicast]
shell@n7000:/ #
Pe Linux, în situația în care dispozitivul mobil este detectat, fără a se indica tipul său, este necesar să se pornească serverul de ADB cu drepturi de administrator:
student@eim2016:/opt/android-sdk-linux/platform-tools$ ./adb devices
List of devices attached
0019531d59461f    ????????????
student@eim2016:/opt/android-sdk-linux/platform-tools$ sudo ./adb kill-server
student@eim2016:/opt/android-sdk-linux/platform-tools$ sudo ./adb start-server
student@eim2016:/opt/android-sdk-linux/platform-tools$ sudo ./adb devices
List of devices attached
0019531d59461f    device

Pentru dispozitivele care nu permit conectarea cu adb shell in modul tethering soluție stackoverflow, trebuie creat în Ubuntu fișierul /etc/udev/rules.d/51-android.rules și adăugat codul obținut cu lsusb pentru fabricantul telefonului. Odată dispozitivul de rețea obținut (de exemplu rndis0), se folosește dhclient rndis0 pentru a obține o adresă IP de la telefon.

Pe Windows, dacă dispozitivul mobil nu este detectat, poate fi necesară instalarea unor drivere suplimentare pentru stabilirea legăturii prin intermediul portului USB.

Ulterior, se determină adresa Internet a mașinii fizice, asociată interfeței usb0 (Linux), respectiv Ethernet (Windows).

Linux

student@eim2016:~$ sudo ifconfig usb0
usb0      Link encap:Ethernet  HWaddr 32:ca:4b:1c:ff:7b 
          inet addr:192.168.42.170  Bcast:192.168.42.255  Mask:255.255.255.0
          inet6 addr: fe80::30ca:4bff:fe1c:ff7b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:109 errors:0 dropped:0 overruns:0 frame:0
          TX packets:319 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:24103 (23.5 KiB)  TX bytes:64369 (62.8 KiB)

Windows

C:\Program Files (x86)\Android\android-sdk\platform-tools>ipconfig

Windows IP Configuration


Ethernet adapter Local Area Connection:

   Connection-specific DNS Suffix  . :
   Link-local IPv6 Address . . . . . : fe80::18bf:d0be:3625:6b1%44
   IPv4 Address. . . . . . . . . . . : 192.168.42.81
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 192.168.42.129

Dispozitiv Virtual (Emulator)

Genymotion

Fiecărui dispozitiv virtual Genymotion îi este alocată în mod automat o adresă IP de către serverul DHCP configurat pe mașina virtuală VirtualBox în cadrul căruia rulează.

Implicit, este utilizat spațiul de adrese 192.168.56.1/24:

  • mașina fizică (default gateway) are adresa 192.168.56.1;
  • pentru dispozitivele virtuale sunt alocate adrese în intervalul 192.168.56.101 .. 192.168.56.254.

Configurarea spațiului de adrese care este folosit poate fi configurat prin intermediul VirtualBox, accesând FilePreferencesNetworkHost Only Networks

Adresa IP care a fost atașată fiecărui dispozitiv virtual Genymotion poate fi verificată:

  • folosind comanda adb devices;
    student@eim2016:/opt/android-sdk-linux/platform-tools$ ./adb devices
    List of devices attached
    192.168.56.101:5555     device
  • prin intermediul plugin-ului Genymotion pentru mediile integrate de dezvoltare.

Conectivitatea dintre mașina fizică și dispozitivul virtual poate fi verificată folosind comanda ping.

student@eim2016:~$ ping 192.168.56.101
Pinging 192.168.56.101 with 32 bytes of data:
Reply from 192.168.56.101: bytes=32 time<1ms TTL=64
Reply from 192.168.56.101: bytes=32 time<1ms TTL=64
Reply from 192.168.56.101: bytes=32 time<1ms TTL=64
Reply from 192.168.56.101: bytes=32 time<1ms TTL=64

Ping statistics for 192.168.56.101:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms

Android Virtual Device

Fiecare instanță a unui dispozitiv virtual Android oferă o pereche de porturi pentru diferite conexiuni:

  • un port de consolă, prin intermediul căruia este permis accesul prin telnet pentru execuția de diverse comenzi;
  • un port pentru adb.

Numerele folosite pentru aceste porturi sunt succesive. Implicit, numerotarea porturilor începe de la 5554 (portul de consolă) / 5555 (portul adb). Determinarea portului poate fi realizată:

  • prin inspectarea ferestrei în care este afișat emulatorul, având forma Android Emulator (55nr), unde nr poate lua valori cuprinse între 54 și 87 (doar valori impare - pentru portul de consolă, valorile pare fiind rezervate pentru portul adb); astfel, sunt suportate maxim 16 instanțe de dispozitive virtuale Android simultan;

  • prin rularea comenzii adb devices
    student@eim2016:/opt/android-sdk-linux/platform-tools$ ./adb devices
    List of devices attached
    emulator-5554   device

Conectarea la consola dispozitivului virtual Android se face prin comanda:

student@eim2016:~$ telnet localhost 55nr

specificându-se portul pe care rulează emulatorul.

În consolă, realizarea unei legături între mașina fizică și dispozitivul virtual Android se face prin redirectarea portului, folosind comanda redir, aceasta suportând mai multe opțiuni:

  • list;
  • add;
  • del.
OPȚIUNE DESCRIERE
list afișează toate redirectările de port folosite la momentul respectiv
add <protocol>:<port_masina_fizica>:<port_dispozitiv_virtual> adaugă o redirectare de port
<protocol> poate avea doar valorile tcp sau udp
<port_masina_fizica> reprezintă numărul portului utilizat pe mașina fizică
<port_dizpozitiv_virtual> reprezintă numărul portului de pe dispozitivul virtual spre care vor fi redirecționate datele
del <protocol>:<port_masina_fizica> șterge o redirectare de port

Exemplu

student@eim2016:~$ telnet localhost 5554
Android Console: type 'help' for a list of commands
OK
redir add tcp:2000:4000
OK
redir list
tcp:2000  => 4000
OK
redir del tcp:2000
OK
redir list
no active redirections
OK
exit
Connection to host lost.
student@eim2016:~$

Mecanisme pentru Comunicația prin Rețea în Android

Comunicația între mai multe dispozitive poate fi realizată:

  1. prin metode generale, disponibile la nivelul platformei Java:
    1. sockeți TCP, a căror funcționalitate este implementată în pachetul java.net, oferind acces la cele mai multe operații ce implică programarea de rețea (atât server, cât și client);
    2. clasa HttpURLConnection, prin care este facilitat accesul la servere care folosesc protocolul http;
  2. prin mecanisme specifice Android:
    1. clasa HttpClient permite posibilitatea de a descărca un conținut disponibil la o anumită adresă Internet, identificată printr-un URL;
    2. clasa JSONObject este utilizată pentru gestiunea datelor în format JSON.
De regulă, o aplicație Android nu are acces la comunicația prin rețea decât prin intermediul unei permisiuni speciale:
<manifest ...>
  <!-- other application properties or components -->
  <uses-permission
    android:name="android.permission.INTERNET" />
  <!-- other application properties or components --> 
</manifest>

Programarea de Rețea folosind Sockeți TCP în Android

Folosirea unui socket TCP presupune comunicația între două entități:

  1. un client care se conectează la o anumită adresă, pe un anumit port, pe care le cunoaște în prealabil;
  2. un server care așteaptă să fie invocat, la o adresă și la un port.

În Android (ca și în cazul platformei Java), clasa de bază pentru comunicația dintre client și server este Socket. Aceasta pune la dispoziție un flux de intrare și un flux de ieșire prin intermediul cărora diferite entități, ale căror adrese IP sunt vizibile între ele, pot transmite diferite date, folosind protocolul TCP. Un socket este reprezentat de o asociere dintre o adresă și un port.

“Ascultarea” invocărilor este realizată prin intermediul clasei ServerSocket. În momentul în care este detectată o astfel de solicitare, este creată o nouă conexiune, reprezentată de un obiect de tip Socket prin care se va realiza comunicarea. Astfel, la nivelul serverului, fiecare client este identificat printr-o instanță proprie a unui obiect Socket.

Clientul

Conexiunea unui client la un server se poate realiza numai în situația în care sunt cunoscute adresa IP (sau denumirea, rezolvată apoi prin intermediul DNS) și portul la care acesta așteaptă să fie invocat. Aceste valori sunt transmise ca parametrii în constructorul obiectului Socket. Ulterior, operațiile de comunicație (citire / scriere) sunt realizate prin operații pe fluxuri de intrare (InputStream) și pe fluxuri de ieșire (OutputStream).

Așadar, comunicația prin intermediul unui obiect de tip Socket presupune următorii pași:

1. deschiderea unui socket, prin transmiterea parametrilor de identificare a gazdei (adresă IP / denumire și port) ca parametrii ai constructorului unui obiect de tip Socket

String hostname = "localhost";
int port = 2016;
Socket socket = new Socket(hostname, port);
În momentul în care se realizează o instanță a unui obiect de tip Socket, pot fi generate următoarele excepții:
  • UnknownHostException, în situația în care se folosește o denumire pentru gazdă și aceasta nu poate fi rezolvată de serverul DNS (alternativ, poate fi furnizată chiar adresa IP, dacă este cunoscută în prealabil);
  • IOException, atunci când:
    • conexiunea nu a fost acceptată de către server;
    • perioada de așteptare pentru realizarea operației a fost depășită:
    • s-a produs o întrerupere sau altă problemă neașteptată.

2. crearea unui flux de ieșire pentru a trimite date prin intermediul socket-ului TCP și trimiterea efectivă a datelor:

BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
PrintWriter printWriter = new PrintWriter(bufferedOutputStrea, true);
De remarcat faptul că obiectul PrintWriter primește în constructor un parametru de tip boolean prin care se indică dacă transmiterea efectivă a datelor este realizată sau nu în mod automat atunci când se întâlnește caracterul \n (newline).

Metodele implementate de clasa PrintWriter sunt similare cu cele oferite de PrintStream (clasa folosită de metodele din System.out), diferența fiind faptul că pot fi create mai multe instanțe pentru seturi de caractere Unicode diferite:

  • print();
  • println();
  • printf();
  • write().

Trimiterea efectivă a datelor este realizată atunci când se apelează metoda flush().

3. crearea unui flux de intrare pentru a primi date prin intermediul socket-ului TCP și primirea efectivă a datelor:

InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
Un obiect de tip BufferedReader poate stoca maxim 8192 de caractere în zona de memorie tampon de care dispune.

Metodele implementate de clasa BufferedReader sunt:

  • read() - pentru a primi un singur caracter (dacă este apelată fără parametrii) sau un tablou de caractere (de o anumită dimensiune, acestea fiind stocate într-un vector furnizat ca parametru, începând cu o anumită poziție);
  • readLine() - pentru a primi o linie.
Metoda readLine() este blocantă, așteptând un mesaj terminat prin \n (newline). În situația în care conexiunea este terminată, se transmite EOF, iar metoda întoarce valoarea null.
Pot fi utilizate obiecte de tipul ObjectOutputStream și ObjectInputStream pentru transmiterea de obiecte Java, atunci când entitățile care comunică rulează în contextul unei mașini virtuale de acest tip (JVM).

Se recomandă ca obținerea de referințe către fluxul de intrare respectiv fluxul de ieșire asociate unui obiect de tip Socket să fie realizată prin intermediul unor metode statice definite în cadrul unor clase ajutător:

Utilities.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
 
public class Utilities {
 
  public static BufferedReader getReader(Socket socket) throws IOException {
    return new BufferedReader(new InputStreamReader(socket.getInputStream()));
  }
 
  public static PrintWriter getWriter(Socket socket) throws IOException {
    return new PrintWriter(socket.getOutputStream(), true);
  }
 
}

4. închiderea obiectului Socket, atunci când acesta nu mai este necesar

socket.close();

În momentul în care se apelează metoda close(), sunt transmise și datele care erau stocate în zonele de memorie tampon.

Metoda close() eliberează și resursele folosite de fluxul de intrare și fluxul de ieșire asociate, care sunt de asemenea închise.

Utilizarea unui Fir de Execuție Dedicat pentru Comunicația prin Rețea

Începând cu nivelul de API 11 (Android 3.0 Honeycomb), nu este permisă comunicația prin rețea pe același fir de execuție folosit pentru interfața grafică și pentru interacțiunea cu utilizatorul (eng. UI Thread), întrucât unele operații realizate în acest context sunt blocante, astfel încât experiența folosirii aplicației Android ar fi afectată. O astfel de tentativă va fi sancționată prin generarea unei excepții de tip NetworkOnMainThreadException. Din acest motiv, orice operație ce presupune folosirea unor sockeți TCP trebuie realizată pe un fir de execuție dedicat.

Trebuie avut în vedere faptul că pe firul de execuție dedicat comunicației prin rețea NU pot fi actualizate informațiile asociate controalelor grafice, excepția generată în această situație fiind CalledFromWrongThreadException. Explicația este dată în mesajul care însoțește această excepție: numai firul de execuție în care a fost instanțiat un control grafic (obiect de tip android.view.View) are dreptul de a realiza modificări asupra acestuia (Only the original thread that created a view hierarchy can touch its views). În această situație, accesul la firul de execuție pe care este gestionată interfața grafică poate fi obținut în mai multe moduri:

  1. folosind metoda runOnUiThread(Runnable), disponibilă în clasa Activity;
  2. folosind una din metodele post(Runnable) sau postDelayed(Runnable, long) disponibile:
    1. în clasa android.view.View, pentru fiecare control grafic care se dorește a fi accesat;
    2. în clasa android.os.Handler, dacă se dorește accesarea mai multor controale grafice (fiind necesar ca instanțierea obiectului să fie realizată tot pe firul care gestionează interfața grafică);
  3. folosind un obiect de tip AsyncTask, procesarea în rețea putând fi realizată în metoda doInBackground(), iar accesul la obiectele interfeței grafice fiind acordat în metodele onProgressUpdate() (invocată în mod automat de fiecare dată când se apelează metoda publishProgress()) și onPostExecute().

Exemplu

Se dorește interogarea serverului National Institute of Standards & Technology, care oferă un serviciu de interogare a datei și orei curente, cu o precizie ridicată, conform Daytime Protocol (RFC-867).

În acest sens, se va deschide un socket TCP, prin interogarea serverului disponibil la adresa time-b.nist.gov, pe portul 13, în cadrul unui fir de execuție separat (clasa NISTCommunicationThread). Întrucât nu este necesară decât operația de primire a unor date, se va crea doar un obiect de tip BufferedReader, citindu-se două linii (una fiind vidă - așadar ignorată, cealaltă conținând informațiile necesare, care se doresc a fi afișate). Întrucât modificarea conținutului unui control grafic nu poate fi realizată decât din contextul firului de execuție în care a fost creat, acesta va fi obținut prin parametrul metodei post() a obiectului respectiv, doar aici fiind permisă asocierea conținutului solicitat. În momentul în care datele au fost preluate, socket-ul TCP poate fi închis. Pe fiecare eveniment de tip apăsare a butonului se va crea un fir de execuție dedicat în care se va instanția un obiect de tip Socket.

De remarcat faptul că obiectele interfeței grafice sunt definite ca membrii protejați ai clasei activitate, întrucât accesul acestea trebuie să poată fi accesate și din clasele interne, fără a se mai obține referința către ele prin metoda findViewById(), care depreciază performanțele aplicației Android.

DayTimeProtocolActivity.java
package ro.pub.cs.systems.eim.lab06.daytimeprotocol.views;
 
import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
 
import ro.pub.cs.systems.eim.lab06.daytimeprotocol.R;
import ro.pub.cs.systems.eim.lab06.daytimeprotocol.general.Constants;
import ro.pub.cs.systems.eim.lab06.daytimeprotocol.general.Utilities;
 
public class DayTimeProtocolActivity extends AppCompatActivity {
 
  private Button getInformationButton;
  private TextView daytimeProtocolTextView;
 
  private class NISTCommunicationAsyncTask extends AsyncTask<Void, Void, String> {
 
    @Override
    protected String doInBackground(Void... params) {
      String dayTimeProtocol = null;
      try {
        Socket socket = new Socket(Constants.NIST_SERVER_HOST, Constants.NIST_SERVER_PORT);
        BufferedReader bufferedReader = Utilities.getReader(socket);
        bufferedReader.readLine();
        dayTimeProtocol = bufferedReader.readLine();
        Log.d(Constants.TAG, "The server returned: " + dayTimeProtocol);
      } catch (UnknownHostException unknownHostException) {
        Log.d(Constants.TAG, unknownHostException.getMessage());
        if (Constants.DEBUG) {
          unknownHostException.printStackTrace();
        }
      } catch (IOException ioException) {
        Log.d(Constants.TAG, ioException.getMessage());
        if (Constants.DEBUG) {
          ioException.printStackTrace();
        }
      }
      return dayTimeProtocol;
    }
 
    @Override
    protected void onPostExecute(String result) {
      daytimeProtocolTextView.setText(result);
    }
  }
 
  private ButtonClickListener buttonClickListener = new ButtonClickListener();
  private class ButtonClickListener implements Button.OnClickListener {
 
    @Override
    public void onClick(View view) {
      NISTCommunicationAsyncTask nistCommunicationAsyncTask = new NISTCommunicationAsyncTask();
      nistCommunicationAsyncTask.execute();
    }
 
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_day_time_protocol);
 
    daytimeProtocolTextView = (TextView)findViewById(R.id.daytime_protocol_text_view);
 
    getInformationButton = (Button)findViewById(R.id.get_information_button);
    getInformationButton.setOnClickListener(buttonClickListener);
  }
}

Serverul

Un sever poate “aștepta” conexiuni de clienți prin intermediul unei instanțe a clasei ServerSocket, care poate primi ca parametrii:

  • o adresă - dacă nu este furnizată se folosește adresa mașinii / dispozitivului pe care a fost rulată, putând fi obținută prin metoda getInetAddress();
  • un port - dacă nu este specificat (sau se transmite valoarea 0), sistemul de operare va genera o astfel de valoare (care poate fi obținută ulterior prin metoda getLocalPort().
Se recomandă să se utilizeze un port cu o valoare în afara celor rezervate (0 - 1023). Totuși, folosirea acestor porturi este posibilă în cazul dispozitivele mobile pentru care există drepturi de root, după ce se acordă drepturi privilegiate aplicației Android.

Pentru ca un client să se poată conecta la server, pe obiectul ServerSocket trebuie să se apeleze metoda accept(), care se blochează ascultând invocările care ar putea fi realizate. În momentul în care este detectată o solicitare, metoda o tratează, returnând un obiect de tip Socket prin care este realizată comunicarea propriu-zisă.

De obicei, un server invocă metoda accept() într-o buclă, atâta timp cât se află în execuție. În momentul în care a fost obținut un obiect de tip Socket, se recomandă ca acesta să fie transmis unui alt fir de execuție care să gestioneze comunicația cu clientul, în caz contrar putându-se înregistra latențe în tratarea altor solicitări de conexiune. Mai mult, având în vedere faptul că acestea sunt plasate într-un obiect de tip coadă (FIFO), în momentul în care aceasta este completată, nu vor mai fi acceptate alte cereri de conexiune, acestea fiind respinse cu motivul Connection refused.

Exemplu 1. Server cu un sigur fir de execuție

Se implementează o aplicație Android cu un singur câmp text prin care se controlează:

  • diferite operații asupra serverului:
    • pornire: se transmite șirul de caractere Start Server;
    • oprire: se transmite șirul de caractere Stop Server;
  • mesajul care va fi transmis clienților care se conectează.

Astfel, serverul va accepta invocări de la clienți între momentele la care s-au introdus șirurile de caractere Start Server, respectiv Stop Server. Cât timp serverul este activ (ascultă solicitări de la clienți), se pot specifica orice valoare în câmpul text, acesta fiind transmis prin intermediul canalului de comunicație.

activity_single_threaded.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context="ro.pub.cs.systems.eim.lab06.singlethreadedserver.views.SingleThreadedActivity" >
 
  <EditText
    android:id="@+id/server_text_edit_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="text" />
 
</LinearLayout>

În momentul în care se pornește serverul, se deschide un obiect de tipul ServerSocket. Ulterior, în cadrul unui ciclu, se așteaptă invocări de la clienți, prin intermediul metodei accept(), care deschide un obiect Socket prin intermediul căruia se transmite textul stocat în controlul grafic corespunzător (după ce se obține fluxul de ieșire atașat). Se impune închiderea canalului de comunicație după ce acesta nu mai este necesar.

În momentul în care se oprește serverul, se închide obiectul de tipul ServerSocket, ceea ce va genera o excepție java.net.SocketException în cadrul metodei blocante accept(), invalidându-se și condiția care asigura execuția ciclului pe care erau tratate solicitările de conexiune provenite de la clienți.

Repornirea serverului, astfel încât să accepte din nou invocări de la clienți, presupune crearea unui nou fir de execuție (cel vechi nu poate fi refolosit).
SingleThreadedServer.java
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
 
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
 
public class SingleThreadedActivity extends Activity {
 
  private EditText serverTextEditText;
 
  private ServerThread singleThreadedServer;
 
  private ServerTextContentWatcher serverTextContentWatcher = new ServerTextContentWatcher();
  private class ServerTextContentWatcher implements TextWatcher {
 
    @Override
    public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
    }
 
    @Override
    public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
      if (charSequence.toString().equals(Constants.SERVER_START)) {
        singleThreadedServer = new ServerThread();
        singleThreadedServer.startServer();
      }
      if (charSequence.toString().equals(Constants.SERVER_STOP)) {
        singleThreadedServer.stopServer();
      }
    }
 
    @Override
    public void afterTextChanged(Editable editable) {
    }
 
  }
 
  private class ServerThread extends Thread {
 
    private boolean isRunning;
 
    private ServerSocket serverSocket;
 
    public void startServer() {
      isRunning = true;
      start();
    }
 
    public void stopServer() {
      isRunning = false;
      new Thread(new Runnable() {
        @Override
        public void run() {
          try {
            if (serverSocket != null) {
              serverSocket.close();
            }
            Log.v(Constants.TAG, "stopServer() method invoked "+serverSocket);
          } catch(IOException ioException) {
            Log.e(Constants.TAG, "An exception has occurred: "+ioException.getMessage());
            if (Constants.DEBUG) {
              ioException.printStackTrace();
            }
          }
        }
      }).start();
    }
 
    @Override
    public void run() {
      try {
        serverSocket = new ServerSocket(Constants.SERVER_PORT);
        while (isRunning) {
          Socket socket = serverSocket.accept();
          Log.v(Constants.TAG, "Connection opened with "+socket.getInetAddress()+":"+socket.getLocalPort());
          PrintWriter printWriter = Utilities.getWriter(socket);
          printWriter.println(serverTextEditText.getText().toString());
          socket.close();
          Log.v(Constants.TAG, "Connection closed");
        }
      } catch (IOException ioException) {
        Log.e(Constants.TAG, "An exception has occurred: "+ioException.getMessage());
        if (Constants.DEBUG) {
          ioException.printStackTrace();
        }
      }
    }
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_single_threaded);	
 
    serverTextEditText = (EditText)findViewById(R.id.server_text_edit_text);
    serverTextEditText.addTextChangedListener(serverTextContentWatcher);
  }
 
}

Prin intermediul utilitarului nc (apelat cu adresa Internet și portul serverului) se poate interoga mesajul care a fost transmis. De asemenea, comanda time măsoară timpul în care a fost executată operația respectivă.

student@eim2016:~$ nc 192.168.56.101 2016
Hello, EIM Student!
student@eim2016:~$ time nc 192.168.56.101 2016
Hello, EIM Student!

real    0m0.009s
user    0m0.000s
sys     0m0.000s

Fiecare comunicație dintre client și server este tratată secvențial, ceea ce poate determina latențe semnificative în cazul în care sunt înregistrate mai multe solicitări de acest tip concomitent.

De asemenea, în momentul în care coada în care sunt stocate este completată, o cerere poate fi refuzată.

Exemplu 2. Server cu fire de execuție dedicate pentru fiecare canal de comunicație în parte

Pentru ca serverul să poată trata corespunzător solicitările venite de la clienți, fără a se înregistra latențe semnificative sau cereri refuzate, mai ales în situația în care traficul prin canalul de comunicație cu fiecare dintre aceștia este intens, se impune definirea mai multor fire de execuție:

  • un fir de execuție pe care serverul așteaptă invocările;
  • câte un fir de execuție pentru comunicația cu fiecare client în parte.

Astfel, atunci când este înregistrată o solicitare venită din partea unui client (metoda accept() furnizează un obiect Socket corespunzător canalului de comunicație), se creează un fir de execuție care va gestiona comunicația dintre client și server, fără ca aceasta să aibă un impact asupra capacității serverului de a răspunde la alte cereri, venite din partea altor clienți, și asupra comunicației dintre acestea.

private class CommunicationThread extends Thread {
  private Socket socket;
 
  public CommunicationThread(Socket socket) {
    this.socket = socket;
  }
 
  @Override
  public void run() {
    try {
      Log.v(Constants.TAG, "Connection opened with "+socket.getInetAddress()+":"+socket.getLocalPort());
      PrintWriter printWriter = Utilities.getWriter(socket);
      printWriter.println(serverTextEditText.getText().toString());
      socket.close();
      Log.v(Constants.TAG, "Connection closed");
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An exception has occurred: "+ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
  }
}
 
private class ServerThread extends Thread {
  private boolean isRunning;
 
  private ServerSocket serverSocket;
 
  public void startServer() {
    isRunning = true;
    start();
  }
 
  public void stopServer() {
    isRunning = false;
    new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          if (serverSocket != null) {
            serverSocket.close();
          }
          Log.v(Constants.TAG, "stopServer() method invoked "+serverSocket);
        } catch(IOException ioException) {
          Log.e(Constants.TAG, "An exception has occurred: "+ioException.getMessage());
          if (Constants.DEBUG) {
            ioException.printStackTrace();
          }
        }
      }
    }).start();
  }
 
  @Override
  public void run() {
    try {
      serverSocket = new ServerSocket(Constants.SERVER_PORT);
      while (isRunning) {
        Socket socket = serverSocket.accept();
        new CommunicationThread(socket).start();
      }
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An exception has occurred: "+ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
  }
}

Activitate de Laborator

1. În contul Github personal, să se creeze un depozit denumit 'Laborator06'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

2. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la https://www.github.com/eim2016/Laborator06.

În urma acestei operații, directorul Laborator06 va trebui să se conțină directoarele labtasks, samples și solutions.

student@eim2016:~$ git clone https://www.github.com/eim2016/Laborator06.git

3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator06' de pe contul Github personal.

student@eim2016:~$ cd Laborator06
student@eim2016:~/Laborator06$ git remote add Laborator06_perfectstudent https://github.com/perfectstudent/Laborator06
student@eim2016:~/Laborator06$ git push Laborator06_perfectstudent master

4. Să se importe în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectul FTPServerWelcomeMessage din directorul labtasks.

Se cere să se implementeze o aplicație Android care citește mesajul de întâmpinare transmis în momentul în care se realizează o conexiune către un server FTP.

Adresa Internet a serverului FTP la care se dorește să se realizeze conexiunea va fi introdusă de utilizator, iar portul curent este 21 (reținut în Constants.FTP_PORT).

În cazul în care mesajul de întâmpinare conține mai multe linii, mesajul este precedat de șirul de caractere 220- (reținut în Constants.FTP_MULTILINE_START_CODE). Mesajul se încheie fie cu șirul de caractere 220, fie cu un șir de caractere precedat de 220 (valori stocate în Constants.FTP_MULTILINE_END_CODE1, respectiv Constants.FTP_MULTILINE_END_CODE2).

Operațiile care trebuie realizate pe metoda doInBackground() a firului de execuție ce gestionează comunicația cu serverul FTP sunt:

  1. deschiderea unui socket care primește ca parametrii:
    1. adresa Internet a serverului FTP (precizată anterior de utilizator, preluată din câmpul text FTPServerAddressEditText, transmis ca parametru al metodei - params[0])
    2. portul 21 (preluat din Constants.FTP_PORT).
  2. obținerea fluxului de intrare atașat socket-ului, printr-un apel al metodei Utilities.getBufferedReader();
  3. citirea unei linii de pe fluxul de intrare:
    1. dacă aceasta începe cu șirul de caractere 220- (preluat din Constants.FTP_MULTILINE_START_CODE), mesajul este analizat;
      1. se citesc linii de pe fluxul de intrare atașat socket-ului cât timp valoarea:
        1. nu este egală cu 220 (preluat din Constants.FTP_MULTILINE_END_CODE1);
        2. nu începe cu 220 (preluat din Constants.FTP_MULTILINE_END_CODE2);
      2. valoarea primită de la serverul FTP este inclusă în câmpul text welcomeMEssageTextView (informația se transmite, pe măsură ce este primită, prin metoda publishProgress() care face să se invoce automat metoda de callback onProgressUpdate() executată pe firul de execuție principal al aplicației Android, la care se oferă acces la controalele grafice);
    2. altfel, mesajul este ignorat.
  4. închiderea socket-ului.

Să se verifice mesajul afișat în momentul în care se realizează o conexiune la serverul ftp.ngc.com.

5. Să se importe în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectul SingleThreadedServer din directorul labtasks.

Acesta reprezintă o aplicație Android care implementează un server ce ascultă pe un port (2016, în Constants.SERVER_PORT) solicitări de conexiune provenite de la clienți.

Pe același fir de execuție pe care se face așteptarea, este realizată și transmiterea mesajului către client, preluat dintr-un câmp text.

Pornirea serverului se face prin introducerea în câmpul text a șirului de caractere Start Server.

Oprirea serverului se face prin introducerea în câmpul text a șirului de caractere Stop Server.

Preluarea invocărilor de la clienți este realizată doar atâta timp cât serverul se află în execuție.

Linux

Citirea mesajelor se face cu nc care primește ca parametrii adresa Internet și portul serverului de pe dispozitivul Android. Prin utilitarul time se verifică durata operației respective.

student@eim2016:~$ nc 192.168.56.101 2016
Hello, EIM Student!
student@eim2016:~$ time nc 192.168.56.101 2016
Hello, EIM Student!

real    0m0.009s
user    0m0.000s
sys     0m0.000s

Windows

Citirea mesajelor se face cu telnet care primește ca parametrii adresa Internet și portul serverului de pe dispozitivul Android.

În situația în care Telnet nu este disponibil, se accesează Control PanelPrograms and FeaturesTurn Windows Features on or off, selectându-se produsul Telnet Client.

C:\Users\Student> telnet 192.168.56.101 2016
Start Server

Connection to host lost.
C:\Users\Student> telnet 192.168.56.101 2016
Connecting To 192.168.56.101...Could not open connection to the host, on port 2016: Connect failed

a) Să se trimită aplicația Android în fundal prin apăsarea tastei Home. Verificați conectivitatea cu serverul. De ce sunt tratate cererile în continuare?

b) Să se apese tasta Back. De ce sunt tratate cererile în continuare după ce aplicația Android este distrusă? Ce se întâmplă în momentul în care se dorește să se repornească aplicația Android? Cum ar putea fi remediată această problemă?

Aplicația Android este distrusă, însă resursele utilizate de acestea nu sunt eliberate, motiv pentru care serverul continuă să gestioneze solicitările primite de la clienți.

Astfel, în momentul în care se dorește să se repornească aplicația Android, se va genera o excepție, întrucât portul pe care se dorește să asculte serverul invocările de la clienți este deja ocupat.

04-06 00:00:00.000: E/Single Threaded Server(934): An exception has occurred: bind failed: EADDRINUSE (Address already in use)
04-06 00:00:00.000: W/System.err(934): java.net.BindException: bind failed: EADDRINUSE (Address already in use)
04-06 00:00:00.000: W/System.err(934):     at libcore.io.IoBridge.bind(IoBridge.java:89)
04-06 00:00:00.000: W/System.err(934):     at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:150)
04-06 00:00:00.000: W/System.err(934):     at java.net.ServerSocket.<init>(ServerSocket.java:100)
04-06 00:00:00.000: W/System.err(934):     at java.net.ServerSocket.<init>(ServerSocket.java:69)

O soluție ar putea fi eliberarea resurselor pe metoda onDestroy().

@Override
public void onDestroy() {
  super.onDestroy();
  if (serverThread != null) {
    serverThread.stopServer();
  }
}

c) Să se simuleze faptul că operația de comunicare dintre client și server durează o perioadă de timp mai mare (spre exemplu, 3 secunde). Ce se întâmplă în momentul în care există mai multe solicitări trimise simultan de mai mulți clienți (din console diferite)? Monitorizați timpul de răspuns în această situație.

Metoda Thread.sleep(), primind ca parametru un interval de timp (exprimat în milisecunde) simulează o așteptare, după care execuția aplicației Android este reluată în mod normal.

try {
  Thread.sleep(3000);
} catch (InterruptedException interruptedException) {
  Log.e(Constants.TAG, interruptedException.getMessage());
  if (Constants.DEBUG) {
    interruptedException.printStackTrace();
  }
}

d) Să se implementeze comunicația pentru fiecare conexiune dintre client și server pe un fir de execuție separat, astfel încât pe server, gestiunea solicitărilor provenite de la clienți să nu fie afectată.

6. Să se importe în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectul ClientServerCommunication din directorul labtasks.

Acesta reprezintă o aplicație Android având două fragmente, implementând atât un server cât și un client:

  1. serverul dispune de un câmp text prin intermediul căruia:
    1. pot fi realizate diferite operații asupra sa:
      1. pornire, prin introducerea mesajului Start Server;
      2. oprire, prin introducerea mesajului Stop Server.
    2. se poate preciza un mesaj care să fie transmis către client prin intermediul canalului de comunicație;
  2. clientul dispune de două câmpuri text, prin care se precizează parametrii de conexiune la server (adresa de Internet / portul) precum și un buton prin intermediul căruia se realizează legătura propriu-zisă, fiind preluată informația din cadrul canalului de comunicație și afișată corespunzător.

a) Să se implementeze funcționalitatea serverului.

În clasa ComunicationThread din fragmentul asociat serverului:

  1. se obține o referință către fluxul de ieșire asociat socket-ului, prin intermediul metodei ajutătoare Utilities.getWriter();
  2. se scrie o linie conținând textul din serverTextEditText.

b) Să se implementeze funcționalitatea clientului.

În clasa ClientAsyncTask din fragmentul asociat serverului:

  1. se resetează conținutul câmpului text serverMessageTextView, astfel încât acesta să conțină șirul vid; acest lucru este realizat înainte de a rula firul de execuție dedicat, așadar pe metoda onPreExecute();
  2. se obțin parametrii de conexiune la server (adresa de Internet, obținută din câmpul text serverAddressEditText și portul, obținut din câmpul text serverPortEditText); aceștia sunt transmiși prin intermediul argumentelor metodei doInBackground();
  3. se deschide un socket, folosind configurația precizată de utilizator;
  4. se obține o referință către fluxul de intrare asociat socket-ului, prin intermediul metodei ajutătoare Utilities.getReader();
  5. se citesc linii din contextul canalului de comunicație, atâta timp cât nu se întâlnește EOF (nu se întoarce valoarea null), valoarea fiind concatenată la câmpul text serverMessageTextView; acest lucru este realizat prin transmiterea liniilor către firul de execuție principal - al interfeței grafice - (unde controalele grafice sunt accesibile), prin intermediul metodei publishProgress() care determină apelul metodei de callback onPublishProgress();
  6. se închide socketul.
Accesul la controalele grafice trebuie să fie realizat numai din contextul firului de execuție asociat interfeței cu utilizatorul.

c) Să se pornească serverul (se introduce textul Start Server). În client, să se verifice valoarea furnizată în cazul în care se introduc valorile 127.0.0.1 / 2016, respectiv localhost / 2016.

Să se oprească serverul (se introduce textul Stop Server). Să se verifice ce valori sunt furnizate în această situație.

d) (doar Linux) Să se creeze o listă cu procesele care rulează la momentul curent, textul putând fi obținut, linie cu linie, prin intermediul unei conexiuni pe portul 2016, serverul fiind astfel simulat pe mașina fizică.

student@eim2016:~$ ps a | while read x; do echo "$x" | nc -l 2016; done
Pe mașinile Debian, comanda nc se rulează cu opțiunea suplimentară -p pentru a se indica portul pe care se dorește ca acesta să accepte invocările.

Să se folosească clientul pentru a citi, linie cu linie, valorile furnizate de script-ul anterior. Va trebui precizată adresa mașinii fizice (în funcție de configurația folosită) și portul 2016.

7. Să se importe în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectul PheasantGame din directorul labtasks.

Acesta reprezintă o aplicație Android care implementează jocul Fazan în limba engleză, atât partea de server, cât și partea de client.

Vor fi folosite următoarele reguli de joc:

  1. clientul trimite un cuvânt serverului;
  2. serverul verifică dacă nu s-a solicitat terminarea jocului, situație în care continuă analiza termenului respectiv;
  3. dacă s-a primit un cuvânt care are prefixul așteptat, termenul existând în limba engleză, se caută un alt cuvânt care începe cu ultimele 2 litere ale termenului:
    1. dacă există un astfel de cuvânt, acesta fiind transmis înapoi clientului;
    2. altfel, se semnalează faptul că jocul s-a terminat;
  4. altfel, se trimite înapoi cuvântul primit pentru a semnala faptul că există o problemă.

Astfel, serverul va avea următoarea funcționare:

  1. dacă se primește End Game (stocat în Constants.END_GAME), pentru a se indica faptul că nu se mai dorește continuarea jocului sau clientul a fost închis (nu găsește nici un cuvânt cu prefixul solicitat), serverul își termină execuția;
  2. altfel, serverul verifică dacă termenul care a fost primit reprezintă un cuvânt valid în limba engleză (accesând în acest sens pagina Internet http://www.wordhippo.com/what-is/words-starting-with/) - se folosește metoda Utilities.wordValidation();
    1. dacă termenul este valid:
      1. verifică dacă termenul începe cu prefixul solicitat (ultimele 2 litere ale cuvântului pe care l-a trimis anterior) - în acest sens, trebuie reținut cel mai recent cuvânt transmis / prefixul așteptat;
        1. în caz afirmativ:
          1. identifică prefixul cuvântului pe care trebuie să îl formeze (ultimele 2 litere ale cuvântului primit);
          2. realizează o interogare la nivelul paginii Internet http://www.wordhippo.com/what-is/words-starting-with/ pentru a obține lista cuvintelor care respectă constrângerea respectivă - se folosește metoda Utilities.getWordListStartingWith() care furnizează un obiect List<String>;
            1. dacă lista conține unul sau mai multe cuvinte, face o alegere întâmplătoare și se transmite clientului;
            2. dacă lista nu conține nici un cuvânt, transmite clientului șirul de caractere End Game (stocat în Constants.END_GAME), pentru a indica faptul că a fost închis (nu găsește nici un cuvânt cu prefixul solicitat);
        2. în caz negativ, transmite înapoi cuvântul pe care l-a primit;
    2. dacă termenul nu este valid, trimite înapoi cuvântul pe care l-a primit.
Este posibil ca un cuvânt care există în limba engleză să nu fie marcat ca valid, datorită faptului că se inspectează numai prima pagină dintre cele care conțin lista de cuvinte precedate de un anumit prefix.

Similar, clientul va avea următoarea funcționare:

  1. dacă termenul din câmpul text wordEditText are mai mult de două caractere, este trimis clientului;
  2. se primește o valoare de la server:
    1. dacă se primește End Game, se invalidează controalele grafice, astfel încât jocul să nu poată fi continuat;
    2. dacă s-a primit alt cuvânt decât cel trimis anterior, se determină prefixul cu care trebuie format termenul și se afișează în câmpul text wordEditText;
    3. dacă s-a primit același cuvânt cu cel trimis anterior, înseamnă că termenul nu a fost corect, astfel încât trebuie format un cuvânt cu același prefix ca anterior, acesta fiind afișat în câmpul text wordEdiTtext;
  3. altfel, se afișează un mesaj de eroare.

Informațiile cu privire la șirurile de caractere care au fost trimise, respectiv primite, vor fi jurnalizate în câmpurile text serverHistoryTextView și clientHistoryTextView.

8. Să se încarce modificările realizate în cadrul depozitului 'Laborator06' de pe contul Github personal, folosind un mesaj sugestiv.

student@eim2016:~/Laborator06$ git add *
student@eim2016:~/Laborator06$ git commit -m "implemented taks for laboratory 06"
student@eim2016:~/Laborator06$ git push Laborator02_perfectstudent master

Resurse Utile

Connecting to the Network
Managing Network Usage
Wei Meng LEE, Beginning Android 4 Application Development, Wiley, 2012 - capitolul 10, Networking, subcapitolul How to connect to a Socket server
Reto MEIER, Professional Android for Application Development, John Wiley & Sons, 2012 - capitolul 16 (Bluetooth, NFC, Networks and Wi-Fi), subcapitolele Managing Network and Internet Connectivity, Transferring Data using Wi-Fi Direct
Android Programming Tutorials - Core Servlets - secțiunea Networking I: General Techniques
Dezvoltarea Aplicațiilor pentru Android - partea a II-a, Sockeți
Processes and Threads
ServerSocket
Socket

laboratoare/laborator06.txt · Last modified: 2016/04/08 07:52 by Andrei Roșu-Cojocaru
CC Attribution-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0