Client-Server komunikacija

 

TCP/IP je skup protokola koji se koristi za povezivanje računala i srodnih uređaja. Iako unutar njega postoji više protokola promatrat ćemo samo dva, TCP(Connection oriented)  i UDP (Connectionless) te ćemo pričati o StreamSocket-ima odnosno Datagram Socket-ima. Što je zapravo socket. Ako za trenutak izuzmemo sockete na lokalnom čvoru (Unix Sockets) i promatramo samo one Internet sockete, možemo reći da je socket pristupna točka (definirana određenim parametrima) koja služi za komunikaciju između aplikacija na različitim računalima.

 

Karakteristike:

Bespojna uluga (Connectionless) – UDP protokol

-         osnovna jedinica prijenosa je datagram

-         bespojna veza

-         ne garantira se isporuka pojedinog datagrama, ali se također može desiti isporuka više identičnih kopija istog datagrama

-         ne garantira se očuvanje redoslijeda poslanih datagrama

-         nema kontrole protoka

Spojna usluga:

-         pouzdanost: svaki niz poslanih okteta će sigurno stići na odredište, osim u slučaju prekida veze ili neke druge greške na poslužitelju o čemu će aplikacija pošiljatelj biti obaviještena

-         kontrola toka – nije moguće zaguženje primatelja od strane pošiljatelja

-         očuvani redoslijed isporuke

 

Strukture potrebne za rad sa socketima

 

Svaki socket je zapravo deskriptor (socket descriptor) što je cijeli broj. Osnovna struktura koju funkcije očekuju je

struct sockaddr {
        unsigned short    sa_family;    // address family, AF_xxx
        char              sa_data[14];  // 14 bytes of protocol address
    }; 

međutim zbog jednostavnijeg korištenje razvijena je struktura:

 

    struct sockaddr_in {

        short int          sin_family;  // Address family (AF_xxx)

        unsigned short int sin_port;    // Port number

        struct in_addr     sin_addr;    // Internet address

        unsigned char      sin_zero[8]; // postaviti na 0 sa memset

    };

 

(Napomena: sin_zero element strukture služi da bi ova struktura imala istu veličinu kao i struktura struct_sockaddrsockaddr_in je izvedeno zbog lakšeg rukovanja.)

Pokazivač na ovu strukturu može se castati u struct sockaddr* i obrnuto.

 

struct in_addr je struktura sa samo jednim članom (unsigned long s_addr).U slučaju klijenta ovo polje sadrži adresu računala kojem se pristupa i na kojem se očekuje poslužitelj, dok kod servera ovo polje sadrži adresu sučelja na koje se veže server. U slučaju da želimo da server prima pakete na sva sučelja koristi se konstanta INADDR_ANY.
 

Članovi strukure sin_port i sin_addr moraju imati vrijednost u network-byte orderu, pa se koriste funkcije:

       The htonl() function converts the  long  integer  hostlong

       from host byte order to network byte order.

 

       The  htons() function converts the short integer hostshort

       from host byte order to network byte order.

 

       The ntohl() function converts  the  long  integer  netlong

       from network byte order to host byte order.

 

       The  ntohs()  function converts the short integer netshort

  from network byte order to host byte order.

 

Adrese koje se koriste i stavljaju unutar ove strukture su 32-bitne adrese. U slučaju da imamo (s našeg gledišta) standardni zapis adrese (npr. 161.53.119.118) ona se mora pretvoriti u 32-bitni broj za što možemo iskoristiti funkciju :

 

int inet_aton(const char *cp, struct in_addr *inp);
int inet_pton(int af, const char *cp, struct in_addr *inp);  (novija)
 
(obrnuta funkcija je inet_ntoa )
U slučaju da ove gornje funkcija ne postoje može se koristiti funkcija 
inet_addr() 
koja u slučaju greške vraća -1 što može biti krivo interpretirano kao (255.255.255.255)
 
 

Redoslijed pozivanja

Slika prikazuje redoslijed pozivanja za TCP protokol. Funkcija accept blokira program sve do trenutka dok se neka od konekcija ne pojavi

 

Server:

 

create endpoint (socket())

bind address (bind())

specify queue (listen())

wait for conection (accept())

transfer data (read() write())

 

Client:

 

create endpoint (socket())

connect to server (connect())

transfer data (read() write())

 

 

 

 

 

 

UDP protokol

 

Server:

 

create endpoint (socket())

bind address (bind())

transfer data (sendto() recvfrom())

 

Client:

 

create endpoint (socket())

bind address (bind())

connect to server (connect())

transfer data (sendto() recvfrom())

 

 

 

 

 

Funckije za rad sa socketima (socket API)

