Original article: http://beej.us/guide/bgnet/

Brug af Internet Sockets

Brian "Beej" Hall

Revision History
Revision 1.0.0August, 1995Revised by: beej
Første version.
Revision 1.5.513. Januar, 1999Revised by: beej
Seneste HTML version.
Revision 2.0.06. Marts, 2001Revised by: beej
Konverteret til DocBook XML, rettelser, tilføjelser.
Revision 2.3.18. Oktober, 2001Revised by: beej
Ordnede tastefejl, syntaksfejl i client.c, tilføjelser til SOS.

1. Introduktion

Hej! er du træt af socket programmering? er det en smule for svært at finde ud af ud fra man pages? Vil du lave sejt internetprogrammering, men har ikke tid til at vade gennem bunker af structs for at finde ud af om du skal kalde bind() før connect(), osv., osv.

Gæt engang! Jeg har allerede gjort det trælse arbejde, og jeg kan ikke vente med at dele informationen med alle! Du er kommet til det rigtige sted. Dette dokument skulle give en gennemsnits C programmør mulighed for at få et fodfæste i dette netværkshalløj.


1.1. MÃ¥lgruppe

Dette dokument er blevet skrevet som en lærebog, ikke en opslagsbog. Det virker nok bedst hvis den bliver læst af folk som netop er begyndt med socket programmering, og leder efter et ståsted. Det er under ingen omstændigheder en komplet guide til socket programmering.

Forhåbentlig er det nok til at få de manpages til at give mening... :-)


1.2. Platform og Compiler

Koden i dette dokument er kompileret på en Linux PC vha. GNU's gcc compiler. Det skulle være muligt at kompilere den på enhver platform der bruger gcc. Naturligvis går dette ikke hvis du programmerer til Windows - se sektionen om Windows programmering herunder.


1.3. Officiel hjemmeside

Det officielle sted for dette dokument er ved California State University, Chico, ved http://www.ecst.csuchico.edu/~beej/guide/net/.


1.4. Note til Solaris/SunOS programmører

Ved kompilation til Solaris eller SunOS, skal der tilføjes nogle ekstra kommandolinieparametre, for at linke til de korrekte libraries. For at gøre dette, skal der tilføjes "-lnsl -lsocket -lresolv" i slutningen af kompileringskommandoen, sådan:


    $ cc -o server server.c -lnsl -lsocket -lresolv

Hvis du stadig får fejl kan du prøve at tilføje "-lxnet" til slutningen af denne kommando. Jeg ved ikke helt hvad den gør, men nogle folk synes at have brug for den.

Et andet sted du måske vil have problemer er i kaldet til setsockopt(). Prototypen er forskellig fra den på min linux boks, så i stedet for:


	int yes=1;

skal dette indtastes:


	char yes='1';

Da jeg ikke har en Sun boks, har jeg ikke testet noget af det overstående -- det er bare hvad folk har sagt gennem email.


1.5. Note til Windows Programmører

Jeg har en særlig modvilje mod Windows, og jeg opfordrer dig til at prøve Linux, BSD eller Unix i stedet. Med det på plads, er der stadig mulighed for at bruge det her i Windows.

Først skal du ignorere det meste der står om systemheaderfiler. Alt der skal inkluderes er:


    #include <winsock.h> 

Vent! Du skal også have et kald til WSAStartup() før du gør noget som helst med Windows socket library. Koden til det ligner nogenlunde det her:


    #include <winsock.h>

    {
        WSADATA wsaData;   // Hvis det her ikke virker
        //WSAData wsaData; // Saa proev det her

        if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
            fprintf(stderr, "WSAStartup failed.\n");
            exit(1);
        } 

Du skal også fortælle din compiler at den skal linke Winsock biblioteket med, som typisk kaldes wsock32.lib eller winsock32.lib eller noget i den stil. I VC++ kan dette gøres vha. Project menuen, under Settings.... Klik på Link fanen, og kig efter boksen ved navn "Object/library modules". Tilføj "wsock32.lib" til denne liste.

Eller, det har jeg i hvert fald hørt.

Til sidst skal du kalde WSACleanup() når du er færdig med socket biblioteket. Se din online hjælp for detaljer.

Når du har gjort det, skulle resten af eksemplerne i denne lærebog virke, med nogle få undtagelser. For det første kan du ikke bruge close() til at lukke en socket -- du bliver nød til at bruge closesocket() i stedet. Derudover virker select() kun med socket descriptors, ikke file descriptors (som 0 for stdin).

Der er også en socket klasse du kan bruge, CSocket. Check din compilers hjælp for mere information.

For at få mere information om Winsock, læs Winsock SOS og gå videre derfra.

Til sidst har jeg hørt at Windows ikke har noget fork() systemkald som, desværre, er brugt i nogle af mine eksempler. Måske bliver du nød til at linke et POSIX bibliotek med eller noget for at få det til at virke, ellers kan du bruge CreateProcess() i stedet. fork() tager ingen argumenter, og CreateProcess() tager omkring 48 milliarder argumenter. Hvis du ikke vil det, er CreateThread() noget lettere at fordøje...desværre er en gennemgang af multithreading udenfor dette dokuments rammer. Jeg kan kun fortælle om så og så meget!


1.6. Email Politik

Jeg er typisk klar til at hjælpe med emailede spørgsmål, så bare send nogle til mig. Jeg kan dog ikke garantere et svar. Jeg har et temmeligt travlt liv, og der er tider hvor jeg simpelthen ikke kan besvare et spørgsmål du sender. Når dette er tilfældet, sletter jeg typisk beskeden. Det er ikke noget personligt, jeg vil bare aldrig få tid til at give det detaljerede svar du søger.

Som regel er det sådan, at jo sværere spørgsmålet er, jo mindre sandsynlighed er der for at jeg svarer. Hvis du kan koge dit spørgsmål ned før du sender det, og hvis du sender al relevant information (platform, compiler, fejlbeskeder og alle andre ting du synes er vigtige), er der større chance for at du får et svar. For flere pointers kan du læse ESRs dokument, How To Ask Questions The Smart Way.

Hvis du ikke får et svar, så hack lidt mere på det og prøv at find svaret. Hvis det stadig er uklart, så skriv til mig igen med det du har fundet ud af, og forhåbentlig vil der være nok for mig så jeg kan hjælpe.

Nu, når jeg har plaget dig med hvordan du skal og ikke skal skrive til mig vil jeg lige understrege at jeg fuldt ud er taknemmelig for al den ros denne guide har fået årene igennem. Det er et godt molalboost, og det glæder mig at høre at den er blevet brugt! :-) Tak!


1.7. Opsætning af Mirror

Du er mere end velkommen til at lave et mirror af denne side, om det er offentligt eller privat. Hvis du laver et offentligt mirror af siden, og vil have mig til at linke fra den fra hovedsiden, så send en mail til .


1.8. Note til Oversættere

Hvis du vil oversætte denne guide til et andet sprog, så skriv til mig på . Så vil jeg linket til din oversættelse fra hovedsiden.

Føl dig fri til at tilføje dit navn og email adresse til oversættelsen.

Desværre kan jeg ikke hoste oversættelsen selv, da jeg ikke har plads til det.


1.9. Copyright og Distribution

Beej's Guide to Network Programming er Copyright © 1995-2001 Brian "Beej" Hall.

Denne guide må frit kopieres på ethvert medie, så længe indholdet ikke er ændret, det er vist i sin helhed, og dette Copyright ikke fjernes.

Undervisere er specielt opfordret til at anbefale eller give kopier af denne guide til deres studerende.

Denne guide må frit oversættes til ethvert sprog, givet at oversættelsen er eksakt, og at guiden er genoptrykt i sin helhed. Oversættelsen må også indeholde navn og kontaktinformationer til oversætteren.

C kildekoden heri kan hermed frit benyttes af offentligheden.

Kontakt for mere information.


1.10. Oversættelsesnote

Oversættelsen af "Beej's Guide to Network Programming" er udført af Nikolaj Fogh. Denne note er mine tilføjelser til dokumentet.

Jeg har valgt ikke at oversætte gængse programmeringsmæssige termer så som integer, file descriptor og socket, da jeg synes dansk edb sprog (eller skulle jeg sige datamat sprog) er grimt og forstyrrende. Nogle gange vil jeg dog bruge danske termer (bibliotek i stedet for library) hvor det giver mening, og ikke er "grimt".

Nogle steder hvor det giver mest mening at skrive termer på dansk, men hvor de engelske termer er brugt inden for faglitteratur, er den danske oversættelse bragt først efterfulgt at den originale engelske term i paranteser.

Har du nogen kommentarer til oversættelsen kan du kontakte mig på


2. Hvad er en socket?

Du hører snak om "sockets" hele tiden, og måske undrer du dig over hvad det lige præcis er de er. Nåmen, de er det her: en måde at snakke til andre programmer vha. standard Unix file descriptors.

Hva?

Ok -- du har måske hørt en Unix hacker sige "Jøsses, alt i Unix er en fil!". Hvad personen måske snakker om er at når Unix programmer laver nogen for for I/O, gør de det ved at læse eller skrive til en file descriptor. En file descriptor er simplthen en integer associeret med en åben fil. Men (og her er det vigtige), den fil kan være en netværksforbindelse, en FIFO, en pipe, en terminal, en rigtig fil på en disk, eller stort set alt mulig andet. Alt i Unix er en fil! Så når du vil kommunikere med et andet program over Internettet, gør du det gennem en file descriptor. Du kan lige så godt indse det.

"Hvor får jeg denne her file descriptor til netværkskommunikation, Karl Smart?" er nok det sidste spørgsmål du har lige nu, men jeg fortæller det alligevel: Du laver et kald til system routinen socket(). Den returnerer en socket descriptor, og du kommunikerer gennem den vha. de specialiserede socket kald send() og recv() (man send, man recv)

"Men, hey!" siger du nok nu. "Hvis det er en file descriptor, hvorfor i himlens navn kan jeg dog ikke bruge normale read() og write() kald til at kommunikere gennem en socket?". Det korte svar er "Det kan du også!". Det lange svar er "Det kan du også, men send() og recv giver meget større kontrol over datatransmissionen."

Hvad så? Hvad med dette: der er alle mulige slags sockets. Der er DARPA Internet adresser (Internet Sockets), path names på en lokal maskine (Unix Sockets), CCITT X.25 adresser (X.25 Sockets som du bare kan ignorere), og sikkert mange andre, alt efter hvilken slags Unix du bruger. Dette dokument tager sig kun af den første: Internet Sockets.


2.1. To Slags Internet Sockets

Hvad nu? Er der to slags Internet sockets? Ja. Eller, nej. Jeg lyver. Der er flere, men jeg ville ikke skræmme dig. Jeg vil kun snakke om to slags her. Lige undtagen den her sætning, hvor jeg vil fortælle dig at "Raw Sockets" også er meget kraftfulde, og at du skulle kigge på dem.

Okay, hvad er de to slags? En er "Stream Sockets"; den anden er "Datagram Sockets", som vi herfra refererer til som hhv. "SOCK_STREAM" og "SOCK_DGRAM". Datagram sockets er nogle gange kaldet "forbindelsesløse sockets" (selvom de kan connect()es hvis du virkelig vil. Se connect(), herunder.)

Stream sockets er pålidelige tovejs forbundne kommunikations streams. Hvis du skriver to ting til en socket "1, 2", vil de ankomme i samme orden "1, 2" i den modsatte ende. De vil også være fri for fejl. Enhver fejl du måske vil støde på er ting du finder på i dit eget syge sind, og vil ikke blive disskuteret her.

Hvad bruger stream sockets? Du har måske hørt om telnet? Den bruger stream sockets. Alle bogstaverne du taster skal ankomme i samme orden som du taster dem, ikke? Også webbrowsere bruger HTTP protokollen, som bruger stream sockets til at hente sider. Sandelig, hvis du telnetter til en webside på port 80, og skriver "GET /" vil den skrive HTML tilbage til dig!

