Sockets NL NL

 Network IPC

[ HOME ] Dit stukje probeert duidelijk te maken hoe je programma's over het netwerk kunnen communiceren en tijdens het verhaal kijken we ook meteen naar file descriptors en wat een API (Application Programmer's Interface) is.
Introductie in sockets
Voor communicatie over het netwerk heeft bijna elk OS een API genaamd de sockets API. Een API is een ander woord voor een library, een bibliotheek van functies. Het diagrammetje hier beneden probeert de relatie duidelijk te maken tussen het proces, de socket en het netwerk.

images/socket1.gif: The socket provides a place for a definition with a namespace and manner of communicating, which in its turn connects to the protocol in question.
Principes van de Socket API
We hopen dat de illustratie het duidelijk maakt: de applicatie praat tegen de socket omdat hij een zogenaamde file descriptor heeft, net als de gemiddelde file. De programmeur hoeft niks te weten van protocollen, hij hoeft maar twee dingen op te geven: het type socket en de protocol familie. Die twee kenmerken gaan we hieronder behandelen. Dat is heel mooi. Waarom? Omdat we bestanden ook aanspreken met een file descriptor. Dus dat is mooi consistent. Een file descriptor is gewoon een integer. Voor elke file die je opent, krijgt je proces een file descriptor. Tik maar eens man open in, dan zie je dat de return value een file descriptor is.
Socket types
Er zijn drie typen sockets: stream sockets, datagram sockets en raw sockets. De laatste laten we liggen, want dan moet je echt in het protocol duiken en dat leggen we hier niet uit.

Stream sockets, aangeduid met de constante SOCK_STREAM, kan een stroom van informatie transporteren. Er wordt een verbinding opgezet en die wordt pas weer gesloten totdat jij het zegt, net als een telefoongesprek. Er is twee richtings verkeer. Voorbeelden van gebruik: je browser Netschaap of Internet Exploiter, je FTP programma, je chat-proggie. Het is een mooie manier om data over te zenden. Je weet namelijk altijd zeker, dat de pakket-inhoud, -aankomst en -volgorde goed zijn.

Datagram sockets, SOCK_DGRAM, deze zijn telegrammetjes met data erin. Het is dus niet een kwestie van een verbinding aanleggen en dergelijke, het is meer je bericht aan de postduif geven en hopen dat het aankomt. Dus je weet niet zeker of hij aankomt. En als je er meer verstuurt, weet je niet zeker of ze in dezelfde volgorde aankomen als dat je ze verstuurde. De inhoud is trouwens wel gegarandeerd correct. Toepassingen: je ICQ, en allerlei wat meer low-level protocolletjes werken ermee (BOOTP, router berichten).
Protocol Family
De protocol familie (ook wel genoemd 'namespace' of 'domain') is de complete set protocollen die worden gebruikt voor communicatie over het netwerk. Protocol families zijn bijvoorbeeld TCP/IP, IPX/SPX, X.25 en nog veel meer. Elke protocolfamilie heeft een constante (wat niet wil zeggen dat het geimplementeerd is). Wij kijken alleen naar de TCP/IP protocol familie, oftewel de internet namespace.
Overzicht van het opzetten van een verbinding
De procedure voor een server is als volgt:

   1. Maak een socket met socket()<<, deze geeft een file descriptor terug. socket is eigenlijk hetzelfde als het installeren van een telefoon-aansluiting.
   2. Definieer welke poort je wilt gebruiken door middel van de bind() functie. Dit kan gezien worden als het vragen aan KPN of men een nummer toewijst aan je telefoontoestel.
   3. Nu roep je listen() aan, net alsof je je telefoontoestel werkelijk aansluit in de wallport, met de hoorn op de haak - klaar om over te gaan.
   4. Tenslotte roep je accept() aan, start de telefoon-beantwoorder die de hoorn opneemt wanneer iemand belt. 

Een client hoeft niet listen() en accept() aan te roepen, in plaats daarvan roept hij (of zij) connect() aan naar een andere socket.

Wanneer een verbinding is gelegd, kun je data verzenden met send() en recv(). Inderdaad, met de read() en write() calls gaat het ook, want het is een filedescriptor. Maar de twee eerstgenoemde functies zijn wat handiger want gespecialiseerder.