#include <sys/types.h>

#include <sys/socket.h>

 

    int socket(int domain, int type, int protocol);

domain predstavlja grupu komunikacijskih usluga koju želimo koristiti. (AF_INET (IPv4) mrežni protokol, AF_INET6, AF_UNIX (cjevovod)..[1].)

type označava koji tip socketa želimo koristiti (npr. SOCK_STREAM ,  SOCK_DGRAM)

protocol označava protokol koji će se koristiti za komunikaciju. Ako za vrijednost stavimo 0, protokol će biti odabran od strane jezgre ili funkcije (što i najbolja opcija)

U slučaju greške funkcija će vratiti -1, a inače vrijednost deskriptora.

Jednom kad je socket kreiran da bi ga se moglo koristiti treba ga vezati (asocirati) na port na lokalnom stroju. (Ovo radi samo server)

    #include <sys/types.h>

    #include <sys/socket.h>

 

    int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

sockfd je socket file deskriptor  kojeg je vratio poziv funkcije socket(). my_addr je pokazivač na strukturu struct sockaddr koja sadrži informacije o adresi i  pristupnom broju (port). Portovi ispod 1024 su rezervirani. Moguće je koristiti bilo koji broj porta do 65535 ako već nije zauzet od strane nekog drugog programa. U slučaju da se za port ostavi 0 veže se na bilo koji broj koji jezgra ili funkcija odredi. addrlen je veličina prethodne strukrure i može se postaviti na sizeof(struct sockaddr).

Dok poslužitelj mora vezati socket na neki port i osluškivati, klijent se kod spojne usluge treba konektirati na neki određeni stroj na neki određeni port i to pomoću funkcije connect

 

    #include <sys/types.h>

    #include <sys/socket.h>

 

    int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd je socket file deskriptor  kojeg je vratio poziv funkcije socket(), serv_addr je struktura struct sockaddr koja sadrži odredišnu IP adresu i odredišni port, a addrlen je veličina prethodne strukrure i može se postaviti na sizeof(struct sockaddr).

Primjetimo da klijent ne zove bind() jer ga ne zanima lokalni port, već samo onaj odredišni. Da bi se klijent spojio na odredišni stroj, tamo mora netko i čekati i osluškivati, za što sluši funkcija listen

    int listen(int sockfd, int backlog);

sockfd je socket file deskriptor  kojeg je vratio poziv funkcije socket(),a backlog je broj dozvoljenih konekcija u redu čekanja. Konekcije se prihvaćaju funkcijom accept().

     #include <sys/socket.h>

 

     int accept(int sockfd, void *addr, int *addrlen);

sockfd je socket deskriptor od socketa na kojem se osluškuje. addr je obično pokazivač na strukturu struct sockaddr_in i predstavlja informaciju o klijentu(adresa, port). addrlen je cijeli broj kojeg bi trebalo postaviti na sizeof(struct sockaddr_in) prije poziva. accept neće staviti više od toliko byteova u addr. Ako stavi manje promijenit će vrijednost  varijable addrlen.

Za slanje i primanje podataka služit će funkcije send i recv kod spojne usluge, odnosno sendto i recvfrom kod bespojne.

    int send(int sockfd, const void *msg, int len, int flags);

 
    int recv(int sockfd, void *buf, int len, unsigned int flags); 
 
    int sendto(int sockfd, const void *msg, int len, unsigned int flags,
               const struct sockaddr *to, int tolen); 
 
    int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
                 struct sockaddr *from, int *fromlen);

Želimo li zatvoriti socket to možemo učiniti pomoću close, ili ako želimo malo više kontrole (npr. zatvoriti ga samo u jednom smjeru) shutdown

    close(sockfd);

    int shutdown(int sockfd, int how); 