Hvordan får stream sockets denne høje datatransmissionskvalitet? De bruger en protokol kaldet "Transmission Control Protocol", også kendt som "TCP" (se RFC-793 for ekstremt detalieret information omkring TCP.) TCP sikrer at dine data ankommer sekventielt og fejlfrit. Du har nok hørt om "TCP" før som den bedre halvdel af "TCP/IP", hvor IP står for "Internet Protocol" (se RFC-791.) IP tager sig primært af Internet routing og er generelt ikke ansvarlig for dataintegritet.

Cool. Hvad med Datagram sockets? Hvorfor bliver de kaldt forbindelsesløse? Hvad er meningen med det? Hvorfor er de upålidelige? Her er nogle facts: hvis du sender et datagram, så ankommer det måske. Det ankommer måske uden orden. Hvis det ankommer, er dataene deri fejlfri.

Datagram sockets bruger også IP til routing, men de bruger ikke TCP; de bruger "User Datagram Protocol", eller "UDP" (see RFC-768.)

Hvorfor er de forbindelsesløse? Dybest set er det fordi du ikke behøver at opretholde en åben forbindelse som du gør med stream sockets. Du laver bare pakken, putter en IP header på den med destinationsinformation, og sender den ud. Ingen forbindelse er påkrævet. De er typisk brugt til pakke-på-pakke overførsler. Eksempler på programmer: tftp, bootp, etc.

"Nok!" skriger nu måske. "Hvordan virker de programmer hvis datagrammer måske går tabt?!" De gør det på den måde, at ethvert program har dets egen protokol ovenpå UDP. F.eks. tftp protokollen siger at for hver pakke der bliver sent, skal modtageren sende en pakke tilbage som bekræfter "Jeg fik den!" (en "ACK" pakke.) Hvis afsenderen af den første pakke ikke får et svar indenfor måske 5 sekunder, vil han gentransmittere pakken indtil han endelig får en ACK. Denne bekræftelsesprocedure er meget vigtig når man implementerer SOCK_DGRAM programmer.


2.2. Lavniveaus Nonsens og Netværks Teori

Siden jeg lige nævnede lagdeling af protokoller er det tid til at snakke om hvordan netværk virkelig virker, og til at vise nogle eksempler på hvordan SOCK_DGRAM pakker laves. Du kan som sådan springe over denne sektion, selvom det er god baggrundsviden.

Figure 1. Data Encapsulation.

Hej børn. Det er tid til at lære om Data Indkapsling (Data Encapsulation)! Dette er meget meget vigtigt. Det er så vigtigt at du måske endda vil lære om det hvis du tager et netværkskursus her ved Chico State ;-). Grundlæggende siger det følgende: en pakke fødes, pakker wrappes ("indkapsles") i en header (og undtagelsesvis en footer) af den første protokol (f.eks. TFTP protokollen), så bliver det hele (inklusive TFTP headeren) indkapslet igen af den næste protokol (f.eks. UDP), så igen af den næste (IP) og til sidst af den sidste protokol i hardware (den fysiske) niveau (f.eks. Ethernet).

NÃ¥r en anden computer modtager pakken, fjerner hardwaren Ethernet headeren, kernen fjernen IP og UDP headerne, TFTP programmet fjerner TFTP headeren, og har til sidst dataene.

Nu kan jeg omsider snakke om den berygtede Niveaudelte Netværksmodel (Layered Netvork Model). Denne netværksmodel beskriver et netværks funktionalitetssystem som har mange fordele over andre modeller. F.eks. kan du skrive socket programmer som er de samme uden at tænke på hvordan dataene fysisk er overført (serielt, tynd Ethernet, AUI, etc.) fordi programmer på lavere niveauer håndtere det for dig. Den faktiske netværkshardware og topologi er transparent til socket programmøren.

Uden nogen som helst pause, vil jeg vise lagene af den fulde model. Husk på dette til eksamen i netværkskunnen:

  • Applikation

  • Præsentation

  • Session

  • Transport

  • Netværk

  • Data Link

  • Fysisk

Det fysiske lag er hardwaren (seriel, Ethernet, etc.). Applikationslaget er så langt fra det fysiske lag som du kan forestille dig--det er stedet hvor brugere interagerer med netværket.

Denne model er så generel at du sikkert kunne bruge den som en reparationsguide til biler hvis du virkelig ville. En lagdelt model, som er mere konsistent med Unix kunne være:

  • Applikationslag (telnet, ftp, etc.)

  • Vært-til-Vært (Host-to-Host) Transportlag (TCP, UDP)

  • Internetlag (IP og routing)

  • Netværkslag (Ethernet, ATM, eller noget andet)

PÃ¥ det her tidspunkt kan du sikkert se hvordan disse lag kan sammenlignes med indkapslingen af de originale data.

Her kan du se hvor meget arbejde der egentlig er i at lave en simpel pakke. Jøsses! Og du bliver nød til at skrive pakkeheaderene selv vha. "cat"! Arh, det er gas. Alt du skal gøre for stream sockets er at send()e dataene ud. Alt du skal gøre med datagram sockets er at indkapsle pakken på den måde du vil og sendto()e den ud. Kernen bygger Transportlaget og Internetlaget på for dig, og hardwaren laver Netværkslaget. Ah, moderne teknologi.

Her ender vores korte ekskurs ind i netværksteorien. Ah, ja, jeg glemte at fortælle alt jeg ville omkring routing: ingenting! Det er rigtigt, jeg vil ikke fortælle om det overhovedet. Routeren fjerner pakken til IP headeren, konsulterer dens routingtabel, bla. bla. bla. Kig på IP RFCen hvis du virkelig er interesseret. Hvis du aldrig lærer om det, så overlever du nok.


3. structs og Data HÃ¥ndtering

Nå, nu er vi omsider her. Det er tid til at snakke om programmering. I denne sektion vil jeg snakke om forskellige datatyper som bliver brugt af socket interfacet, siden nogle af dem er svære at gennemskue.

Først den lette: en socket descriptor. En socket descriptor er af følgende type:


    int 

Bare en normal int.

Ting bliver lidt mystiske herfra, så du kan bare læse igennem og bære over med mig. Vid dette: der er to byte ordener: mest betydende byte (nogle gange kaldet en "octet") først, eller mindst betydende byte først. Den første kaldes "Network Byte Order". Nogle maskiner gemmer deres tal internt i Network Byte Order, nogle gør ikke. Når jeg siger noget skal være i Network Byte Order, bliver du nød til at kalde en funktion (såsom htons()) for at ændre det fra "Host Byte Order". Hvis jeg ikke siger "Network Byte Order", så skal du have værdien som Host Byte Order.

(For de nysgerrige, "Netværks Byte Order" er også kendt som "Big-Endian Byte Order".)

Min Første StructTM--struct sockaddr. Denne struct indeholder socket adresse information for mange typer sockets:


    struct sockaddr {
        unsigned short    sa_family;    // adressefamilie, AF_xxx
        char              sa_data[14];  // 14 bytes protokol adresse
    }; 

sa_family kan være et væld af ting, men det vil være AF_INET for alt i dette dokument. sa_data indeholder en destinationsadresse og portnummer for den socket. Dette er temmelig underligt, siden du ikke altid vil pakke adressen i sa_data med håndkraft.

Til at håndtere struct sockaddr skabte programmører en lignende struct: struct sockaddr_in ("in" som i "Internet".)


    struct sockaddr_in {
        short int          sin_family;  // Adressefamilie
        unsigned short int sin_port;    // Port
        struct in_addr     sin_addr;    // Internet adresse
        unsigned char      sin_zero[8]; // Samme stoerrelse som struct sockaddr
    }; 

Denne struct gør det let at referere til elementer i socket adressen. Bemærk at sin_zero (som er inkluderet til at fylde structen til længden af en struct sockaddr) bør sættes til nuller med funktionen memset(). Derudover, og dette er det vigtige skridt, en pointer til en struct sockaddr_in kan blive castet til en pointer til en struct sockaddr og omvendt. Så selvom socket() vil have en struct sockaddr* kan du stadig bruge en struct sockaddr_in og caste den i sidste øjeblik! Bemærk også, at sin_family svarer til sa_family i en struct sockaddr og skal sættes til "AF_INET". Til sidst skal sin_port og sin_addr være i Network Byte Order!

"Men,", klager du, "hvordan kan hele structen, struct in_addr sin_addr, være i Network Byte Order?" Dette spørgsmål kræver en grundig gennemgang af struct in_addr, en af de værste unions i live:


    // Internetadresse (en struct af historiske aarsager)
    struct in_addr {
        unsigned long s_addr; // det er en 32-bit long, eller 4 bytes
    }; 