Nu we het idee hebben, kunnen we deze functies eventjes goed gaan bekijken. Maar.... vaak zijn de functies niet het probleem. Wel de manier waarop je ze gebruikt. Systeemprogrammeren onder C is vaak object-georienteerd, maar dan zonder mooie principes van C++ en Java. Programmeren onder C komt vaak neer op de data klaarzetten in een of meer structs en die dan doorgeven als parameters aan je functies. En die structures zijn vaak het probleem; die zijn lastig.
De structs
Het volgende vertelt je hoe je bepaalde structs op moet zetten. Deze zijn speciaal ontworpen om doorgegeven te worden als parameters aan de bovengenoemde functies. En hier zijn ze, speciaal voor jou, achter deur nummer 1.
De structuur sockaddr om adressen door te geven
De volgende struct hoef je niet te kennen. Het is namelijk een algemene struct die allerlei adressen kan onthouden. Maar we zouden toch alleen naar TCP/IP kijken? Inderdaad. Maar een algemene functie als bind() geld voor alle protocol families, net alleen voor TCP/IP. Dus doen ze het zo: bind() accepteert een parameter struct sockaddr, wij vullen een speciale (voor TCP/IP) vorm van die struct met informatie, casten hem naar een gewone struct sockaddr, en geven 'em aan de functie. Dus nu hebben we maar 1 functie om te binden nodig, terwijl er verschillende protocollen zijn. Snap je? In het kort: speciale struct vullen, typecasten naar gewone struct, doorgeven aan functie. Mooi he?

struct sockaddr
{

      unsigned short    sa_family;   /* address format, AF_xxx       */
      char              sa_data[14]; /* 14 bytes of protocol address */};

Ik leg hier niets over uit. Je gebruikt hem toch eigenlijk niet.
Onthoud internet adresjes met struct sockaddr_in

struct sockaddr_in
{

      sa_family_t     sin_family;         /* address format: AF_INET */
      u_int16_t       sin_port;        /* port in network byte order */
      struct in_addr  sin_addr;                  /* internet address */};

Er zijn twee dingen belangrijk om te onthouden hier.

De eerste is dat sommige systemen hun bytes in een andere manier bewaren dan sommige andere systemen. Wat je systeem ook is, de bytes in sin_port en sin_addr moeten bewaard worden in Network Byte Order. Als je dit maar onthoud, is het no problemo, want er zijn een paar libraryfuncties om je te helpen! Kijk naar de man pages voor htons(), htonl(), ntohs() en ntohl().

Ten tweede: de struct sin_addr is van type struct in_addr. Als je een structure van type sockaddr_in hebt, die je bijvoorbeeld (volledige random naam) client noemt, dan is het adres van je client "client.sin_addr.s_addr". Maar je moet hem niet direct vullen! Gebruik een functie van de groep inet_*() functies, de beste is hier inet_aton().

.
.
.

#define N_PORT 7

int main(int argc, char[] argv)
{

      int n_sin_size;                    /* the size of the client's address */
      struct sockaddr_in st_server_addr;      /* to store the server address */
      char sz_address[] = sz_argv[1];       /* this client takes an internet
                                                      address as an argument */
      .
      .
      .
      /* first commit the address information to a struct sockaddr_in; 
       * we have defined server_addr for that purpose. */ 
      st_server_addr.sin_family = AF_INET;
      st_server_addr.sin_port = htons(PORT);
      inet_aton(sz_address, &st_server_addr.sin_addr);   
      .
      .
      .
      /* An interesting function
       * is inet_aton(), it converts a string with an IP address in
       * standard numbers-and-dots notation to information that your
       * TCP/IP stack can use. */
      .
      .
      .
      /* Now we are ready to create a socket, communicate and close the
       * whole thing. */
      return 0;}

The image below provides an overview.

images/socket2.gif: The struct sockaddr is never used, instead struct sockaddr_in is typecasted. Note that this last struct has a member struct in_addr.

De functies
Roep socket() om een file descriptor te krijgen
De niet echt moeilijke functiecall socket():

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

