Socketprogrammering

Från Unix.se, den fria unixresursen.

Nedan följer en kort introduktion till hur du kommer igång med socketprogrammering i programmeringsspråket C. Vi sitter naturligtvis i UNIX-miljö och denna introduktion är skriven därefter.

Innehåll

Vad du bör kunna

Detta är ingen introduktion till C, grundkunskaper i språket är nödvändiga.

Vad är en socket?

Du har säkert hört att allt i UNIX är en fil, det som egentligen menas med det är att all sorts I/O görs genom att läsa/skriva till fds. En socket är en fd, och det går att använda write()/read() för att skriva/läsa till den.

Typer

Det finns huvudsakligen två sockettyper som används för kommunikation över internet, SOCK_STREAM och SOCKET_DGRAM. Den förstnämnda används för TCP-kommunikation, den andra för anslutningslös UDP-kommunikation. Vilken ska man då välja? Det beror självfallet på den uppgift som programmet ska utföra, "the right tool for the right job". Du kommer dock i de flesta fall använda SOCK_STREAM-sockets. Varför? För att TCP (Transmission Control Protocol) ser till så att anslutningen verkligen existerar, kollar så att paketen är felfria och att de anländer i rätt ordning. UDP (User Datagram Protocol) däremot, erbjuder ingen som helst felkontroll eller anslutningsgaranti, därav "anslutningslösa". De flesta populära tjänster (http, smtp, irc, ftp, telnet, pop3, ssh mfl.) använder sig av TCP, därför fokuserar jag mig på TCP i denna del av introduktionen.

Network byte order

Det finns två byteorderings (i vilken ordning bytes lagras i minnet). Big-endian och Little-endian. Olika orderings används på olika maskiner. En x86-maskin lagrar talen i Little-endian (talet 0x1234 lagras som 0x34, 0x12), en sparc-maskin däremot lagrar talen i Big-endian (talet 0x1234 lagras som 0x12, 0x34). För att portnummer/ip-adresser inte ska få helt andra betydelser när en x86-maskin och en sparc-maskin pratar med varandra använder man Network byte order (Big-endian). Ett antal macros finns för att konvertera tal till Network byte order, mer om dem senare.

Första exemplet

#include <stdio.h>              /* perror() */
#include <unistd.h>             /* close() */
#include <sys/types.h>
#include <sys/socket.h>         /* socket() */
#include <netinet/in.h>         /* IPPROTO_TCP */
#include <arpa/inet.h>          /* inet_addr() */
#include <errno.h>              /* errno */

int main()
{
int sockfd;
struct sockaddr_in sin;

if( ( sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) ) == -1) {
        perror("socket");
        exit(1);
}
sin.sin_family = PF_INET;
sin.sin_port = htons(80);
sin.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(&(sin.sin_zero), '\0', 8);
if( connect(sockfd, (struct sockaddr *)&sin, sizeof(struct sockaddr) ) == -1) {
       perror("connect");
       exit(1);
}

printf("Anslutna till 127.0.0.1, port 80!\n");

close(sockfd);

return(0);
}

Såja, vårt första exempel. Kan du gissa vad det gör? Precis! Det ansluter till port 80 (http) på 127.0.0.1. Vi ska nu gå igenom koden, rad för rad.

På rad tre definierar vid en int, sockfd, som vi ska binda vår socket till. På rad fyra definierar vi en struktur av typen sockaddr_in, sockaddr_in ser ut så här:

    struct sockaddr_in {
        short int          sin_family;  /* Adressfamilj */
        unsigned short int sin_port;    /* Port (Network byte order!)*/
        struct in_addr     sin_addr;    /* ipadress */
        unsigned char      sin_zero[8]; /* Padda till samma storlek som sockaddr */
    };

Denna struktur finns till för att göra livet lättare för oss programmerare. Om du kollar mansidan för connect() (som vi ska gå igenom snart) ser du att den vill ha en struktur av typen sockaddr, den ser ut så här:

    struct sockaddr {
        unsigned short    sa_family;    /* Adressfamilj */
        char              sa_data[14];  /* ip och port */
    };

Vi kan spendera tiden bättre än att fylla sa_data för hand, därför använder vi sockaddr_in och castar om den till sockaddr. Du ser också att sockaddr_in innehåller en annan struktur, som vi ska stoppa vår ipadress i, den ser ut så här:

    struct in_addr {
        unsigned long s_addr; /* IP-adress, 32-bitar (4 bytes) (Network byte order!) */
    };

På rad 6 gör vi vår int till en socketfd av typen PF_INET, SOCK_STREAM och IPPROTO_TCP, kort sagt, en socket vi kan prata TCP över IPv4 med. På rad 7 använder vi perror() för att skriva ut det even. fel som har inträffat (perror() tar variabeln errno (som sätts av socket()), kollar felkoden och skriver ut en fin sträng som berättar vad som är fel).

