Sunteți pe pagina 1din 16

Ministerul Educaţiei al Republicii Moldova

Universitatea Tehnică a Moldovei


Facultatea Calculatoare Informatica si
Microelectronica Departamentul Ingineria
Software si Automatica

RAPORT

Lucrarea de laborator nr.5


Disciplina: Programare în rețea
Tema: Video streaming

A efectuat: st. gr. TI-171 Guzun Ion

A verificat: Buldumac Oleg

Chisinau 2020
Mersul lucrării:
Server
Primul pas pentru crearea video streaming-ului a fost Serverul, anume el este centrul cu care
comunică fiecare dintre clienții conectați. Serverul și este aplicația ce înregistrează video de pe
desktop, setează video la sine și apoi le transmite clienților. UI al serverului arată astfel:

Figura 1. Interfața grafică a serverului

La pornirea serverului se crează un thread de înregistrare a desktop-ului -


startStreamThread(), un thread de ascultare a clienților startWaitingMessagesThread() și se
trasmite mesaj despre aceea că stream-ul este online:

private void startStream() {


// Set running to true
running = true;
// Create new screen recording thread
startStreamThread();
// Create new waiting for clients thread
startWaitingMessagesThread();
// Send message that stream is online
String message = "\\online";
sendMessageToAllClients(message.getBytes());
}