Den plejede at være en union, men nu er de dage ovre. Så hvis du har deklareret ina som typen struct sockaddr_in, så refererer ina.sin_addr.s_addr til den 4-byte IP adresse (i Netwærk Byte Order). Bemærk at selvom dit system stadig bruger den uduelige union til struct in_addr kan du stadig referere den 4-byte IP adresse på samme måde som jeg gjorde herover (det er pga #defines.)


3.1. Konverter de Indfødte (Natives)!

Vi er nu bleved ledt direkte ind i den næste sektion. Der har været for meget snak om denne Network til Host Byte Order konvertering -- nu er det tid til at gøre noget ved det!

Okay. Der er to slags du kan konvertere: short (to bytes) og long (fire bytes). Disse funktioner virker også til unsigned variationer. Hvis du f.eks. vil konvertere en short fra Host Byte Order til Network Byte Order. Start med "h" som i "host", efterfulgt af "to", så "n" som i "network" og "s" som i "short": h-to-n-s, eller htons() (læs: "Host to Network Short").

Det er næsten for let...

Du kan bruge enhver kombination af "n", "h", "s" og "l", undtagen de virkelig dumme. F.eks. er der IKKE en stolh() ("Short to Long Host") function -- ikke lige her i hvert fald. Men her er de:

  • htons() -- "Host to Network Short"

  • htonl() -- "Host to Network Long"

  • ntohs() -- "Network to Host Short"

  • ntohl() -- "Network to Host Long"

Nu tror du du er blevet klogere. Du tænker nok, "Hvad gør jeg hvis jeg vil ændre byte orden på en char?" Og så tænker du nok "Ah, ingenting alligevel." Du tænker nok også at, siden din 68000 maskine allerede bruger network byte order, så behøver du ikke at kalde htonl() på din IP adresser. Du ville have ret, MEN hvis du prøver at porte til en maskine der har omvendt network byte order, så vil dit program fejle. Husk at være porterbar! Dette er en Unix verden! (Hvor meget Bill Gates end gerne vil tro anderledes.) Husk: put dine bytes i Network Byte Order før du putter dem på netværket.

En sidste pointe: hvorfor bliver sin_addr og sin_port nød til at være Network Byte Order i en scruct sockaddr_in, men ikke sin_family? Svaret er: sin_addr og sin_port bliver indkapslet i pakken ved hhv. IP og UDP lagene. Derfor skal de være i Network Byte Order. sin_family feltet er kun brugt af kernen til at fastslå hvilken type adresse structen indeholder, så det skal være i Host Byte Order. Og siden sin_family ikke bliver sendt ud på netværket, så kan den være i Host Byte Order.


3.2. IP Adresser og Hvordan de HÃ¥ndteres

Heldigvis for dig findes der en række funktioner som tillader dig at manipulere IP adresser. Der er ingen grund til at beregne dem i hånden og så putte dem i en long med << operatoren.

Lad os sige du har en struct sockaddr_in ina, og du har en IP adresse "10.12.110.57" som du vil gemme i den. Funktionen du vil bruge, inet_addr(), konverterer en IP adresse i tal og punktum notation til en unsigned long. Tildelingen kan gøres på følgende måde:


    ina.sin_addr.s_addr = inet_addr("10.12.110.57"); 

Bemærk at inet_addr() allerede returnerer adressen i Network Byte Order -- du behøver ikke at kalde htonl(). Herligt!

Den overstående kodestump er ikke særlig robust eftersom der ikke er nogen fejlkontrol. inet_addr() returnerer -1 ved en fejl. Husker du binære tal? (unsigned>-1 svarer sjovt nok til IP adressen 255.255.255.255! Det er broadcast adressen! Forkert. Husk at lave en ordentlig fejlkontrol.

Faktisk er der et renere interface du kan bruge i stedet for inet_addr(), kaldet inet_aton() ("aton" betyder "ascii to network"):


    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    int inet_aton(const char *cp, struct in_addr *inp); 

Her er et eksempel under pakning af en struct sockaddr_in (dette eksempel vil give mere mening når du kommer til sektionerne om bind() og connect().)


    struct sockaddr_in my_addr;

    my_addr.sin_family = AF_INET;         // host byte order
    my_addr.sin_port = htons(MYPORT);     // short, network byte order
    inet_aton("10.12.110.57", &(my_addr.sin_addr));
    memset(&(my_addr.sin_zero), '\0', 8); // nulstil resten af structen 

inet_aton() returnerer, i modsætning til stort set alle andre socket-relaterede funktioner ikke-nul ved succes, og nul ved fejl. Og adressen er sendt tilbage i inp.

Desværre er inet_aton() ikke implementeret på alle platforme, så selvom dens brug er fortrukket, så er den gamle, og mere udbredte inet_addr() brugt i denne guide.

Okay, nu kan du konvertere IP adresser som strenge til deres binære format. Hvad med den anden vej? Hvad hvis du har en struct in_addr og du vil printe den som tal og punktum notation? I så fald skal du bruge funktionen inet_ntoa() ("ntoa" betyder "network to ascii") som sådan:


    printf("%s", inet_ntoa(ina.sin_addr)); 

Dette vil printe IP adressen. Bemærk at inet_ntoa() tager en struct in_addr som argument, ikke en long. Bemærk også at det returnerer en pointer til en char. Den pointer til et statisk gemt char array inde i inet_ntoa(), så næste gang du kalder inet_ntoa(), vil den overskrive den gamle IP adresse du bad om. F.eks.:


    char *a1, *a2;
    .
    .
    a1 = inet_ntoa(ina1.sin_addr);  // dette er 192.168.4.14
    a2 = inet_ntoa(ina2.sin_addr);  // dette er 10.12.110.57
    printf("address 1: %s\n",a1);
    printf("address 2: %s\n",a2); 

vil printe:


    address 1: 10.12.110.57
    address 2: 10.12.110.57 

Hvis du skal gemme adressen skal du strcpy() den til din egen char array.

Det er alt om dette emne lige nu. Senere vil du lære at konvertere en streng som "whitehouse.gov" til dens IP adresse (se DNS, længere nede.)


4. Systemkald eller kaos

Dette er sektionen hvor vi kommer ind på systemkald som giver adgang til netværksfunktionaliteten i en Unix boks. Når du kalder en af disse funktioner tager kernen over og gør automagisk al arbejdet for dig.

Stedet de fleste går i stå, er ved spørgsmålet om hvilken orden man skal kalde tingene i. Som du nok har opdaget er man pages ikke noget til i det henseende. For at hjælpe i denne frygtelige situation, har jeg prøvet at forklare systemkaldene i de følgende sektioner præcis (cirka) i samme orden som de skal kaldes i dine programmer.

Dette, sammen med nogle få stykker kode her og der, lidt mælk og et par kager (som du nok selv må skaffe), og lidt knofedt og mod, vil du snart kunne sende data rundt på Internettet som en vanvittig!


4.1. socket()--FÃ¥ Fat i en File Descriptor!

Nu kan jeg ikke udskyde det længere--jeg bliver nød til at snakke om sytemkaldet socket(). Her er opdelingen:


    #include <sys/types.h>
    #include <sys/socket.h>

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

Men hvad er det for argumenter? Først, skal domain sættes til "AF_INET", ligesom i struct sockaddr_in (Herover.) Som det næste fortæller type argumentet kernen hvilken slags socket der er tale om: SOCK_STREAM eller SOCK_DGRAM. Til sidst skal protocol sættes til "0" for at få socket() til at vælge den korrekte protokol baseret på type argumentet. (Noter: der er mange flere domains end jeg har vist. Der er mange flere types end jeg har vist. See socket() man pagen. Der er også en "bedre" måde at få fat i protocol. Se getprotobyname() man pagen.)

socket() returnerer simpelthen en socket descriptor som du kan bruge i senere systemkald, eller -1 ved fejl. Den globale variabel errno bliver sat til fejlværdien (se perror() man pagen.)

I noget dokumentation vil du støde på den mystiske "PF_INET". Dette er et underligt bæst som ikke ofte ses i den fri natur, men jeg kan lige så godt forklare det lidt her. Engang for lang tid siden var det tænkt at en adressefamilie (som "AF" i "AF_INET" står for) måske villo understøtte mange protokoller som var refereret til vha. deres protokolfamilie (som "PF" i "PF_INET" står for). Det skete ikke. Nå. Den rigtige ting at gøre er at bruge AF_INET i dine struct sockaddr_in og PF_INET i dit kald til socket(). Men praktisk talt kan du bruge AF_INET over det hele. Og siden det er hvad W. Richard Steven gør i sin bog, så er det hvad jeg vil gøre her.

Fint, fint, fint, men hvad gør den her socket godt for? Svaret er at den ikke rigtig kan noget for sig selv. Du er nød til at læse videre og lave flere systemkald for at det skal give mening.


4.2. bind()--Hvilken Port er jeg på?

Når du har en socket, bliver du måske til at associere den socket med en port på din lokale maskine. (Det er typisk gjort hvis du skal listen()e efter indkommende forbindelser på en specifik port.--MUDs gør dette når de fortæller dig at du skal "telnet til x.y.z port 6969".) Port nummeret bliver brugt af kernen til at matche en indgående pakke til specifikke processers socket descriptor. Hvis du kun skal lave en connect() er det måske unødvendigt. Læs det under alle omstændigheder, bare fordi det er sjovt.

Her er synopsis for systemkaldet bind():


    #include <sys/types.h>
    #include <sys/socket.h>

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

sockfd er socket file descriptoren returneret fra socket(). my_addr er en pointer til en struct sockaddr som indeholder information om din adresse, specifikt, din port og IP adresse. addrlen kan sættes til sizeof(struct sockaddr).

Puha. Det er en del at absorbere på en gang. Lad os tage et eksempel:


    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>

    #define MYPORT 3490

    main()
    {
        int sockfd;
        struct sockaddr_in my_addr;

        sockfd = socket(AF_INET, SOCK_STREAM, 0); // lav noget fejlkontrol!

        my_addr.sin_family = AF_INET;         // host byte order
        my_addr.sin_port = htons(MYPORT);     // short, network byte order
        my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
        memset(&(my_addr.sin_zero), '\0', 8); // nulstil resten af structen

        // glem ikke at lave fejlkontrol for bind():
        bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
        .
        .
        . 

Der er nogle få ting at bemærke her: my_addr.sin_port er i Network Byte Order. Samme med my_addr.sin_addr.s_addr. En anden ting at holde øje med er at headerfilen måske ændrer sig fra system til system. For at være sikker bør du checke dine lokale man pages.

Til sidst ved emnet bind(), burde jeg nævne at noget af arbejdet af at få din egen IP adresse og/eller port kan blive automatiseret:


        my_addr.sin_port = 0; // vaelg en tilfaeldig ubrugt port
        my_addr.sin_addr.s_addr = INADDR_ANY;  // brug min IP adresse 

Se. Ved at sætte my_addr.sin_port til nul fortæller du bind() at den skal vælge en port for dig. Ligeledes, ved at sætte my_addr.sin_addr.s_addr til INADDR_ANY, fortæller du den at den automatisk skal fylde IP adressen af den maskine du arbejder på ud.

Hvis du er en af dem der bemærker småting har du nok set at jeg ikke satte INADDR_ANY som Network Byte Order! Sikke frækt. Men jeg har ved noget i ikke ved. INADDR_ANY er faktisk nul! Nul har stadig nul på alle bits selvom du flytter om på bytes. Folk med hang til ren kode vil pointere at der kunne være en parallel dimension hvor INADDR_ANY måske var 12, og at min kode ikke ville virke der. Det er ok med mig:


        my_addr.sin_port = htons(0); // vaelg en tilfaeldig ubrugt port
        my_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // brug min IP adresse 

Nu er vi så portable man ikke skulle tro det. Jeg ville bare lige pointere at siden det meste af den kode du vil støde på, ikke vil sende INADDR_ANY gennem htonl().

bind() returnerer også -1 ved fejl og sætter errno til fejlens værdi.

En anden ting man skal være på udkig efter når man kalder bind(): Gå ikke for lavt ned i dine portnumre. Alle porte under 1024 er RESERVEREDE (med mindre du er administratoren)! Du kan have enhver port over det nummer, helt op til 65535 (så længe de ikke allerede bliver brugt af et andet program.)

Nogle gange vil du nok opdage, at du prøver at genstarte en server og bind() fejler og påstår at adressen allerede er i brug ("Address already in use.") Hvad betyder dette? En del af den socket der var forbundet er stadig til stede i kernen, og den hænger fast i porten. Du kan enten vente intil den er væk (et minut eller noget i den stil), eller tilføje kode til dit program som tillader at porten bliver brugt igen, som her:


    int yes=1;
	//char yes='1'; // Solaris folk bruger det her

    // Fjern den "Address already in use" fejlbesked
    if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
        perror("setsockopt");
        exit(1);
    } 

En lille ekstra sidste note om bind(): Der er tider hvor du ikke absolut skal kalde den. Hvis du connect()er til en fjern maskine, og du er ligeglad hvad din lokale port er (som er tilfældet med telnet, hvor du kun er interesseret i den fjerne port), så kan du nøjes med at kalde connect(). Den vil checke om socketen ikke er blevet bounded og vil bind()e den til en ubrugt lokal port hvis det er nødvendigt.


4.3. connect()--Hey, du der!

Lad os lige for en stund forestille os at du er et telnet program. Din bruger beder dig (ligesom i filmen TRON om at få en socket file descriptor. Du adlyder og kalder socket(). Bagefter beder brugeren dig om at forbinde til "10.12.110.57" på port "23" (standerd telnet port). Yo, hvad gør du nu?

Heldigvis for dig, program, er du nu i gang med at læse en sektion om connect()--hvordan man forbinder til en fjern vært (remote host). Læs videre! Der er ingen tid at spilde!

connect() kaldet er som følger:


    #include <sys/types.h>
    #include <sys/socket.h>

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

sockfd er vores egen socket file descriptor som bliver returneret af socket() kaldet, serv_addr er en struct sockaddr som indeholder destinations porten og IP adressen, og addrlen kan sættes til sizeof(struct sockaddr).

Giver det ikke lidt mere mening? Lad os tage et eksempel:


    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>

    #define DEST_IP   "10.12.110.57"
    #define DEST_PORT 23

    main()
    {
        int sockfd;
        struct sockaddr_in dest_addr;   // vil holde destinationsadressen

        sockfd = socket(AF_INET, SOCK_STREAM, 0); // lav noget fejlkontrol!

        dest_addr.sin_family = AF_INET;          // host byte order
        dest_addr.sin_port = htons(DEST_PORT);   // short, network byte order
        dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
        memset(&(dest_addr.sin_zero), '\0', 8);  // nulstil resten af structen

        // glem ikke at kontrollere connect() for fejl!
        connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
        .
        .
        . 

Igen skal du huske at kontrollere return værdien fra connect()--den vil returnere -1 ved en fejl, og sætter errno.

Bemærk også at vi ikke kaldte bind(). Faktisk er vi ligeglade med det lokale portnummer; vi er kun interesserede i hvor vi skal hen (den fjerne port). Kernen vælger automatisk en lokal port for os, og sitet vi forbinder til vil automatisk få denne information fra os. Ingen problemer.


4.4. listen()--Vil nogen ringe til mig?

Ok, nu er det tid til et temposkift. Hvad hvis vi ikke vil forbinde til en fjern vært. F.eks. kunne vi, bare for sjov, forestille os at vi ville vente på indgående forbindelser og håndtere dem på en eller anden måde. Denne proces er delt op i to: først listen()er du, så accept()er du (se herunder.)

Listen kaldet er ret simpelt, men kræver lidt forklaring:


    int listen(int sockfd, int backlog); 

sockfd er den velkendte socket file descriptor fra socket() systemkaldet. backlog er antallet af forbindelser tilladt i den indgående kø. Hvad betyder det? Indgående forbindelser vil vente i denne kø indtil du accept()er dem (se herunder), og der er en grænse for hvor mange der kan være i køen. De fleste systemer sætter dette antal til omkring 20; du kan sikkert slippe afsted med at sætte det til 5 eller 10.

Som altid returnerer listen() -1 og sætter errno ved fejl.

Nå, men som du sikkert kan forestille dig skal vi kalde bind() før vi kalder listen(), ellers vil kernen få os til at lytte på en tilfældig port. Bløh! Så hvis du vil lytte til indgående forbindelser skal rækkefølgen du kalder systemkald på være:


    socket();
    bind();
    listen();
    /* accept() kommer her */ 

Jeg vil nøjes med dette i stedet for et kodeeksempel, siden det er selvforklarende. (Koden i accept() sektionen herunder er mere fyldestgørende.) Den virkelig vanskelige del af alt dette er kaldet til accept().


4.5. accept()--"Tak for dit kald til port 3490."

Gør dig klar på at accept() er noget mærkværdig! Hvad der sker er følgende: nogen langt langt væk vil prøve at connect()e til din maskine på en port som du listen()er på. Deres forbindelse vil blive sat i kø og vente på at blive accept()eret. Du kalder accept() og fortæller den at den skal finde den ventende forbindelse. Den vil returnere en helt ny socket file descriptor som du kan bruge til den her ene forbindelse! Det er rigtigt, pludselig har du to socket file discriptors for ens pris! Den oprindelige lytter stadig på din port, og den nyeligt oprettede er omsider klar til at send()e og recv()e. Så har vi nået målet!

Kaldet er som følger:


     #include <sys/socket.h>

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

sockfd er den lyttende ( listen()e) socket descriptor. Let nok. addr vil normalt være en pointer til en lokal struct sockaddr_in. Dette er hvor informationon om den indgående forbindelse vil være (og med den kan du finde ud af hvilken vært der kalder dig fra hvilken port). addrlen er en lokal integer variabel som skal sættes til sizeof(struct sockaddr_in) før adressen er sendt til accept(). Accept vil ikke putte flere end så mange bytes ind i addr. Hvis den putter færre i den, vil den ændre værdien af addrlen til at afspejle det.

Gæt hvad? accept() returnerer -1 og sætter errno hvis der sker en fejl. Det havde du nok ikke regnet med.

Som før, er det her en del at absorbere på en gang, så her er et kodeeksempel for din fornøjelses skyld:


    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>

    #define MYPORT 3490    // porten brugere vil forbinde til

    #define BACKLOG 10     // antallet af indgaaende forbindelser koeen
                           // vil holde

    main()
    {
        int sockfd, new_fd;  // lyt paa sock_fd, ny forbindelse paa new_fd
        struct sockaddr_in my_addr;    // min adresseinformation
        struct sockaddr_in their_addr; // den forbindendes adresseinformation
        int sin_size;

        sockfd = socket(AF_INET, SOCK_STREAM, 0); // lav noget fejlkontrol!

        my_addr.sin_family = AF_INET;         // host byte order
        my_addr.sin_port = htons(MYPORT);     // short, network byte order
        my_addr.sin_addr.s_addr = INADDR_ANY; // Fyld ud med min IP
        memset(&(my_addr.sin_zero), '\0', 8); // nulstil resten af structen

        // glem ikke at lave fejlkontrol paa disse kald:
        bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

        listen(sockfd, BACKLOG);

        sin_size = sizeof(struct sockaddr_in);
        new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
        .
        .
        . 

Igen, bemærk at vi bruger new_fd socket descriptoren til alle send() og recv() kald. Hvis du kun får en enkelt forbindelse kan du close() den lyttende sockfd for at undgå flere indgående forbindelser på den samme port, hvis det er det du vil.


4.6. send() og recv()--Snak med mig, baby!

Disse to funktioner er til for at kommunikere over stream sockets eller forbundne datagram sockets. Hvis du vil bruge regulære uforbundne datagram sockets bliver du nød til at kigge på sektionen om sendto() og recvfrom().

send() kaldet:


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

sockfd er den socket descriptor du vil sende data til (om det er en returneret af socket() eller en du fik med accept().) msg er en pointer til det data du vil sende, og len er længden af dataene i bytes. Bare sæt flags til 0. (Se send() man pagen for mere information om flags.)

Et kodeeksempel kunne være:


    char *msg = "Beej was here!";
    int len, bytes_sent;
    .
    .
    len = strlen(msg);
    bytes_sent = send(sockfd, msg, len, 0);
    .
    .
    . 

send() returnerer antallet af bytes der faktisk blev sendt--Dette kan være færre end det antal du bad om at sende!. Nogle gange beder du den om at sende en hel masse data. Det kan den ikke håndtere. Den vil sende så meget data den kan og regne med at du sender resten senere. Husk, hvis værdien returneret af send() ikke svarer til værdien i len, så er det op til dig at sende resten af strengen. De gode nyheder er dette: hvis pakken er lille (mindre end 1K eller noget i den stil) så vil den sandsynligvis kunne sende det hele på en gang. Igen bliver -1 returneret ved en fejl, og errno bliver sat til fejlnummeret.

recv() kaldet er ens i mange henseender:


    int recv(int sockfd, void *buf, int len, unsigned int flags); 

sockfd er socket descriptoren at læse fra, buf er bufferen at læse informationen ind i, len er den maximale længde af bufferen og flags kan igen sættes til 0. (Se recv() man pagen for information om flag.)

recv() returnerer antallet af bytes der egentlig blev læst ind i bufferen, eller -1 ved en fejl (med errno sat alt efter resultatet.)

Vent! recv() kan returnere 0. Dette kan kun betyde en ting: den fjerne side har lukket forbindelsen! En return værdi på 0 er recv()'s måde at lade dig vide at dette er sket.

Så, det var da let, var det ikke? Nu kan du sende data frem og tilbage på stream sockets! Wee! Du er nu en Unix Netværks Programmør!


4.7. sendto() and recvfrom()--Snak med mig, DGRAM-style

"Det er jo altsammen meget fint," hører jeg dig sige, "men hvad har det med uforbundne datagram sockets at gøre?" No problemo, amigo. Det er lige hvad vi skal til.

Siden datagram sockets ikke er forbundne til en fjern vært, så gæt hvad vi skal give af information før vi sender en pakke? Det er rigtigt! Destinationsadressen! Her er funktionen:


    int sendto(int sockfd, const void *msg, int len, unsigned int flags,
               const struct sockaddr *to, int tolen); 

Som du kan se er dette kald stort set det samme som et kald til send() med tilføjelsen af to slags information. to er en pointer til en struct sockaddr (som du sikkert har som en sockaddr_in og caster i sidste øjeblik) som indeholder destinationens IP adresse og port. tolen kan sættes til sizeof(struct sockaddr).

Ligesom med send(), returnerer sendto antallet af bytes som faktisk blev sendt (som, igen, kan være mindre end antallet af bytes du bad den om at sende!), eller -1 ved en fejl.

På samme måde er recv() og recvfrom() meget ens. Synopsis for recvfrom() er:


    int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
                 struct sockaddr *from, int *fromlen); 

Igen er dette præcis ligesom recv() med tilføjelsen af et par felter. from er en pointer til en lokal struct sockaddr som vil blive fyldt med IP adressen og porten hos den sendende maskine. fromlen er en pointer til en lokal int som bør initialiseres til sizeof(struct sockaddr). Når funktionen returnerer, vil fromlen indeholde længden af den faktiske adresse der er gemt i from.

recvfrom() returnerer antallet af bytes der er modtaget, eller -1 ved fejl (med errno sat alt efter fejlen.)

Husk at du, hvis du connect()er en datagram socket, kan bruge send() og recv() til alle dine overførsler. Socketen selv er stadig en datagram socket, og pakkerne vil stadig bruge UDP, men socket interfaces vil automatisk tilføje destination og source information for dig.


4.8. close() og shutdown()--SÃ¥ er det ud!

Pyha! Du har send()t og recv()et data hele dagen, og du er træt. Nu er du klar til at lukke forbindelsen på din socket descriptor. Dette er let. Du kan bare bruge den normale Unix file descriptor close() funktion:


    close(sockfd); 

Dette vil hindre flere læsninger eller skrivninger til socketen. Enhver der prøver at læse eller skrive til socketen vil få en fejl.

I tilfælde af at du vil have lidt mere kontrol over hvordan din socket lukker, kan du bruge shutdown() funktionen. Den tillader dig at lukke for kommunikationen i en given retning, eller begge veje (ligesom close() gør.) Synopsis:


    int shutdown(int sockfd, int how); 

sockfd er den socket file descriptor du vil lukke ned, og how er en af følgende:

  • 0 -- Luk for yderligere modtagelse

  • 1 -- Luk for yderligere afsendelse

  • 2 -- Luk for yderligere afsendelse og modtagelse (ligesom close())

shutdown() returnerer 0 ved succes, og -1 ved fejl (med errno sat.)

Hvis du bruger shutdown() på uforbundne datagram sockets vil den kun gøre din socket ude af stand til at bruge send() og recv() (husk at du kan bruge disse hvis du connect()er dine datagram sockets.)

Det er vigtigt at bemærke at shutdown() faktisk ikke lukker file descriptoren--den ændrer bare dens funktionalitet. For at frigøre en socket descriptor skal du bruge close().

Det er intet problem.


4.9. getpeername()--Hvem er du?

Denne funktion er så let.

Den er så let at jeg nærmest ikke gad give den sin egen sektion, men her er den alligevel.

Funktionen getpeername() fortæller dig hvem der er i den anden ende af en forbundet stream socket. Synopsis:


    #include <sys/socket.h>

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

sockfd er descriptoren af den forbundne stream socket, addr er en pointer til en struct sockaddr (eller en struct sockaddr_in) som holder informationen om den anden side af forbindelsen, og addrlen er en pointer til en int, som bør initialiseres til sizeof(struct sockaddr).

Funktionen returnerer -1 ved fejl, og sætter errno

Når du har deres adresse kan du bruge inet_ntoa() eller gethostbyaddr() til at printe eller få mere information. Nej, du kan ikke få deres loginnavn. (Ok, ok. Hvis den anden computer har en ident daemon er det muligt. Dette vil ikke blive forklaret yderligere. Check RFC-1413 hvis du vil vide mere.)


4.10. gethostname()--Hvem er jeg?

gethostname() er ligefrem lettere end getpeername(). Den returnerer navnet på den computer dit program kører på. Navnet kan så bruges af gethostbyname(), herunder, til at finde ud af IP adressen på den lokale maskine.

Hvad kunne være sjovere? Jeg kunne finde på et par ting, men de har intet at gøre med socket programmering. Under alle omstændigheder er opdelingen således::


    #include <unistd.h>

    int gethostname(char *hostname, size_t size); 

Argumenterne er simple: hostname er en pointer til en array af chars, som vil indeholde værtsnavnet når funktionen returnerer, og size er længden i bytes af hostname arrayen.

Funktionen returnerer 0 ved succes og -1 ved fejl, og sætter errno som sædvanelig.


4.11. DNS--Du siger "whitehouse.gov", jeg siger "198.137.240.92"

Hvis du ikke ved hvad DNS er, så står det for "Domain Name Service". Som sådan fortæller du det hvad den adressen er på et menneskeligt forståeligt format, og så giver den dig IP adressen (så du kan bruge den med bind(), connect(), sendto() eller hvad du nu vil bruge den til.) Hvis nogen f.eks. skriver:


    $ telnet whitehouse.gov

Finder telnet ud af at den skal connect()e til "198.137.240.92".

Men hvordan virker det? Du vil bruge funktionen gethostbyname():


    #include <netdb.h>
    
    struct hostent *gethostbyname(const char *name); 

Som du kan se returnerer den en pointer til en struct hostent, hvilken ser sådan ud:


    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] 

Her er forklaringerne af felterne i en struct hostent:

  • h_name -- Det officielle navn af værten

  • h_aliases -- En NULL-termineret array af alternative navne til værten.

  • h_addrtype -- Typen af adressen der er returneret; normalt AF_INET.

  • h_length -- Længden af adressen i bytes.

  • h_addr_list -- En nul-termineret array af netværksadresser til værten. Værtsadresserne er i Network Byte Order.

  • h_addr -- Den første adresse i h_addr_list.

gethostbyname() returnerer en pointer til den fyldte struct hostent, eller NULL ved fejl. (Men errno er ikke sat--h_errno sættes i stedet Se herror(), herunder.)

Men hvordan bruges den? Nogle gange (hvilket vi finder ud af ved at læse computermanualer), giver det ikke meget at smide en masse informationer til læseren. Denne funktion er bestemt lettere at bruge en det ser ud til.

Her er et kodeeksempel:


    /*
    ** getip.c -- en hostname lookup demo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    int main(int argc, char *argv[])
    {
        struct hostent *h;

        if (argc != 2) {  // fejlkontroller kommandolinien
            fprintf(stderr,"usage: getip address\n");
            exit(1);
        }

        if ((h=gethostbyname(argv[1])) == NULL) {  // faa vaertsinfo
            herror("gethostbyname");
            exit(1);
        }

        printf("Host name  : %s\n", h->h_name);
        printf("IP Address : %s\n", inet_ntoa(*((struct in_addr *)h->h_addr)));
       
       return 0;
    } 

Du kan ikke bruge perror() til at skrive fejlbeskeder med gethostbyname() (da errno ikke bliver brugt). Istedet kalder du herror().

Det er temmelig lige ud af landevejen. Du sender simpelthen strengen som indeholder maskinens navn ("whitehouse.gov") til gethostbyname(), og henter derefter informationerne ud af den returnerede struct hestent.

Den eneste mulige besynderlighed kunne være at når man printer IP adressen herover. h->h_addr er en char*, men inet_ntoa() kræver en struct in_addr. Så jeg caster h->h_addr til en struct in_addr*, og derefererer den for at få dataene.


5. Client-Server Baggrund

Det er en klient-server verden vi lever i, baby. Stort set alt på nettet har at gøre med en klient proces der snakker med server processer og omvendt. F.eks. telnet. Når du forbinder til en fjern vært på port 23 med telnet (klienten) vækkes et program på den vært (kaldet telnetd, serveren). Det håndterer den indgående telnetforbindelse, laver en loginprompt, osv.

Figure 2. Client-Server Interaktion.

Udvekslingen af information mellem klient og server er opridset i Figur 2.

Bemærk, at klient-server parret kan snakke SOCK_STREAM, SOCK_DGRAM, eller enhver anden ting (så længe de snakker det samme.) Nogle gode eksempler på klient-server par er telnet/telnetd, ftp/ftpd, eller bootp/bootpd. Hver gang du bruger ftp, er der et fjernprogram, ftpd, som tilbyder sig.

Ofte vil der kun være en server på een maskine, og den server vil håndtere mange klienter vha. fork(). Det typiske rutine er: server venter på en forbindelse, accept()erer den og fork()er en child proces til at håndtere den. Dette er hvad vores servereksempel i næste sektion gør.


5.1. En Simpel Stream Server

Alt denne server gør, er at sende tekststrengen "Hello, World!\n" ud over en stream forbindelse. Alt du behøver at gøre for at teste denne server er at køre den i et vindue, og telnette til den fra en anden med:


    $ telnet remotehostname 3490

hvor remotehostname er navnet på den computer du kører den på.

Server koden: (Bemærk: et backslash i slutningen af en linie betyder at linien fortsætter på den næste.)


    /*
    ** server.c -- en stream socket server demo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/wait.h>
    #include <signal.h>

    #define MYPORT 3490    // porten brugere forbinder til

    #define BACKLOG 10     // hvor mange forbindelser koeen vil holde

    void sigchld_handler(int s)
    {
        while(wait(NULL) > 0);
    }

    int main(void)
    {
        int sockfd, new_fd;  // lyt paa sock_fd, nye forbindelser paa new_fd
        struct sockaddr_in my_addr;    // min adresseinformation
        struct sockaddr_in their_addr; // den forbindenes adresseinformation
        int sin_size;
        struct sigaction sa;
        int yes=1;

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

        if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }
        
        my_addr.sin_family = AF_INET;         // host byte order
        my_addr.sin_port = htons(MYPORT);     // short, network byte order
        my_addr.sin_addr.s_addr = INADDR_ANY; // fyld ud med min IP
        memset(&(my_addr.sin_zero), '\0', 8); // nulstil resten af packeten

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

        if (listen(sockfd, BACKLOG) == -1) {
            perror("listen");
            exit(1);
        }

        sa.sa_handler = sigchld_handler; // draeb alle doede processer
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGCHLD, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }

        while(1) {  // hoved accept() loop
            sin_size = sizeof(struct sockaddr_in);
            if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
                                                           &sin_size)) == -1) {
                perror("accept");
                continue;
            }
            printf("server: got connection from %s\n",
                                               inet_ntoa(their_addr.sin_addr));
            if (!fork()) { // dette er child processen
                close(sockfd); // child behoever ikke at lytte
                if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  // parent behoever ikke denne
        }

        return 0;
    } 

Hvis du er nysgerrig har jeg min kode i en stor main() funktion for (synes jeg) at gøre syntaksen mere klar. Du kan splitte den op i mindre funktioner hvis du føler det er bedre.

Alle de sigaction() ting der sker er måske nye--det er ok. Den kode der er der er til for at dræbe zombie processor som opstår når de fork()ede child processer slutter. (Hvis du laver mange zombier og ikke dræber dem bliver din systemadministrator sur.

Du kan få dataene fra denne server ved at bruge klienten som er lavet i den næste sektion.


5.2. En Simpel Stream Klient

Ham her er lettere at lave end serveren. Alt denne klient gør er at forbinde til den vært du bestemmer på kommandolinien, port 3490. Den henter strengen som serveren sender.

Kildekoden til klienten:


    /*
    ** client.c -- en stream socket client demo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>

    #define PORT 3490 // porten klienten vil forbinde til

    #define MAXDATASIZE 100 // antallet af bytes vi kan faa paa en gang

    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;  
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // forbinderens adresseinformation

        if (argc != 2) {
            fprintf(stderr,"usage: client hostname\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // faa vaertsinfo
            perror("gethostbyname");
            exit(1);
        }

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

        their_addr.sin_family = AF_INET;    // host byte order 
        their_addr.sin_port = htons(PORT);  // short, network byte order 
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8);  // nulstil resten af structen

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

        if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
            perror("recv");
            exit(1);
        }

        buf[numbytes] = '\0';

        printf("Received: %s",buf);

        close(sockfd);

        return 0;
    } 

Bemærk at connect() returnerer "Connection refused" hvis du ikke kører serveren før du kører klienten. Det er en brugbar fejlmeddelelse.


5.3. Datagram Sockets

Jeg har virkelig ikke meget at snakke om her, så jeg vil bare vise et par eksempler: talker.c og listener.c.

listener sidder på en maskine og venter på en indgående pakke på port 4950. talker sender en pakke til den port på den givne maskine der indeholder hvad brugeren skrev på sin kommandolinie.

Her er kilden til listener.c:


    /*
    ** listener.c -- en datagram socket "server" demo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define MYPORT 4950    // porten brugere vil forbinde til

    #define MAXBUFLEN 100

    int main(void)
    {
        int sockfd;
        struct sockaddr_in my_addr;    // min adresseinformation
        struct sockaddr_in their_addr; // forbinderens adresseinformation
        int addr_len, numbytes;
        char buf[MAXBUFLEN];

        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        my_addr.sin_family = AF_INET;         // host byte order
        my_addr.sin_port = htons(MYPORT);     // short, network byte order
        my_addr.sin_addr.s_addr = INADDR_ANY; // fyldes med min IP
        memset(&(my_addr.sin_zero), '\0', 8); // nulstil resten af structen

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

        addr_len = sizeof(struct sockaddr);
        if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0,
                           (struct sockaddr *)&their_addr, &addr_len)) == -1) {
            perror("recvfrom");
            exit(1);
        }

        printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
        printf("packet is %d bytes long\n",numbytes);
        buf[numbytes] = '\0';
        printf("packet contains \"%s\"\n",buf);

        close(sockfd);

        return 0;
    } 

Bemærk, at vi brugte SOCK_DGRAM i vores kald til socket(). Bemærk også at der ikke er brug for at bruge listen() eller accept(). Dette er en af fordelene ved at bruge uforbundne datagram sockets!

Nu kommer kilden til talker.c:


    /*
    ** talker.c -- en datagram "klient" demo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <netdb.h>

    #define MYPORT 4950    // porten brugere forbinder til

    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; // forbinderens adresseinformation
        struct hostent *he;
        int numbytes;

        if (argc != 3) {
            fprintf(stderr,"usage: talker hostname message\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // faa vaertsinformation
            perror("gethostbyname");
            exit(1);
        }

        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        their_addr.sin_family = AF_INET;     // host byte order
        their_addr.sin_port = htons(MYPORT); // short, network byte order
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8); // nulstil resten af structen

        if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
             (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
            perror("sendto");
            exit(1);
        }

        printf("sent %d bytes to %s\n", numbytes,
                                               inet_ntoa(their_addr.sin_addr));

        close(sockfd);

        return 0;
    } 

Og det er alt der skal til! Kør listener på en eller anden maskine, og kør talker på en anden, og se dem kommunikere! Sjov og spas for hele familien!

Lige bortset fra en lille detalje jeg har nævnt mange gange før: forbundne datagram sockets. Jeg vil få brug for at snakke lidt om det her, da vi er i datagram sektionen af dokumentet. Lad os forestille os at talker kalder connect() og specificerer listeners adresse. Derfra må talker kun sende og skrive til adressen givet til connect(). Derfor behøver du ikke bruge sendto() og recvfrom(); du kan simpelthen bruge send() og recv().


6. Rimelig Advancerede Teknikker

Disse er egentlig ikke særlig advancerede, men de går lidt ud over det basale som vi allerede har været igennem. Faktisk er du, hvis du er nået hertil, ganske velbevandret i det mest basale Unix netværksprogrammering! Tillykke!

Så nu er vi på vej ud i den fagre nye verden, til nogen af de mere eksotiske ting som du måske har lyst til at lære omkring sockets.


6.1. Blokering (Blocking)

Blokering. Du har hørt om det--men hvad i alverden er det? Som sådan er det bare west coast indianerslang for "at sove". Du har sikkert lagt mærke til, at når du kører listener herover, så sidder den bare og venter til der ankommer en pakke. Hvad der sker er, at den kalder recvfrom(). Der er ingen data, så recvfrom() "blokerer" (sover) indtil der ankommer noget data.

Der er mange funktioner der blokerer. accept() blokerer. Alle recv() funktioner blokerer. Grunden til at de blokerer er at de har tilladelse til det. Når du laver en socket descriptor med socket() sætter kernen den til at blokere. Hvis du ikke vil have at en socket blokerer, skal du kalde fnctl():


    #include <unistd.h>
    #include <fcntl.h>
    .
    .
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    .
    . 

Ved at sætte en socket til ikke at blokere, kan du "polle" den socket for information. Hvis du prøver at læse fra en ikke-blokerende socket, og der ingen data er, er det ikke tilladt at den blokerer--den vil returnere -1 og errno vil blive sat til EWOULDBLOCK.

Normalt er denne type polling en dum ide. Når du sætter dit program i en busy-wait hvor den venter på data fra en socket vil du bruge meget CPU kraft. En mere elegant løsning til at checke om der er data der venter på at blive læst kommer i den næste sektion om select().


6.2. select()--Synkron I/O Multiplexing

Denne funktion er en smule sær, men den er meget brugbar. Tag f.eks. denne situation: Forestil dig du er en server, og vil lytte efter indgående forbindelser, samtidig med at du vil læse fra de forbindelser du allerede har.

Så siger du nok "Det er intet problem!". Det er bare en accept() og et par recv(). Hov, ikke så hurtigt du! Hvad hvis du blokerer på et accept() kald? Hvordan vil du recv()e data samtidig? "Ved brug at ikke-blokerende sockets!" Nej da! Du skal ikke bruge al den CPU kraft. Men hvad så?

select() giver dig mulighed for at overvåge flere sockets på samme tid. Den fortæller dig hvilke der er klar til at blive læst fra, hvilke der er klar til at blive skrevet til og hvilke der har lavet exceptions, hvis du virkelig vil vide det.

Uden videre omsvøb, vil jeg her bringe synopsis for select():


       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int numfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout); 

Funktionen overvåger et "sæt" af file descriptors; nemlig readfds, writefds, og exceptfds. Hvis du vil se om du kan læse fra fra standard input og en eller anden socket descriptor, sockfd, kan du bare tilføje file descriptorene 0 og sockfd til sættet readfds. Parametren numfds bør sættes til værdien af den største file descriptor plus en. I det her eksempel skulle den sættes til sockfd+1, siden den altid er større end standard input (0).

Når select() returnerer vil readfds være ændret til at være den af file descriptorne du valgte som er klar til læsning. Du kan teste dem med makroen FD_ISSET().

Før du fortsætter meget længere vil jeg snakke om hvordan man kan manipulere disse sæt. Ethvert sæt er af typen fd_set. Følgende makroer kan bruges på denne type:

  • FD_ZERO(fd_set *set) -- rydder et file descriptor sæt

  • FD_SET(int fd, fd_set *set) -- tilføjer fd til sættet

  • FD_CLR(int fd, fd_set *set) -- fjerner fd fra sættet

  • FD_ISSET(int fd, fd_set *set) -- tester om fd er i sættet

Til sidst skal jeg fortælle hvad den mystiske struct timeval er. Nogle gange vil man ikke vente for evigt indtil nogen sender nogle data. Måske vil du skrive "Still Going..." til terminalen hver gang der er gået 96 sekunder, selvom der ikke er sket noget. Denne time structure gør at du kan give en timeout. Hvis tiden overskrider og select() stadig ikke har fundet en file descriptors der er klar, vil den returnere så du kan fortsætte dit program.

struct timeval har de følgende felter:


    struct timeval {
        int tv_sec;     // seconds
        int tv_usec;    // microseconds
    }; 

Bare sæt tv_sec til antallet af sekunder der skal ventes, og tv_usec til antallet af mikrosekunder der skal ventes. Ja det er mikrosekunder, ikke millisekunder. Der er 1000 mikrosekunder på et millisekund, og 1000 millisekunder på et sekund. Altså er der 1000000 mikrosekunder på et sekund. Hvorfor er det "usec"? "u"'et skulle ligne det græske bogstav μ (Mu) som vi bruger som "mikro". Når funktionen returnerer bliver timeout måske opdateret til at vise tiden der mangler. Dette kommer an på hvilken slags Unix du kører.

Hurra! Vi har en timer med en opløsning på et mikrosekund! Eller, det skal du ikke helt stole på!. Den normale timeslice for Unix er omkring 100 millisekunder, så du bliver måske nød til at vente i hvert fald så lang tid, ligegyldigt hvor lille du sætter din struct timeval.

Andre interessante ting: Hvis du sætter felterne i din struct timeval til 0 vil select() straks timeoute, og poller dermed alle file descriptors i dine sæt. Hvis du sætter parametren timeout til NULL, vil den aldrig timeoute, og vil i stedet vente til den første file descriptor er klar. Hvis du ikke er interesseret i at vente på et specielt sæt, kan du bare sætte det til NULL i kaldet til select().

Det følgende kodeeksempel venter i 2.5 sekunder på at der kommer noget på standard input:


    /*
    ** select.c -- en select() demo
    */

    #include <stdio.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>

    #define STDIN 0  // file descriptor for standard input

    int main(void)
    {
        struct timeval tv;
        fd_set readfds;

        tv.tv_sec = 2;
        tv.tv_usec = 500000;

        FD_ZERO(&readfds);
        FD_SET(STDIN, &readfds);

        // vi er ligeglad med writefds og exceptfds:
        select(STDIN+1, &readfds, NULL, NULL, &tv);

        if (FD_ISSET(STDIN, &readfds))
            printf("A key was pressed!\n");
        else
            printf("Timed out.\n");

        return 0;
    } 

Hvis du er på en terminal der er line buffered skal du trykke på RETURN. Ellers vil du timeoute alligevel.

Nu synes nogle af jer nok at det her er en god måde at vente på data fra en datagram socket--og du har ret: det er det måske. Nogle Unix versioner kan bruge select på denne måde, og nogle kan ikke. Du bør se i din lokale man page hvis du vil prøve på det.

Nogle Unices opdaterer tiden i din struct timeval til at være tiden der er tilbage før der sker et timeout. Andre gør ikke. Lås dig ikke fast på at det altid sker hvis du vil være portabel. (brug gettimeofday() hvis du har brug for at checke hvor meget tid der er gået. Jeg ved det, det er træls, men sådan er det bare.)

Hvad sker der hvis en socket i read sættet lukker forbindelsen? I så fald returnerer select() med den socket descriptor sat som "klar til at læse". Når du så faktisk recv()er fra den returnerer recv() 0. Sådan ved du at din klient har lukket forbindelsen.

En anden interessant ting ved select() er: hvis du har en socket som listen()er, så kan du se om der er en ny forbindelse ved at putte den sockets file descriptor i readfds sættet.

Og det er så et hurtigt overblik over den allestedsnærværende select() funktion.

Men, pga. stor efterspørgsel, har jeg her et eksempel. Uheldigvis er forskellen fra det meget simple overstående eksempel og dette rimelig stor. Men tage et kig og læs forklaringen som følger.

Dette program virker som en simpel multi-bruger chat server. Start den i et vindue, og telnet til den ("telnet værtsnavn 9034") fra flere andre vinduer. Så skulle det vises i alle sessioner når du skriver noget i en telnet session.


    /*
    ** selectserver.c -- en lille multibruger chat server
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define PORT 9034   // den port vi lytter p&aring;

    int main(void)
    {
        fd_set master;   // master file descriptor liste
        fd_set read_fds; // midlertidig file descriptor liste til select()
        struct sockaddr_in myaddr;     // server adresse
        struct sockaddr_in remoteaddr; // klient adresse
        int fdmax;        // maksimalt antal af file descriptors
        int listener;     // lyttende socket descriptor
        int newfd;        // ny accept()eret socket descriptor
        char buf[256];    // buffer til klientdata
        int nbytes;
        int yes=1;        // til setsockopt() SO_REUSEADDR
        int addrlen;
        int i, j;

        FD_ZERO(&master);    // ryd master og temp saettene
        FD_ZERO(&read_fds);

        // opret en lyttende socket
        if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        // fjern fejlbeskeden "address already in use"
        if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes,
                                                            sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }

        // bind
        myaddr.sin_family = AF_INET;
        myaddr.sin_addr.s_addr = INADDR_ANY;
        myaddr.sin_port = htons(PORT);
        memset(&(myaddr.sin_zero), '\0', 8);
        if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) {
            perror("bind");
            exit(1);
        }

        // listen
        if (listen(listener, 10) == -1) {
            perror("listen");
            exit(1);
        }

        // tilfoej listener til master saettet
        FD_SET(listener, &master);

        // hold styr paa den stoerste file descriptor
        fdmax = listener; // indtil nu er det den her

        // main loop
        for(;;) {
            read_fds = master; // kopier det
            if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
                perror("select");
                exit(1);
            }

            // led efter data der kan l&aelig;ses i de eksisterende forbindelser
            for(i = 0; i <= fdmax; i++) {
                if (FD_ISSET(i, &read_fds)) { // her er en!!
                    if (i == listener) {
                        // haandter nye forbindelser
                        addrlen = sizeof(remoteaddr);
                        if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr,
                                                                 &addrlen)) == -1) { 
                            perror("accept");
                        } else {
                            FD_SET(newfd, &master); // tilfoej til master saettet
                            if (newfd > fdmax) {    // hold styr paa maksimum
                                fdmax = newfd;
                            }
                            printf("selectserver: new connection from %s on "
                                "socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd);
                        }
                    } else {
                        // haandter data fra en klient
                        if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) {
                            // fejl, eller forbindelsen er lukket
                            if (nbytes == 0) {
                                // forbindelsen lukket
                                printf("selectserver: socket %d hung up\n", i);
                            } else {
                                perror("recv");
                            }
                            close(i); // farvel!
                            FD_CLR(i, &master); // fjern fra master saettet
                        } else {
                            // vi har faaet noget data fra klienten
                            for(j = 0; j <= fdmax; j++) {
                                // send det til alle!
                                if (FD_ISSET(j, &master)) {
                                    // bortset fra listeneren og os selv
                                    if (j != listener && j != i) {
                                        if (send(j, buf, nbytes, 0) == -1) {
                                            perror("send");
                                        }
                                    }
                                }
                            }
                        }
                    } // Det her er bare GRIMT !!
                }
            }
        }
        
        return 0;
    } 

Bemærk at jeg har to file descriptor sæt i koden: master og read_fds. Den første, master, indeholder alle socket descriptors som er forbundne, samt socket descriptoren som lytter efter nye forbindelser.

Grunden til jeg har master sættet er, at select() faktisk ændrer sættet du giver til den for at vise hvilke sockets der er klar til at blive læst fra. Siden jeg bliver nød til at holde styr på forbindelser fra et kald af select() til det næste, bliver jeg nød til at gemme dem sikkert et sted. I sidste øjeblik kopierer jeg master ind i read_fds, og kalder så select().

Men betyder dette ikke at jeg skal tilføje enhver ny forbindelse jeg får til master sættet, hver gang jeg får en ny forbindelse? Jo! Og hver gange en forbindelse lukker, bliver jeg nød til at fjerne den fra master sættet? Jo, det gør det.

Bemærk at jeg kontrollerer hvornår listener socketen er klar til at blive læst fra. Når den er, betyder det at jeg har en ny forbindelse klar, og jeg accept()erer den, og tilføjer den til master sættet. Når en klients forbindelse er klar til at blive læst fra og recv() returnerer 0 ved jeg at klienten har lukket forbindelsen, og at jeg skal fjerne den fra master sættet.

Hvis klintens recv() ikke returnerer 0, ved jeg at der er modtaget noget data. Så jeg henter det, og går gennem master listen og sender det data til resten af de forbundne klienter.

Og det, mine venner, er et knap så simpelt overblik over den allestedsnærværende select() funktion.


6.3. HÃ¥ndtering af delvise send()s

Husker du sektionen om send(), da jeg sagde at send() måske ikke sendte alle de bytes du bad om? At du vil sende 512 bytes, men den returnerer 412. Hvad skete der med de sidste 100 bytes?

De er stadig i din lille buffer og venter på at blive sendt. På grund af omstændigheder som du ikke kan kontrollere, har kernen besluttet at den ikke vil sende alle dataene ud på en gang, og du er det op til dig at få dataene derud.

Du kunne skrive en funktion som denne til at gøre det:


    #include <sys/types.h>
    #include <sys/socket.h>

    int sendall(int s, char *buf, int *len)
    {
        int total = 0;        // hvor mange bytes vi har sendt
        int bytesleft = *len; // hvor mange bytes vi mangler at sende
        int n;

        while(total < *len) {
            n = send(s, buf+total, bytesleft, 0);
            if (n == -1) { break; }
            total += n;
            bytesleft -= n;
        }

        *len = total; // returner antallet af bytes sendt her

        return n==-1?-1:0; // returner -1 ved fejl, 0 ved succes
    } 

I det her eksempel er s den socket du vil sende dataene til, buf er bufferen der indeholder dataene, og len en en pointer til en int der indeholder antallet af bytes i bufferen.

Funktionen returnerer -1 ved fejl (og errno bliver stadig sat af kaldet til send().) Antallet af bytes der faktisk bliver sendt returneres i len. Dette vil være det samme antal af bytes du bad om at sende, med mindre der er en fejl. sendall() vil gøre sit bedste for at sende dataene ud, men hvis der er en fejl kommer den tilbage til dig lige med det samme.

For at færdiggøre det, er der her et eksempel på et kald til funktionen:


    char buf[10] = "Beej!";
    int len;

    len = strlen(buf);
    if (sendall(s, buf, &len) == -1) {
        perror("sendall");
        printf("We only sent %d bytes because of the error!\n", len);
    } 

Hvad sker der i modtagerens ende når en del af en pakke ankommer? Hvis pakkerne er af variabel længde, hvordan ved modtageren så hvornår en pakke ender og en anden begynder? Ja, den virkelige verden er noget træls noget. Du er sikkert nød til at indkapsle (kan du huske det fra sektionen om data indkapsling langt tilbage?) Læs lidt videre for detaljer!


6.4. Indkapsling af Data

Hvad betyder det egentlig at indkapsle data? I den simpleste form betyder det at du sætten en header på, med enten noget identifikation eller en pakkelængde, eller begge.

Hvordan bør din header se ud? Det er jo egentlig bare noget binært data som repræsenterer hvad du føler der er nødvendigt for at gøre dit projekt færdigt.

Wow. Det var noget uklart.

Okay. For eksempel. Lad os sige at du har et multibruger chatprogram som bruger SOCK_STREAMs. Når en bruger taster ("siger") noget, bliver to slags information overført til serveren: Hvad der blev sagt, og hvem der sagde det.

Så langt, så godt? "Hvad er problemet?" spørger du nok.

Problemet er, at beskederne kan være af forskellig længde. En person kaldet "tom" siger måske "Hej", og en anden person ved navn "Benjamin" siger måske "Hej, hva sa?"

Så du send()er elt dette til klienterne som det kommer ind. Din udgående datastrøm ser sådan ud:


    t o m H e j B e n j a m i n H e j   ,   h v a   s a ?

Og så frem deles. Hvordan ved en klient hvornår en besked starter og en anden stopper? Hvis du ville kunne du lave alle beskederne af samme længde og bare kalde den sendall() som vi implementerede herover. Men det spilder båndbredde! Vi vil ikke send()e 1024 bytes så "tom" bare kan sige "Hej".

Derfor indkapsler vi dataene i en lille header og pakkestruktur. BÃ¥de klienten og serveren ved hvordan de skal pakke og udpakke (nogle gange kaldet "marshal" og "unmarshal") disse data. Vi er nu igang med at definere en protokol som beskriver hvordan en klient og en server kommunikerer!

I så fald går vi ud fra at brugernavnet har en fast længde på 8 bogstaver, padded med '\0' hvis det ikke fylder nok. Og lad os gå ud fra at dataene har variabel længde, op til maks 128 bogstaver. Lad os kigge på et eksempel på en pakkestruktur vi kunne bruge i denne situation:

  1. len (1 byte, unsigned) -- Længden af pakken, inklusive det 8-byte brugernavn og chat dataene.

  2. name (8 bytes) -- brugerens navn, padded med NUL hvis det er nødvendigt.

  3. chatdata (n-bytes) -- Dataene selv. Ikke mere end 128 bytes. Længden af pakken bør beregnes som længden af disse data plus 8 (længden af feltet navn)

Hvorfor valgte jeg en grænse på 8 byte og 128 byte for felterne? Det greb jeg bare ud af luften, og gik ud fra at det var nok. Måske er 8 bytes for lidt til dine behov, så du kan lave et 30-byte name felt, eller noget andet. Det må du selv bestemme.

Ved at bruge den overstående definition, vil den første pakke bestå af den følgende information (i hex og ASCII):


      0B     74 6F 6D 00 00 00 00 00      48 65 6A
   (length)  T  o  m    (padding)         H  e  j

And the second is similar:


      14     42 65 6E 6A 61 6D 69 6E      48 65 6A 20 2C 20 68 76 61 20 73 61
   (length)  B  e  n  j  a  m  i  n       H  e  j     ,     h  v  a     s  a

(Længden er selvfølgelig gemt i Network Byte Order. I det her tilfælde er det ligegyldigt. Men i det store hele bør du gemme alle dine binære integers i Network Byte Order i dine pakker.)

Når du sender disse data bør du være på den sikre side og bruge en kommando som sendall() så du ved at alle dine data bliver sendt, selvom det tager flere kald til send() for at få det hele ud.

Når du modtager disse data skal du på samme måde lave en smule ekstra arbejde. For at være på den sikre side bør du gå ud fra at du måske modtager en delt pakke (hvis vi måske modtog "00 14 65 6E" fra Benjamin, men det var alt vi fik ved dette kald til recv()). Vi bliver nød til at kalde recv() igen og igen indtil vi har modtaget hele pakken.

Men hvordan? Vi kender antallet af bytes vi skal modtage i det hele for at pakken er komplet, siden det tal er sendt i starten af pakken. Vi ved også at den største pakkestørrelse er 1+8+128, eller 137 bytes (fordi det er sådan vi definerede pakken.)

Det du kan gøre er, at deklarere en array der er stor nok til to pakker. Dette er din arbejdsarray, hvor du rekonstruerer pakker, som de ankommer.

Hver gang du recv()er data putter du dem ind i arbejdsbufferen, og checker om pakken er komplet, altså om antallet af bytes i bufferen er større eller lig med længden givet i headeren (+1, da længden af headeren ikke indeholder længden selv.) Hvis antallet af bytes i bufferen er mindre end 1 er pakken naturligvis ikke komplet. Du er nød til at lave et specialtilfælde for dette, siden den første byte er skrald, og du ikke kan kigge på den for at få den rigtige pakkelængde.

Så snart pakken er komplet kan du gøre hvad du vil. Brug den, og fjern den fra din arbejdsbuffer.

Puha! Jonglerer du det i dit hovede? Der er endnu en lille ting: Måske har du læst videre end slutningen af den ene pakke og ind i en anden i et enkelt recv() kald. Altså, du har en arbejdsbuffer med en komplet pakke, og en del af en ikke-komplet pakke! Sikke et rod. (Men det er derfor du har gjort din arbejdsbuffer stor nok til at indeholde to pakker--i tilfælde af at det skete!)

Siden du kender længden af den første pakke fra headeren, og du har holdt styr på hvor mange af de bytes der er i arbejdsbufferen, kan du trække fra og beregne hvor mange af bytene i arbejdsbufferen der hører til den anden (ikke-komplette) pakke. Når du har håndteret den første, kan du fjerne den fra arbejdsbufferen, og flytte den partielle anden pakke ned i starten af bufferen, så den er klar til den næste recv().

(Nogle af jer læsere vil bemærke at det at flytte den partielle anden pakke til begyndelsen af arbejdsbufferen tager tid, og at programmet kan kodes så det ikke behøver dette ved at bruge en cirkulær buffer. Uheldigvis for resten af jer er en diskussion omkring cirkulære buffere ikke en del af den her artikel. Hvis du stadig er nysgerrig kan du snuppe en bog om datastrukturer og læse den.)

Jeg har aldrig sagt det var let. Ok, jeg sagde måske det var let. Og det er det også; du skal bare øve dig lidt, og snart vil det være helt naturligt. Det sværger jeg. Ved Tors Hammer!


7. Flere Referencer

Nu er du kommet så langt og du vil have mere. Hvor kan du lære mere om alt det her?


7.2. Bøger

For old-school bøger kan du prøve nogle af følgende guides. Bemærk det flotte Amazon.com logo. Alt dette skamløst kommercielle logo betyder er, at jeg får nogle penge (Amazon.com store credit, faktisk) for at sælge disse bøger gennem denne guide. Så hvis du alligevel skal til at købe en af de her bøger, hvorfor så ikke takke mig ved at starte din shoppetur vha. en af de links herunder?

Set på en anden måde, kan flere bøger til mig faktisk lede til flere guider til dig. ;-)

Unix Network Programming, volumes 1-2 by W. Richard Stevens. Published by Prentice Hall. ISBNs for volumes 1-2: 013490012X, 0130810819.

Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. ISBNs for volumes I, II, and III: 0130183806, 0139738436, 0138487146.

TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs for volumes 1, 2, and 3: 0201633469, 020163354X, 0201634953.

TCP/IP Network Administration by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 1565923227.

Advanced Programming in the UNIX Environment by W. Richard Stevens. Published by Addison Wesley. ISBN 0201563177.

Using C on the UNIX System by David A. Curry. Published by O'Reilly & Associates, Inc. ISBN 0937175234. Out of print.


7.3. Referencer på Web

PÃ¥ webbet:

BSD Sockets: A Quick And Dirty Primer (har også en masse andet info om Unix system programmeing)

The Unix Socket FAQ

Client-Server Computing

Intro to TCP/IP (gopher)

Internet Protocol Frequently Asked Questions

The Winsock FAQ


7.4. RFCer

RFCs--de usle detaljer:

RFC-768--The User Datagram Protocol (UDP)

RFC-791--The Internet Protocol (IP)

RFC-793--The Transmission Control Protocol (TCP)

RFC-854--The Telnet Protocol

RFC-951--The Bootstrap Protocol (BOOTP)

RFC-1350--The Trivial File Transfer Protocol (TFTP)


8. Spørgsmål Og Svar

Q: Hvor kan jeg få de headerfiler fra?
Q: Hvad gør jeg når bind() siger "Address already in use"?
Q: Hvordan får jeg en liste over åbne sockets på systemet??
Q: Hvordan kan jeg se routing tabellen?
Q: Hvordan kører jeg klient og serverprogrammer hvis jeg kun har een computer? Skal jeg ikke have et netværk for at skrive netværksprogrammer?
Q: Hvordan kan jeg se om fjerncomputeren har lukket forbindelsen?
Q: Hvordan implementerer jeg et "ping" program? Hvad er ICMP? Hvor kan jeg finde ud af mere om raw sockets og SOCK_RAW?
Q: Hvordan bygger jeg i Windows?
Q: Hvordan bygger jeg i Solaris/SunOS? Jeg får altid linkerfejl når jeg prøver at kompilere!
Q: Hvorfor falder select() altid ud på ved et signal?
Q: Hvordan kan jeg implementere et timeout i et kald til recv()?
Q: Hvordan krypterer eller komprimerer jeg data før jeg sender det ud gennem en socket?
Q: Hvad er det her "PF_INET" Jeg hele tiden ser? Er det relateret til AF_INET?
Q: Hvordan kan jeg skrive en server der accepterer shell kommpandoer fra en klient og eksekverer dem?
Q: Jeg sender en masse data, men når jeg recv()er, modtager den kun 536 bytes eller 1460 bytes ad gangen. Men hvis jeg kører den på min lokale maskine modtager den alle dataene på samme tid. Hvad sker der?
Q: Jeg bruger en Windows boks, og jeg har ikke adgang til systemkaldet fork() eller nogen form for struct sigaction. Hvad gør jeg?
Q: Hvordan sender jeg data sikkert med TCP/IP vha. kryptering?
Q: Jeg er bag en firewall--hvordan fortæller jeg folk udenfor firewallen min IP adresse, så de kan forbinde til min maskine?

Q: Hvor kan jeg få de headerfiler fra?

A: Hvis du ikke allerede har dem på dit system behøver du dem sikkert ikke. Check manualen for dit system. Hvis du bruger Windows behøver du kun #include <winsock.h>.

Q: Hvad gør jeg når bind() siger "Address already in use"?

A: Du skal bruge setsockopt() med SO_REUSEADDR på den lyttende socket. Kig på sektionen om bind() og sektionen om select() for eksempler .

Q: Hvordan får jeg en liste over åbne sockets på systemet??

A: Brug netstat. Se man pagen for alle detaljer, men du burde få noget godt ved bare at skrive:


$ netstat

Det eneste svære er at finde ud af hvilken socket der hører til hvilket program. :-)

Q: Hvordan kan jeg se routing tabellen?

A: Kør route (in /sbin på de fleste Linux dists) eller kommandoen netstat -r.

Q: Hvordan kører jeg klient og serverprogrammer hvis jeg kun har een computer? Skal jeg ikke have et netværk for at skrive netværksprogrammer?

A: Heldigvis for dig har stort set alle maskiner et såkaldt loopback netværks device, som er i kernen, og lader som om det er et netværkskort. (Dette er interfacet vist som "lo" i routing tabellen.)

Forestil dig at du er logget ind på en maskine ved navn "goat". Kør klienten i et vindue og serveren i et andet. Eller start serveren i baggrunden ("server &") og kør klienten i det samme vindue. Ideen med loopback devicet er at du enten kan køre client goat eller client localhost (siden "localhost" sandsynligvis er defineret i din /etc/hosts fil). Så snakker klienten med serveren uden et netværk!

Kort fortalt, er det ikke nødvendigt at ændre noget af koden for at få det til at køre på en enkelt maskine der ikke er på netværket! Hurra!

Q: Hvordan kan jeg se om fjerncomputeren har lukket forbindelsen?

A: Det kan du se fordi recv() returnerer 0.

Q: Hvordan implementerer jeg et "ping" program? Hvad er ICMP? Hvor kan jeg finde ud af mere om raw sockets og SOCK_RAW?

A: Alle de spørgsmål du har om raw sockets er besvaret i W. Richard Stevens' UNIX Network Programming bøger. Se sektionen bøger.

Q: Hvordan bygger jeg i Windows?

A: Først, slet Windows og installer Linux eller BSD. };-). Nej, faktisk kan du bare se på sektionen om hvordan man bygger i Windows i introduktionen.

Q: Hvordan bygger jeg i Solaris/SunOS? Jeg får altid linkerfejl når jeg prøver at kompilere!

A: Linkerfejlede sker fordi Sun's bokse ikke automatisk kompilerer socket libraries. Kig på sektionen om at bygge på Solaris/SunOS i introduktionen for at få et eksempel.

Q: Hvorfor falder select() altid ud på ved et signal?

A: Signaler har det med at få blokerede systemkald til at returnere -1 med errno sat til EINTR. Når du bruger en signal handler med sigaction() kan du sætte flaget SA_RESTART, som skal genstarte systemkaldet efter det var afbrudt.

Naturligvis virker dette ikke altid.

Min yndlingsløsning bruger et goto statement. Du ved at dette irriterer professorer afsindigt, så brug det!


select_restart:
    if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) {
        if (errno == EINTR) {
            // et eller andet signal afbroed os, s&aring; genstart
            goto select_restart;
        }
        // haandter en rigtig fejl her:
        perror("select");
    } 

Selvfølgelig behøver du ikke bruge goto i det her tilfælde. Du kan bruge andre måder at kontrollere det på. Men jeg synes at goto løsningen faktisk er flottere.

Q: Hvordan kan jeg implementere et timeout i et kald til recv()?

A: Brug select()! Det muliggør at du kan give en timeout parameter til socket descriptors som du vil læse fra. Du kan også wrappe hele funktionaliteten i en enkelt funktion, som her::


#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>

int recvtimeout(int s, char *buf, int len, int timeout)
{
    fd_set fds;
    int n;
    struct timeval tv;

    // Saet file descriptor saettet op
    FD_ZERO(&fds);
    FD_SET(s, &fds);

    // Saet struct timeval op for timeouten
    tv.tv_sec = timeout;
    tv.tv_usec = 0;

    // vent indtil der er timeout, eller der er modtaget noget data
    n = select(s+1, &fds, NULL, NULL, &tv);
    if (n == 0) return -2; // timeout!
    if (n == -1) return -1; // fejl

    // Nu er der data, saa lav en normal recv()
    return recv(s, buf, len, 0);
}

// Eksempel paa et kald til recvtimeout():
    .
    .
    n = recvtimeout(s, buf, sizeof(buf), 10); // 10 sekunder timeout

    if (n == -1) {
        // der opstod en fejl
        perror("recvtimeout");
    }
    else if (n == -2) {
        // der opstod en timeout
    } else {
        // der kom noget data ind i buf
    }
    .
    . 

Bemærk at recvtimeout() returnerer -2 hvis der var en timeout. Hvorfor ikke returnere 0? Hvis du husker, er en return værdi på 0 ved et kald til recv() det samme som at fjerncomputeren har lukket forbindelsen. Så den return værdi er allerede brugt, og -1 betyder "fejl", så jeg valgte -2 som min timeout indikator.

Q: Hvordan krypterer eller komprimerer jeg data før jeg sender det ud gennem en socket?

A: En let måde at lave kryptering er at bruge SSL (secure sockets layer), men det hører ikke til i denne guide.

Men hvis du vil implementere din egen komprimerings eller krypteringssystem, er det bare et spørgsmål om at tænke på at dine data skal igennem en række skridt mellem begge ender. Hvert skridt ændrer dataene på en eller anden måde.

  1. serveren læser data fra en fil (eller noget andet)

  2. serveren krypterer dataene (det her tilføjer du)

  3. serveren send()er de krypterede data

Og den anden vej:

  1. klienten recv()er de krypterede data

  2. klienten dekrypterer dataene (det her tilføjer du)

  3. klienten skriver data til en fil (eller noget andet)

Du kan også komprimere det samme sted hvor du krypterede/dekrypterede. Eller du kunne gøre begge ting! Du skal bare huske at komprimere før du krypterer. :)

Så længe klienten gør det som serveren gør, bare omvendt, så er dataene fine, og så er det ligemeget hvor mange skridt du tilføjer.

Så det eneste du behøver for at bruge min kode er, at finde et sted mellem der hvor dataene er læst, og dataene bliver sendt (vha. send()) over netværket, og sætte noget kode ind som krypterer.

Q: Hvad er det her "PF_INET" Jeg hele tiden ser? Er det relateret til AF_INET?

A: Ja, det er det. Se sektionen om socket().

Q: Hvordan kan jeg skrive en server der accepterer shell kommpandoer fra en klient og eksekverer dem?

A: For at gøre det simpelt, siger vi at klienten connect()er, send()er, og close()er forbindelsen (så der ikke sker nogle andre systemkald uden at klienten forbinder igen.)

Processen klienten følger er denne:

  1. connect() til serveren

  2. send("/sbin/ls > /tmp/client.out")

  3. close() forbindelsen

Imens håndterer serveren data, og eksekverer det:

  1. accept()er forbindelsen fra klienten

  2. recv(str) kommandostrengen

  3. close() forbindelsen

  4. system(str) for at køre kommandoen

Forsigtig! At have en server der kører alt hvad klienten siger, er det samme som at give folk remote shell adgang, og folk kan gøre ting ved din konto når de forbinder til serveren. For eksempel hvis klienten sender "rm -rf ~", så sletter den alt på din konto!

Så du bliver klogere, og du går så at klienten kun kan bruge en lille samling kommandoer, som du ved er sikre, som programmet foobar:


    if (!strcmp(str, "foobar")) {
        sprintf(sysstr, "%s > /tmp/server.out", str);
        system(sysstr);
    } 

Men du er desværre stadig usikker: Hvad hvis klienten indtaster "foobar; rm -rf ~"? Det sikreste er at skrive en lille rutine, som sætter et escape ("\") bogstav foran alle ikke-alfanumeriske bogstaver (måske også space) i argumenterne til kommandoen.

Som du kan se er sikkerhed en temmelig stor ting når serveren begynder at eksekvere ting klienten sender.

Q: Jeg sender en masse data, men når jeg recv()er, modtager den kun 536 bytes eller 1460 bytes ad gangen. Men hvis jeg kører den på min lokale maskine modtager den alle dataene på samme tid. Hvad sker der?

A: Du når MTUen--den maksimale størrelse det fysiske medie kan håndtere. På en lokal maskine bruger du loopback devicet, som kan håndtere 8K eller mere uden problemer. Men på ethernet, som kun kan håndtere 1500 bytes med en header, så når du denne grænse. Over et modem med 576 MTU (igen med header) når du en endnu lavere grænse.

Du skal for det første sikre dig, at alle dataene bliver sendt. (Se sendall() funktionen.) Når du er sikker på du gør det, så skal du kalde recv() i en løkke indtil alle dine data er læst.

Læs sektionen Indkapsling af Data for detaljer om hvordan man modtager komplette pakker vha. flere kald til recv().

Q: Jeg bruger en Windows boks, og jeg har ikke adgang til systemkaldet fork() eller nogen form for struct sigaction. Hvad gør jeg?

A: Hvis de er noget sted, så er de i de POSIX libraries, som måske hørte med til din compiler. Siden jeg ikke har en Windows boks, så kan jeg ikke give dig svaret, men jeg synes at kunne huske at Microsoft har et POSIX kompabilitetslag, og det er der fork() burde være. (Og måske endda sigaction.)

Søg i den hjælp der kom med VC++ efter "fork" eller "POSIX", og se om det giver dig nogen hints.

Hvis det ikke virker, så drop fork()/sigaction og erstat det med Win32 versionen: CreateProcess(). Jeg ved ikke hvordan man bruger CreateProcess()--den tager en fantasillion argumenter, men det burde være dækket af den dokumentation der kom med VC++.

Q: Hvordan sender jeg data sikkert med TCP/IP vha. kryptering?

A: Tag et kig på OpenSSL projektet.

Q: Jeg er bag en firewall--hvordan fortæller jeg folk udenfor firewallen min IP adresse, så de kan forbinde til min maskine?

A: Uheldigvis er formålet med en firewall at forhindre folk udenfor firewallen fra at forbinde til maskiner indenfor firewallen, så hvis man tillader det, så er det egentlig et brud på sikkerheden.

Dette betyder ikke at alt er tabt. En ting du stadig ofte kan, er at connect()e gennem firewallen, hvis den bruger en form for masquerading eller NAT, eller noget i den stil. Så skal du bare designe dine programmer, så det altid er dem der starten en forbindelse, så er det fint.

Hvis ikke det er tilfredsstillende kan du bede dine systemadministratore og at lave et hul i firewallen, så folk kan forbinde til dig. Firewallen kan forwarde til dig enten gennem dens NAT software, eller gennem en proxy server, eller noget lignende.

Vær opmærksom på, at et hul i firewallen ikke er noget man skal kimse med. Du skal sikre dig at du ikke giver slemme folk adgang til det interne netværk; hvis du er en begynder er det en del sværere at lave sikker software end man skulle tro.

Lad være med at gøre din systemadministrator sur på mig. ;-)


9. Disclaimer og et Kald efter Hjælp

Det var det. Forhåbentlig er noget af informationen heri rimelig præcis, og jeg håber så sandelig at der ikke er nogle store fejl. Men selvfølgelig er der altid det.

Så lad dig være advaret! Jeg er ked af af hvis nogen fejl heri har voldt dig nogen kvaler, men du kan ikke holde mig skyldig. I lovmæssig forstand står jeg ikke bag nogen af de ord der står i dette dokument. Det hele kunne være helt of fuldstændig forkert!

Men det er det sikkert ikke. Fordi, jeg har brugt mange timer på at arbejde med det her, og jeg har implementeret adskillige TCP/IP netværksprogrammer i mit arbejde, lavet multiplayerspil osv. Men jeg er ingen socket gud; jeg er bare en eller anden gut.

Forresten, hvis nogen har noget konstruktiv (eller destruktiv) kritik omkring dette her dokument, så kan de sende en mail til , så vil jeg prøve at fikse det.

Såfremt du undrer dig over hvorfor jeg har lavet dette, så gjorde jeg det for pengenes skyld. He! Nej, faktisk gjorde jeg det fordi en masse folk har spurgt mig om socket relaterede spørgsmål, og når jeg fortalte dem at jeg overvejede at lave en socket side, så sagde de "Sejt!". Desuden så føler jeg at al denne information jeg har lært ville gå til spilde, hvis jeg ikke kunne dele den med andre. Internettet viser sig bare at være det perfekte distributionsmedie. Jeg opfordrer andre til at udgive lignende information så snart det er muligt.

Nok om det her--tilbage til kodningen!