På rad 10 sätter vi adressfamilj i strukturen sockaddr_in till PF_INET (måste vara samma som angavs i socket()-anropet). På rad 10 sätter vi vilken port vi ska ansluta till, vi ger sin.sin_port värdet 80 (http). Funktionen htons() (Host TO Nework byte order Short) används för att konvertera en short int till Network byte order. På rad 12 bestämmer vi det viktigaste, vilken dator på internet vi ska prata med. Vi tilldelar sin.sin_addr.s_addr ipadressen 127.0.0.1 genom att skicka strängen "127.0.0.1" till inet_addr() som konverterar strängen till en long på 4 bytes i Network byte order och returnerar resultatet. Koden på rad 13 fyller ut sin_zero, detta för att den ska få samma storlek som sockaddr.

Slutligen ansluter vi genom att anropa connect(), första argumentet är vår socketfd, andra argumentet är vår sockaddr_in-struktur castad till sockaddr, tredje kan du nog räkna ut själv :-) Vi avslutar programmet med att stänga socketfdn och returnera 0.

Du bör också läsa igenom mansidorna för samtliga funktioner / syscalls vi har använt.

Sen då?

Vårt exempel gör inte mycket dock, vi gör inget vettigt med vår öppna socket, vi vet heller inte hur man omvandlar ett hostname till en ipadress.

Det finns flera metoder för att läsa/skriva till en socketfd. Vi kan använda write()/read() eller send()/recv(). Ett litet exempel på hur man använder send()/recv() samt gethostbyname() som nämndes tidigare kommer här:

#include <stdio.h>              /* perror() */
#include <unistd.h>             /* close() */
#include <sys/types.h>
#include <sys/socket.h>         /* socket() */
#include <netinet/in.h>         /* IPPROTO_TCP */
#include <arpa/inet.h>          /* inet_addr() */
#include <errno.h>              /* errno */
#include <netdb.h>              /* gethostbyname() */

int main()
{
       int sockfd;
       struct sockaddr_in sin;
       struct hostent *he;
       char *hosten = "kuba.unix.se";
       char *req = "GET / HTTP/1.1\nHost: kuba.unix.se\n\n";
       char recvbuf[2000];

       if( (he = gethostbyname(hosten)) == NULL ) {
               fprintf(stderr, "Kunde inte sl\xe5 upp %s!", hosten);
               exit(1);
       }

       if( ( sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) ) == -1) {
               perror("socket");
               exit(1);
       }

       sin.sin_family = PF_INET;
       sin.sin_port = htons(80);
       sin.sin_addr = *((struct in_addr *)he->h_addr);
       memset(&(sin.sin_zero), '\0', 8);

       if( connect(sockfd, (struct sockaddr *)&sin, sizeof(struct sockaddr) ) == -1) {
               perror("connect");
               exit(1);
       }

       if( send(sockfd, req, strlen(req), 0) == -1) {
               perror("send");
               exit(1);
       }

       if( recv(sockfd, recvbuf, 2000, 0) == -1) {
               perror("recv");
               exit(1);
       }

       printf("Servern svarade med:\n%s\n", recvbuf);

       close(sockfd);

       return(0);
}

Det här exemplet gör betydligt mer än det förra. Vi ansluter till port 80 (http) på kuba.unix.se, skickar en request till servern, läser in svaret och skriver ut det på skärmen. Häng med nu, rad för rad.

På rad fem definierar vi en struktur av typen hostent, hostent ser ut så här:

    struct hostent {
        char    *h_name;                /* Namnet på hosten */
        char    **h_aliases;            /* En NULL-terminerad array av alternativa namn på hosten */
        int     h_addrtype;             /* Adresstyp, vanligtvis PF_INET */
        int     h_length;               /* Adressens längd */
        char    **h_addr_list;          /* En array av ipadresser för hosten från dnsn */
    };
    #define h_addr h_addr_list[0]       /* Första ipadressen från h_addr_list */

Det enda du behöver hålla i minnet är att h_addr är den första ipadressen som hosten pekar på.

På rad 10 anropar vi gethostbyname() som returnerar en pekare till en hostent-struktur med alla värden satta. Observera att du inte kan använda perror() här eftersom gethostbyname() inte sätter errno, använd istället herror(). På rad 22 sätter vi sin_addr till resultatet som returnerades av gethostbyname(), eftersom h_addr_list är en char castar vi om den till in_addr. På rad 30 skickar vi iväg vår request till webbservern, och på rad 35 läser vi in resultatet. send()/recv() returnerar antalet bytes som verkligen skickades iväg / lästes in eller -1 om något fel inträffade. Det finns dock ingen garanti att send()/recv() verkligen skickar / läser in det antal bytes du angav när du anropade funktionen. Håll det i minnet när du skriver dina egna program.

Externa länkar

Personliga verktyg