private void startStreamThread() {


new Thread("Screen Recorder") {

BufferedImage image;
BufferedImage scaledImage;
@Override
public void run() {
try {
while (running) {
// Get a desktop screenshot
image = getImage();

// Scale image and


scaledImage = Scalr.resize(image, Scalr.Method.BALANCED, 480, 270);

// Send image
sendMessageToAllClients(getBytes(scaledImage));

// Set screenshot as label icon


jLabelDisplayImages.setIcon(new ImageIcon(scaledImage));

image = ImageIO.read(ScreenRecorder.class.getResource("/images/StreamOffline.jpg"));
scaledImage = Scalr.resize(image, Scalr.Method.BALANCED, 480, 270);

jLabelDisplayImages.setIcon(new ImageIcon(scaledImage));
} catch (IOException ex) {
Logger.getLogger(ScreenRecorder.class.getName()).log(Level.SEVERE, null, ex);
}
}
}.start();
}

private void startWaitingMessagesThread() {


new Thread("Waiting for Clients Thread") {
DatagramPacket packet;
byte[] waitingBuffer;

@Override
public void run() {
while (running) {
try {
waitingBuffer = new byte[256];

packet = new DatagramPacket(waitingBuffer, waitingBuffer.length);


datagramSocket.receive(packet);

processingMessage(packet);

} catch (IOException ex) {


Logger.getLogger(ScreenRecorder.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}.start();
}
Variabila running, de tip boolean, răspunde pentru timpul de lucru al fiecărui thread, dacă:
 running == true – thread-urile lucrează,
 running == false – thread-urile finisează luc
La oprirea serverului are loc aprirea thread-urilor și transmiterea mesajului că stream-ul este
offline:
private void stopStream() {
// Send message that stream is online
sendMessageToAllClients("\\offline".getBytes());
// Set running to true
running = false;
}

Cu trecerea de la statusul: online – offline se folosește o metodă anumită


changeStreamStatus(), iar pentru schimbarea culorii butoanelor se folosește metoda
changeStreamStatusColor():
private void changeStreamStatus() {
changeStreamStatusColor();

if (!streamStatus) {
startStream();
} else {
stopStream();
}

// Change stream status


streamStatus = !streamStatus;
}

private void changeStreamStatusColor() {

// If stream is on than change to off


if (jPanelStreamStatusOff.getBackground() == brightRed
|| jPanelStreamStatusOn.getBackground() == darkGreen) {

// Set button StreamStatusOff to off position


jPanelStreamStatusOff.setBackground(darkRed);
jLabelStreamStatusOff.setForeground(Color.GRAY);
// Set button StreamStatusOn to on position
jPanelStreamStatusOn.setBackground(brightGreen);
jLabelStreamStatusOn.setForeground(Color.white);

// else if stream is of than change to on


} else if (jPanelStreamStatusOff.getBackground() == darkRed
|| jPanelStreamStatusOn.getBackground() == brightGreen) {

// Set button StreamStatusOff to on position


jPanelStreamStatusOff.setBackground(brightRed);
jLabelStreamStatusOff.setForeground(Color.white);

// Set button StreamStatusOn to off position


jPanelStreamStatusOn.setBackground(darkGreen);
jLabelStreamStatusOn.setForeground(Color.GRAY);
} else {
System.out.println("Stream status error");
}
}

Dacă clientul dorește să se conecteze la server, acesta este primit de către thread și adăugat
la lista de clienți:
private ArrayList<StreamConnection> streamConnections = new
ArrayList<StreamConnection>();
Clasa StreamConnection răspunde pentru transmiterea sau primirea datelor dintre client-
server; fiecare obiect de tip StreamConnection conține informație despre fiecare dintre clienți.
public class StreamConnection {
private String name;
private InetAddress address;
private int port;

StreamConnection(String name, InetAddress address, int port) {


this.name = name;
this.address = address;
this.port = port;
}

String getName() {
return name;
}

InetAddress getAddress() {
return address;
}
int getPort() {
return port;
}
}

Pentru transmitere se folosește metoda sendMessage, iar pentru transmitere către toți clienții
sendMessageToAllClients:
private byte[] getBytes(String message) {
buffer = new byte[256];
buffer = message.getBytes();

return buffer;
}

private void sendMessage(byte[] bytes, InetAddress address, int port) {


try {
if (running) {
// Send bytes
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, port);
datagramSocket.send(packet);
}
} catch (IOException ex) {
Logger.getLogger(ScreenRecorder.class.getName()).log(Level.SEVERE, null, ex);
}
}

private void sendMessageToAllClients(byte[] bytes) {


for (int i = 0; i < streamConnections.size(); i++) {
StreamConnection streamConnection = streamConnections.get(i);

//String name = streamConnection.getName();


InetAddress address = streamConnection.getAddress();
int port = streamConnection.getPort();

sendMessage(bytes, address, port);


}
}

Fiecare mesaj este verificat la comandă de către server prin metoda processingMessage(), iar
la adăugarea unui nou client – se verifică dacă este sau nu ocupat numele curent:
Lista comenzilor ce pot fi primite de către server:
 message – comandă ce conține un mesaj simplu
 connect – comandă de adăugare a unui nou user
 disconnect – comandă de ștergere a unui user

// Proccesing message
private void processingMessage(DatagramPacket packet) {
String message = new String(packet.getData());

if (message.startsWith("\\message:")) {
String messageToAdd = message.substring(message.indexOf(":") + 1,
message.indexOf("\\end"));

// Add message to chat panel


jTextPaneChat.setText(jTextPaneChat.getText().concat(messageToAdd + "\n"));

sendMessageToAllClients(packet.getData());

} else if (message.startsWith("\\connect:")) {
// Get user data
String name = message.substring(message.indexOf(":") + 1, message.indexOf("\\end"));
InetAddress address = packet.getAddress();
int port = packet.getPort();

if (verifyName(name)) {
// Create new user
StreamConnection newUser = new StreamConnection(name, address, port);

// Add new user to list


streamConnections.add(newUser);

// Add text to chat


jTextPaneChat.setText(jTextPaneChat.getText().concat(" [" + name + "] entered the
stream." + "\n"));

// Send message about new user to all client


sendMessageToAllClients(getBytes("\\message: [" + name + "] entered the stream." +
"\\end"));

// Send message that stream is online


String messageStreamOnline = "\\online";
sendMessageToAllClients(messageStreamOnline.getBytes());
} else
sendMessage("\\busyName".getBytes(), address, port);
} else if (message.startsWith("\\disconnect:")) {
// Get user name
String name = message.substring(message.indexOf(":") + 1, message.indexOf("\\end"));

// Disconnect user
for (int i = 0; i < streamConnections.size(); i++) {
StreamConnection user = streamConnections.get(i);
if (user.getName().equals(name)) {
streamConnections.remove(i);
break;
}
}

// Add text to chat


jTextPaneChat.setText(jTextPaneChat.getText().concat(" [" + name + "] came out of the
stream." + "\n"));
// Send message about new user to all client
sendMessageToAllClients(getBytes("\\message: [" + name + "] came out of the stream." +
"\\end"));
}
}

// Verify if user exist in list


boolean verifyName(String name) {
for (int i = 0; i < streamConnections.size(); i++) {
StreamConnection user = streamConnections.get(i);
if (user.getName().equals(name)) {
return false;
}
}

return true;
}

Client

Clientul la rândul său începe cu interfața de logare:

Figura 2. Interfața grafică a clientului

După introducerea numelui se transmite mesaj de conectare către server:


private void startClient() {
try {

clientSocket = new DatagramSocket();


address = InetAddress.getByName("localhost");

// Send message to connect to server


sendMessage("\\connect:", name);

// Set running to true


running = true;

// Start thread to listen server


startClientThread();
// Start thread to set stream status
//startStreamStatusMessageThread();
} catch (IOException ex) {
Logger.getLogger(ClientForm.class.getName()).log(Level.SEVERE, null, ex);
}
}

// Send message to connect to server


private void sendMessage(String type, String message) {
try {
if (name != null) {
String messageToSend = type + message + "\\end";

byte[] connectionbBuffer = messageToSend.getBytes();


DatagramPacket packet = new DatagramPacket(connectionbBuffer,
connectionbBuffer.length, address, port);
clientSocket.send(packet);
}
} catch (IOException ex) {
Logger.getLogger(ClientForm.class.getName()).log(Level.SEVERE, null, ex);
}
}

La creare se pornește thread-ul ce răspunde pentru notificarile clientului:


private void startStreamStatusMessageThread() {
new Thread("Stream status") {

int labelWidth = jLabelStreamStatusMessage.getWidth();

@Override
public void run() {
jLabelStreamStatusMessage.setText("");
String nameless = "Enter your name";

String onStream = "Stream is now online";


String offStream = "Stream is now offline";

String currentMessage = "";

char charAt;

while (streamStatusRunning) {
try {

if (name == null) {
currentMessage = nameless;
jLabelStreamStatusMessage.setForeground(new Color(228, 141, 154));
} else if (streamStatus == false) {
currentMessage = offStream;
jLabelStreamStatusMessage.setForeground(new Color(228, 141, 154));
} else if (streamStatus == true && adminMessages.isEmpty()) {
currentMessage = onStream;
jLabelStreamStatusMessage.setForeground(new Color(0, 255, 51));
} else if (streamStatus == true && !adminMessages.isEmpty()) {
currentMessage = adminMessages.get(0);
adminMessages.remove(0);
jLabelStreamStatusMessage.setForeground(new Color(0, 255, 51));
}

jLabelStreamStatusMessage.setText("");

for (int i = currentMessage.length() - 1; i >= 0; i--) {


charAt = currentMessage.charAt(i);

jLabelStreamStatusMessage.setText(String.valueOf(charAt).concat(jLabelStreamStatusMessage.getText(
)));
Thread.sleep(100);
}

for (int i = 0; i < 5; i++) {


jLabelStreamStatusMessage.setText("
".concat(jLabelStreamStatusMessage.getText()));
Thread.sleep(100);
}

Thread.sleep(3000);

for (int i = 200 - currentMessage.length() - 1; i >= 0; i--) {


jLabelStreamStatusMessage.setText("
".concat(jLabelStreamStatusMessage.getText()));
Thread.sleep(40);
}

//Thread.sleep(2000);
} catch (InterruptedException ex) {
Logger.getLogger(ClientForm.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}.start();
}
Figura 3. Notificare despre starea stream-ului(online)

Figura 4. Notificare despre starea stream-ului(offline)

Figura 5. Notificare despre mesajul adminului

Ca și serverul, clientul are metoda de procesare a mesajelor:


// Proccesing message
private void processingMessage(DatagramPacket packet) {
String message = new String(packet.getData());
if (message.startsWith("\\message")) {
String messageToAdd = message.substring(message.indexOf(":") + 1,
message.indexOf("\\end"));

if (messageToAdd.startsWith(" Admin")) {
System.out.println(messageToAdd);
adminMessages.add(messageToAdd);
}

// Add message to chat panel


jTextPaneChat.setText(jTextPaneChat.getText().concat(messageToAdd + "\n"));
} else if (message.startsWith("\\online")) {
streamStatus = true;

} else if (message.startsWith("\\offline")) {
streamStatus = false;

try {
BufferedImage image =
ImageIO.read(StreamClient.class.getResource("/images/StreamOffline.jpg"));
image = Scalr.resize(image, Scalr.Method.BALANCED, 480, 270);
jLabelDisplayImages.setIcon(new ImageIcon(image));
} catch (IOException ex) {
Logger.getLogger(ClientForm.class.getName()).log(Level.SEVERE, null, ex);
}
} else if (message.startsWith("\\busyName")) {
jTextFieldName.setEditable(true);

jTextPaneChat.setText(jTextPaneChat.getText().concat(" [Client]: Name is already used!" +


"\n"));
} else {
try {

InputStream in = new ByteArrayInputStream(packet.getData());


BufferedImage image = ImageIO.read(in);

jLabelDisplayImages.setIcon(new ImageIcon(image));
} catch (IOException ex) {
Logger.getLogger(ClientForm.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

La închiderea clientului se transmite mesaj de disconnect către server:


private void closeClient() {
running = false;
streamStatusRunning = false;

sendMessage("\\disconnect:", name);
}
Întrebări la apărarea laboratorului:
- Ce este un protocol orientat către tranzacții, fără conexiune ?
Este protocolul ce poate transmite și primi pachete fără verificarea datelor. În mare parte
cel ce transmite nu știe dacă au ajuns sau nu datele, el doar le transmite în continuu, la rândul
său cel ce primește nu poate fi sigur că toate datele ajung la el fără pierderi.

- Ce tipuri de aplicații beneficiază în general de utilizarea protocolului UDP ?


De obicei sunt aplicațiile ce necesită viteză, fără întârzieri, absolut tolerante la pierderea
datelor.
- De ce protocolul UDP nu garantează că datele vor fi transmise cu succes ?
Deoarece transmiterea se face fără o anumită conexiune, în mare parte doar într-o
direcție. Ca exemplu putem lua aplicații de transmitere video - stream, ea poate lucra și fără
clienți, iar transmiterea continuie atâta timp cât funcționează serverul. Aici principala este
viteza de transmitere a datelor, în caz că ea întârzie clientul va privi mai mult o prezentare
power point decât video.

- Diferența dintre blocking si non-blocking sockets.


Socket-urile blocking se blochează ori de câte ori se efectuază o anumită operație, și este
posibil să nu fie completată imediat și să stopeze programul. Un bun exemple poate servi
ServerSocket, în limbajul Java, ce are metoda accept() – aceasta răspunde pentru conexiunea
către socket, și de aceea la fiecare apel ea se stopează și așteaptă o nouă conexiune, de aceea
se recomandă de scos această operațiune într-un nou thread.
Socket-urile non-blocking se utilizează când programul încearcă să transmită/primească
periodic date sau socket-ul are un anumit event ce notifică programul ori de câte ori acesta
are loc.

- În protocolul TCP există Three Way Handshake, de ce în UDP nu există ?


Three Way Handshake răspunde în primul rând pentru stabilirea conexiunii dintre două
calculatoare pentru transmiterea securizată a datelor, la rândul său UDP nu are nevoie de
stabilirea conexiunii, el transmite datele direct.

- Numiti cele 2 apeluri de sistem necesare pentru a crea un server UDP.


Apelurile de sistem necesate pentru a creaa un server UDP sunt: sendto() și recvfrom() –
[read și write].

- Care este rolul metodei bind() ?


Funcția bind() asociază socketulul serverului IP adresa și portul. Astfel pentru ca clientul
să se conecteze la server va trebui să specifice această IP adresă și acest port.

- Care este rolul metodelor sendto() și recvfrom() ?


Metoda sendto() răspunde pentru transmite datele în socket, iar recvfrom() pentru
primirea datelor, așteptând o datagramă de intrare pe o anumită adresă de transport.

- Care este dimensiunea antetului unui pachet UDP în octeți ?


8 octeți.

- Într-o conexiune UDP, clientul sau serverul trimite mai întâi datele ?
Într-o conexiune UDP, pentru primirea datelor despre client, este nevoie ca clientul să
transmită datele primul, iar serverul deja trebuie să asculte.

- Care este adresa de loopback IPv6 și care este rolul ei ?


::1/128 – adresa loopback IPv6
Adresa de loopback permite a stabili conexiune și transmite date pentru programele
server ce rulează pe același calculator ca programele client.

- Datele primite prin recvfrom() au întotdeauna aceeași dimensiune cu datele trimise cu


sendto()?
Dacă se are în vedere despre dimensiunea maximă – atunci da, dar dacă despre lungimea
datelor, atunci dimensiunea depinde de datele transmise, plus că la transmitere noi singuri
transmitem dimensiunea în biți ale datelor.

- De ce nu este folosit algoritmul Nagle în protocolul UDP ?


Algoritmul Nagle funcționează prin întârzierea intenționată a pachetelor. Făcând acest
lucru, crește eficiența lățimii de bandă și agravează latența. Astfel, fiind un algoritm chiar de
succes pentru unele rețelele TCP, în rețelele UDP este absolut imposibil de folosit.

- Același program poate folosi UDP și TCP ?


Nu, deoarece, cum am mai spus mai sus, dacă folosim UDP avem nevoie de viteză, iar
când TCP siguranță că datele ajung la destinație. Astfel cum video streaming-ul nu poate să-
și permită transmiterea datelor prin TCP, deoarece transmisia va arăta ca imagini, astfel și
cloud nu-și poate permite pierderea datelor.

- Diferența dintre aplicații UDP Unicast, Broadcast, și Multicast


1. Unicast transmite mesaje doar unei stații din rețea.
2. Multicast transmite mesaje unui grup de stații din rețea.
3. Broadcast transmite la toate posturile din rețea.

- Ce face mai ușor multiplexarea cu UDP decît cu TCP ?


Deoarece UDP este un protocol de transport fără conexiune, nu are rost să solicităm
scheme multiplex. UDP, ca toate protocoalele fără conexiune de pe toate nivelurile, trebuie
doar să ia segmente de date (pachete) unul câte unul și să le proceseze conform regulilor sale.

- În protocolul UDP este un antet „Total length”, cum se calculează și care este rolul lui ?
Antetul ”Total length” este folosit pentru că UDP transmite mesaje datagram cu o
lungime care poate fi trimis pe mai multe pachete IP fragmentate; plus UDP poate fi transmis
și prin alt protocol decât IP.
Total length = UDP data - UDP header length