De protocol_family is uiteraard PF_INET. Het type moet zoals ik uitgelegd heb, of INET_STREAM of INET_DGRAM zijn. De combinatie van protocol_family en socket type vormen de manier van communiceren. Elke combinatie heeft een standaard protocol in huis, dat is nummer 0 (de derde parameter). Als er meerdere alternatieven zijn voor een bepaald protocol, mag je hier kiezen. Bij TCP/IP is dat niet het geval.

socket() geeft gewoon een file descriptor terug, of -1 wanneer een error optreed. De variabele errno krijgt dan een bepaalde waarde. Ik zeg het een keer en dan nooit meer: controleer op errors.
Ik wil me nog niet bind()en
Wanneer je procesje een server functie vervult, moet je hem wel duidelijk maken dat die socket connecties wil op deze poort en dat adres. Er zijn zat dozen met meerdere adressen, dus vandaar 'dat adres'. Wanneer je proces een client functie vervult (dus het maakt je niet uit welke poort hij bezet, want niemand hoeft hem te vinden), hoef je niet te binden. Het gebeurt trouwens wel, maar dan automatisch. Vaak op een of andere hoge poort, met een net en engels woord: ephemeral port.

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

De eerste parameter pass je de filedescriptor die van socket() terugkwam. De tweede parameter is je getypecaste struct sockaddr_in en de derde parameter kan gevuld worden met sizeof( struct sockaddr ).
connect() naar een internetdoos
Een server connect natuurlijk nergens specifiek heen. Een client wel. (DUH!). Een client doel in het leven is connecten naar een server. En net als je een telefoonnummer nodighebt om iemand te bellen, doen wij hier aan IP adressen.

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

En tot vermoeiend toe is de eerste parameter de socket zijn file descriptor. De tweede en derde parameter zijn exact hetzelfde als met bind().
Steek je telefoontoestel in de muur met listen()

int listen (int sockfd, int backlog);

Op deze manier maak je je zorgvuldig opgezette socket klaar voor zenden en ontvangen. backlog is het maximale aantal verbindingen dat op het moment probeert te verbinden met je socket. Of het kan het aantal connecties zijn dat klaar is om geaccepteerd te worden. Kijk in je man page, baby doll.
Voorbeeldjes
Datagram client/server
De programma's die beginnen met dgram gebruiken datagrammetjes om te communiceren. Datagram sockets garanderen niet dat ze aankomen en dat is makkelijk te demonstreren. Want de client starten zonder dat de server draait geeft geen errors! Probeer de eerste als eerste, want de commentaren zijn een soort samenvatting van al het bovenstaande.

    * A client using SOCK_DGRAM
    * A server using SOCK_DGRAM
    * The .tar.gz file containing the source and the Makefile 

Stream sockets client/server
De volgende voorbeeldjes gebruiken stromende sockets. Het is een wat lastiger voorbeeldje, want multithreaded .

    * A client using SOCK_STREAM
    * A server using SOCK_STREAM
    * The .tar.gz file containing the source and the Makefile 

The Server Class
Tegenwoordig maakt iedereen zijn eigen class en wij durven gewoon niet achter te blijven. Zoveel mogelijk wordt afgevangen, inclusief multithreading problematiek.

De server kant bestaat uit twee klassen: Server en SimpleServer. De Server class hoeft in principe niet aangeraakt te worden. De SimpleServer class is afgeleid van de Server class; deze heeft een run() methode (functie) die gedefinieerd moet worden. Want deze bepaalt eigenlijk wat de server nu doet. Hier gebruik je de RecvString() om client commando's te ontvangen, een wilde horde functies op los te laten en dan terug te sturen met SendString. Een programma testServer wordt ook gegeven.

De client is nu niet spannend meer. Het lijkt op een versimplificeerde versie van de Server class. Het is niet nodig om deze class in een SimpleClass te herleiden. Maar doe vooral wat je niet laten kan.

    * The .tar.gz file of the Server, SimpleServer and Client classes 

Resources

    * The Linux Programmer's Guide
    * Unix-socket-FAQ Sockets tot op het naadje.
    * Beej's guide to network programming Helemaal goud, deze tutorial.
    * "Unix Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI", by W. Richard Stevens (Prentice Hall, 1998). Jajajaja, de meestercoder.