how (0-Daljni primitak je onemogućen, 1-Daljnje slanje je onemogućeno, 2-oboje (kao close)

Od korisnih funkcija spomenimo još dvije koje služe za dobivanje informacije o klijentu, odnosno o vlastitom stroju.

    #include <sys/socket.h>

 

    int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

 

    #include <unistd.h>

 

    int gethostname(char *hostname, size_t size);

Korištenje DNS-a

Općenito je lakše pamtiti simboličko ime stroja, a ne njegovu IP adresu. Za pretvaranje simboličkog imena u IP adresu i obrnuto mogu nam poslužiti sljedeće funkcije.

Funkcija gethostbyname vraća

    #include <netdb.h>

   

    struct hostent *gethostbyname(const char *name);

    struct hostent {

        char    *h_name;

        char    **h_aliases;

        int     h_addrtype;

        int     h_length;

        char    **h_addr_list;

    };

    #define h_addr h_addr_list[0]

Struktura struct hostent ima sljedeća polja:

gethostbyname() vraća pokazivač na popunjenu struct hostent, ili NULL u slučaju greške. Kod greške se stavlja u h_errno (herror().funkcija) . Neke bitnije su: HOST_NOT_FOUND (zadano računalo ne postoji), NO_ADDRESS/NO_DATA (računalo postoji, ali nema IP adresu), NO_RECOVERY (neoporavljiva greška),TRY_AGAIN (greška privremenog karaktera).

Primjer: getip.c

Osluškivanje na više socketa

U slučaju da želimo unutar jednog programa osluškivati na više portova očito ne može (ne smijemo) ostati blokirani na accept. Iako je socket file deskriptor, pa možemo podesiti (fnctl) da nije blokirajući, bolja varijanta je upotrijebiti select (vidi prethodne vježbe)

 

Primjer: chatserver.c

Više informacija na:

Za one koje zanima, evo i mali dodatak:

************************************

Sockets in Java

Client

import java.io.*;
impoert java.net.*;
 
public class SocketTest {
 
  public static void main(String argv[]) {
    try {
      Socket t = new Socket("java.sun.com", 13);
      DataInputStream is =
        new DataInputStream(t.getInputStream());
      boolean more = true;
      while (mnore) {
        String str = is.readLine();
        if (str == null)
          more = false;
        else
          System.out.println(str);
        }
      }
    } catch(IOException e) {
      System.out.println("Error" + e);
    }
  }
}

Server

import java.io.*;
import java.net.*;
 
public class EchoServer {
  public static void main(String argv[]) {
    try {
      ServerSocket s = new ServerSocket(8189);
      Socket incoming = s.accept();
        //za adresu od klijenta vidi incoming.getInetAddress
      DataInputStream in =
        new DataInputStream(incoming.getInputStream());
      PrintStream out =
        new PrintStream(incoming.getOutputStream());
      out.println("Hello. Enter BYE to exit");
 
      boolean done = false;
      while ( ! done) {
        String str = in.readLine();
        if (str == null) 
          done = true;
        else {
          out.println("Echo: " + str);
          if (str.trim().equals("BYE"))
            done = true;
        }
      incoming.close();
    } catch(Exception e) {
      System.out.println(e);
    }
  }
}

Sockets in Perl

Perl has a C-like interface to sockets. It also has a higher level one using the IO::Socket module. This example is a client to fetch documents from a Web server.

 
#!/usr/bin/perl -w
use IO::Socket;
unless (@ARGV > 1) { die "usage: $0 host document ..." }
$host = shift(@ARGV);
$EOL = "\015\012";
$BLANK = $EOL x 2;
foreach $document ( @ARGV ) {
    $remote = IO::Socket::INET->new( Proto => "tcp",
                                     PeerAddr  => $host,
                                     PeerPort  => "http(80)",
                                    );
    unless ($remote) { die "cannot connect to http daemon on $host" }
    $remote->autoflush(1);
    print $remote "GET $document HTTP/1.0" . $BLANK;
    while ( <$remote> ) { print }
    close $remote;
}
 

Here is a server that will execute some commands and return a result

 
#!/usr/bin/perl -w
use IO::Socket;
use Net::hostent;              # for OO version of gethostbyaddr
 
$PORT = 9000;                  # pick something not in use
 
$server = IO::Socket::INET->new( Proto     => 'tcp',
                                 LocalPort => $PORT,
                                 Listen    => SOMAXCONN,
                                 Reuse     => 1);
 
die "can't setup server" unless $server;
print "[Server $0 accepting clients]\n";
 
while ($client = $server->accept()) {
    $client->autoflush(1);
    print $client "Welcome to $0; type help for command list.\n";
    $hostinfo = gethostbyaddr($client->peeraddr);
    printf "[Connect from %s]\n", $hostinfo->name || $client->peerhost;
    print $client "Command? ";
    while (<$client>) {
        next unless /\S/;       # blank line
        if (/quit|exit/i) {
            last;
        } elsif (/date|time/i) {
            printf $client "%s\n", scalar localtime;
        } elsif (/who/i ) {
            print  $client `who 2>&1`;
        } elsif (/cookie/i ) {
            print  $client `/usr/games/fortune 2>&1`;
        } elsif (/motd/i ) {
           print  $client `cat /etc/motd 2>&1`;
        } else {
           print $client "Commands: quit date who cookie motd\n";
        }
     } continue {
        print $client "Command? ";
     }
     close $client;
}

 



[1] Ponekad se koristi PF (ProtocolFamily) umjesto AF (Address Family)