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.
vbox86p:/ # ip ro 172.16.4.0/22 dev eth1 proto kernel scope link src 172.16.7.89 192.168.57.0/24 dev eth0 proto kernel scope link src 192.168.57.101
interfețele, adresele, rutele (în telefon sau emulator)
student@eg106:~$ ip ro default via 172.16.7.254 dev eno1 proto dhcp metric 100 169.254.0.0/16 dev eno1 scope link metric 1000 172.16.4.0/22 dev eno1 proto kernel scope link src 172.16.7.36 metric 100 172.16.101.0/24 dev vmnet1 proto kernel scope link src 172.16.101.1 192.168.57.0/24 dev vboxnet1 proto kernel scope link src 192.168.57.1
interfețele, adresele, rutele (mașina de dezvoltare Linux)
netstat -rn
interfețele, adresele, rutele (mașina de dezvoltare OSX)
ipconfig
interfețele, adresele, rutele (în telefon sau emulator)
O mașină fizică și un dispozitiv fizic pot comunica:
Pe telefon, se accesează Settings → Wireless & Networks → Tethering & 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@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb devices List of devices attached 0019531d59461f device student@eim-lab:/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:/ #
student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb devices List of devices attached 0019531d59461f ???????????? student@eim-lab:/opt/android-sdk-linux/platform-tools$ sudo ./adb kill-server student@eim-lab:/opt/android-sdk-linux/platform-tools$ sudo ./adb start-server student@eim-lab:/opt/android-sdk-linux/platform-tools$ sudo ./adb devices List of devices attached 0019531d59461f device
Pentru dispozitivele care nu permit conectarea cu adb shell
în modul tethering se aplică soluția prezentată pe 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@eim-lab:~$ 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
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ă.
Pentru legătura virtualbox-emulator este utilizat spațiul de adrese 192.168.56.1/24
:
192.168.56.1
;192.168.56.101
.. 192.168.56.254
.
Pentru legătura wifi a emulatorului utilizat spațiul de adrese 172.16.4.0/22
:
Configurarea spațiului de adrese care este folosit poate fi configurat prin intermediul VirtualBox, accesând File → Preferences → Network → Host Only Networks
Adresa IP care a fost atașată fiecărui dispozitiv virtual Genymotion poate fi verificată:
adb devices
; student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb devices List of devices attached 192.168.56.101:5555 device
Conectivitatea dintre mașina de dezvoltare și dispozitivul virtual poate fi verificată folosind comanda ping
.
student@eim-lab:~$ 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
Fiecare instanță a unui dispozitiv virtual Android oferă o pereche de porturi pentru diferite conexiuni:
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ă:
adb devices
student@eim-lab:/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@eim-lab:~$ telnet localhost 55nr
specificându-se portul pe care rulează emulatorul.
auth
, urmată de cheia dispozitivului virtual. Cheia se află în ~/.emulator_console_auth_token
Î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@eim-lab:~$ 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@eim-lab:~$
Comunicația între mai multe dispozitive poate fi realizată:
http
;
<manifest ...> <!-- other application properties or components --> <uses-permission android:name="android.permission.INTERNET" /> <!-- other application properties or components --> </manifest>
Folosirea unui socket TCP presupune comunicația între două entități:
Î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
.
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 = 2000; Socket socket = new Socket(hostname, port);
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:
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);
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:
Trimiterea efectivă a datelor este realizată atunci când se apelează metoda flush() sau automat la întâlnirea caracterului '\n', în funcție de argumentele cu care a fost invocat constructorul.
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);
Metodele implementate de clasa BufferedReader sunt:
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
.
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:
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.
close()
eliberează și resursele folosite de fluxul de intrare și fluxul de ieșire asociate, care sunt de asemenea închise.
Î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:
Activity
;android.view.View
, pentru fiecare control grafic care se dorește a fi accesat;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ă);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()
.// VARIANTA 1 doInBackground{ /*** Net thread ***/ socket(); ... socket.close(); return result } onPostExecute(String result) { /*** GUI thread ***/ daytimeProtocolTextView.setText(result); } // VARIANTA 2 doInBackground{ /*** Net thread ***/ socket.. publishProgress(line); publishProgress(line); ... socket.close(); return null; } onProgressUpdate(String... progres) { /*** GUI thread ***/ welcomeMessageTextView.append(progres[0] + "\n"); } onPostExecute(Void result) {} /*** GUI thread ***/
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 utcnist.colorado.edu, 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.
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); } }
Un sever poate “aștepta” conexiuni de clienți prin intermediul unei instanțe a clasei ServerSocket, care poate primi ca parametri:
getLocalPort()
;getInetAddress()
.
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.
Se implementează o aplicație Android cu un singur câmp text prin care se controlează:
Start Server
;Stop Server
;
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.
<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.
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ă.
ls -l `which nc`
student@eim-lab:~$ nc 172.16.7.89 2000 Hello, EIM Student! student@eim-lab:~$ time nc 172.16.7.89 2000 Hello, EIM Student! real 0m0.009s user 0m0.000s sys 0m0.000s
Există și în Android o variantă simplificată de nc, care poate fi apelată folosind busybox:
busybox nc ftp.ngc.com 21 busybox nc -l -p 5000
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ă.
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:
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(); } } } }
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/eim-lab/Laborator06.
În urma acestei operații, directorul Laborator06 va trebui să se conțină directoarele labtasks
, samples
și solutions
.
student@eim:~$ git clone https://www.github.com/eim-lab/Laborator06.git
3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator06' de pe contul Github personal.
student@eim:~$ cd Laborator06 student@eim:~/Laborator06$ git remote add Laborator06_perfectstudent https://github.com/perfectstudent/Laborator06 student@eim:~/Laborator06$ git push Laborator06_perfectstudent master
4. Să se importe în mediul integrat de dezvoltare Android Studio 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:
FTPServerAddressEditText
, transmis ca parametru al metodei - params[0]
)Constants.FTP_PORT
).Utilities.getBufferedReader()
;220-
(preluat din Constants.FTP_MULTILINE_START_CODE
), mesajul este analizat;220
(preluat din Constants.FTP_MULTILINE_END_CODE1
);220
(preluat din Constants.FTP_MULTILINE_END_CODE2
);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);Să se verifice mesajul afișat în momentul în care se realizează o conexiune la serverul intranet.ngcaerospace.com.
5. Să se importe în mediul integrat de dezvoltare Android Studio proiectul SingleThreadedServer
din directorul labtasks
.
Acesta reprezintă o aplicație Android care implementează un server ce ascultă pe un port (2000, î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.
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@eim-lab:~$ nc 172.16.7.89 2000 Hello, EIM Student! student@eim-lab:~$ time nc 172.16.7.89 2000 Hello, EIM Student! real 0m0.009s user 0m0.000s sys 0m0.000s
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 Panel → Programs and Features → Turn Windows Features on or off, selectându-se produsul Telnet Client.
C:\Users\Student> telnet 192.168.56.101 2000 Start Server Connection to host lost. C:\Users\Student> telnet 192.168.56.101 2000 Connecting To 192.168.56.101...Could not open connection to the host, on port 2000: 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ă?
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.
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 Android Studio 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:
a) Să se implementeze funcționalitatea serverului.
În clasa ComunicationThread
:
Utilities.getWriter()
;serverTextEditText
.b) Să se implementeze funcționalitatea clientului.
În clasa ClientAsyncTask
:
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()
;serverAddressEditText
și portul, obținut din câmpul text serverPortEditText
); aceștia sunt transmiși prin intermediul argumentelor metodei doInBackground()
;Utilities.getReader()
;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()
;
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
/ 2000
, respectiv localhost
/ 2000
.
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 2000, serverul fiind astfel simulat pe mașina fizică.
student@eim:~$ ps a | while read x; do echo "$x" | nc -l 2000; done
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 2000
7. Să se importe în mediul integrat de dezvoltare Android Studio 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:
Astfel, serverul va avea următoarea funcționare:
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;Utilities.wordValidation()
;Utilities.getWordListStartingWith()
care furnizează un obiect List<String>
;Constants.END_GAME
), pentru a indica faptul că a fost închis (nu găsește nici un cuvânt cu prefixul solicitat);
Similar, clientul va avea următoarea funcționare:
wordEditText
are mai mult de două caractere, este trimis clientului;wordEditText
;wordEdiTtext
;
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@eim:~/Laborator06$ git add * student@eim:~/Laborator06$ git commit -m "implemented taks for laboratory 06" student@eim:~/Laborator06$ git push Laborator02_perfectstudent master
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