Uporaba Internetnih Vtičnic
Brian “Beej Jorgensen” Hall
beej@beej.us
Različica 3.0.21
8 Junija 2016
Copyright © 2015 Brian “Beej Jorgensen” Hall
Vsebina
1. Uvod
1.1. Občinstvo
1.2. Platforma in prevajalnik
1.3. Uradna stran in knjige za prodajo
1.4. Opomba za programerje Solaris/SunOS
1.5. Opomba za programerje programa Windows
1.6. Politika E-pošte
1.7. Zrcaljenje
1.8. Opomba za prevajalce
1.9. Avtorske pravice in distribucija
2. Kaj je vtičnica?
2.1. Dve vrsti internetnih vtičnic
2.2. Nizka stopnja nesmisla in teorija omrežja
3. Naslovi IP, struct in obdelava podatkov
3.1. IP naslovi, različice 4 in 6
3.2. Byte Order
3.3. structs
3.4. Naslov IP, del Deux
5. Sistemski Klici ali Bust
5.1. getaddrinfo () – Pripravite se na začetek!
5.2. socket () – Pridobite opis datoteke!
5.3. bind () – V kakšnem pristanišču sem?
5.4. connect () – Hej, ti!
5.5. listen () – Ali me bo kdo poklical?
5.6. accept () – “Hvala za klic pristanišča 3490.”
5.7. send () in recv () – Pogovorite se z mano!
5.8. sendto () in recvfrom () – Pogovorite se z menoj, slog DGRAM
5.9. close () in shutdown () – Iztegni moj obraz!
5.10. getpeername () – Kdo ste?
5.11. gethostname () – Kdo sem jaz?
6. Ozadje odjemalca-strežnik
6.1. Preprost tok strežnika
6.2. Preprost odjemalec toka
6.3. Datagramove vtičnice
7. Nekoliko napredne tehnike
7.1. Blokiranje
7.2. select () – Sinhrono V/I multipleksiranje
7.3. Ravnanje z delnim pošiljanjem ()
7.4. Serializacija – Kako pakirati podatke
7.5. Sindrom enkapsulacije podatkov
7.6. Prenosni paketi – Pozdravljeni, Svet!
9. Man strani
9.1. accept()
9.2. bind()
9.3. connect()
9.4. close()
9.5. getaddrinfo(), freeaddrinfo(), gai_strerror()
9.6. gethostname()
9.7. gethostbyname(), gethostbyaddr()
9.8. getnameinfo()
9.9. getpeername()
9.10. errno
9.11. fcntl()
9.12. htons(), htonl(), ntohs(), ntohl()
9.13. inet_ntoa(), inet_aton(), inet_addr
9.14. inet_ntop(), inet_pton()
9.15. listen()
9.16. perror(), strerror()
9.17. poll()
9.18. recv(), recvfrom()
9.19. select()
9.20. setsockopt(), getsockopt()
9.21. send(), sendto()
9.22. shutdown()
9.23. socket()
9.24. struct sockaddr and pals
1. Uvod
Zdravo! Programiranje vtičnic vas je spustilo? Ali je to nekaj stvari precej težavno, da bi ugotovili, na strani človeka? Želite narediti kul programiranje po internetu, vendar nimate časa, da bi se prebudil s kruhom struk, ki bi poskušali ugotoviti, ali morate klicati bind() pred connect() itd. Itd.
No, ugani kaj! Sem že naredil to grdo posel, in umiraj, da podatke delim z vsemi! Prišli ste na pravo mesto. Ta dokument mora dati povprečnemu kompetentnemu programerju C robu, ki ga potrebuje, da se opne na ta šum v omrežju.
In preverite: končno sem ušel v prihodnost (prav tako v časovnem roku!) In posodobili vodnik za IPv6! Uživajte!
1.1. Občinstvo
Ta dokument je napisan kot vadnica, ne pa popolna referenca. To je verjetno v najboljšem primeru, ko ga preberejo posamezniki, ki šele začenjajo s programiranjem vtičnic in iščejo oporo. Vsekakor ni popolna in popolna navodila za programiranje vtičnic.
Upajmo, da bo dovolj čisto, če bodo tiste človeške strani začele smiselno …
1.2. Platforma in prevajalnik
Koda, vsebovana v tem dokumentu, je bila prevedena na Linux PC z Gnujevim gcc prevajalnikom. Vendar bi se moral graditi na skoraj vsaki platformi, ki uporablja gcc. Seveda to ne velja, če programirate v operacijskem sistemu Windows – glejte poglavje o programiranju programa Windows spodaj.
1.3. Uradna domača stran in knjige za prodajo
Ta uradna lokacija tega dokumenta je http://beej.us/guide/bgnet/. Tam boste našli tudi kodo in prevod vodiča v različne jezike.
Če želite kupiti lepo povezane tiskane kopije (nekateri jih imenujemo “knjige”), obiščite http://beej.us/guide/url/bgbuy. Zahvaljujem se nakupu, ker mi pomaga vzdrževati življenjski stil pisanja dokumentov!
1.4. Opomba za programerje Solaris/SunOS
Pri sestavljanju za Solaris ali SunOS morate določiti nekaj dodatnih stikal ukazne vrstice za povezovanje v ustrezne knjižnice. Če želite to narediti, preprosto dodajte “-lnsl -socket -lresolv” na koncu ukaza za sestavljanje, tako:
$ cc -o server server.c -lnsl -lsocket -lresolv
Če še vedno dobite napake, poskusite dodati še “-lxnet” na koncu ukazne vrstice. Ne vem, kaj to počne točno, toda nekateri ljudje to potrebujejo.
Drug kraj, kjer boste morda našli težave, je v klicu na setsockopt (). Prototip je drugačen od tistega na mojem Linuxu, zato namesto:
int yes=1;
Vnesite to:
char yes='1';
Ker nimam polja Sun, nisem preizkusil nobenega od zgornjih informacij – ravno to so mi ljudje povedali po e-pošti.
1.5. Opomba za programerje programa Windows
Na tej točki v priročniku, zgodovinsko, sem naredil nekaj vreč v sistemu Windows, preprosto zaradi dejstva, da mi tega ni všeč. Ampak res bi rad pošten in vam povem, da Windows ima ogromno namestitveno bazo in je očitno popolnoma fini operacijski sistem.
Pravijo, da odsotnost srce raste, in v tem primeru verjamem, da je to res. (Ali morda je to starost.) Toda, kar lahko rečem, je, da po desetletju neuporabe Microsoftovih OS-jev za moje osebno delo sem veliko srečnejši! Kot taka lahko sedim in varno rečem: “Seveda, prosim uporabite Windows!” … Ok, ja, da bi zamažil zobe, da bi to rekel.
Zato vas še vedno spodbujam, da preizkusite Linux, BSD ali nekaj okusa Unixa.
Toda ljudje všeč, kar jim je všeč, in ti Windows ljudje bodo z veseljem vedeli, da te informacije na splošno veljajo za vas, z nekaj manjšimi spremembami, če sploh.
Ena kul stvar, ki jo lahko storite, je namestiti Cygwin, ki je zbirka orodij Unixa za Windows. Slišal sem na vinski trti, ki omogoča, da vse te programe zbirajo nespremenjene.
Toda nekateri od vas bi morda želeli narediti stvari Pure Windows Way. To je zelo težko od tebe, in to je tisto, kar morate storiti: zmanjkuje in takoj dobite Unix! Ne, ne, jaz se šalim. V teh dneh bi moral biti prijazen do okolja …
To je tisto, kar boste morali storiti (razen če namestite Cygwin!): Najprej prezrite skoraj vse datoteke glave sistema, ki jih tukaj omenjam. Vse, kar morate vključiti, je:
#include <winsock.h>
Počakajte! Prav tako morate poklicati WSAStartup (), preden naredite karkoli drugega s knjižnico vtičnic. Koda za to izgleda nekako takole:
#include <winsock.h> { WSADATA wsaData; // if this doesn't work //WSAData wsaData; // then try this instead // MAKEWORD(1,1) for Winsock 1.1, MAKEWORD(2,0) for Winsock 2.0: if (WSAStartup(MAKEWORD(1,1), &wsaData) != 0) { fprintf(stderr, "WSAStartup failed.\n"); exit(1); }
Prav tako morate svojemu prevajalcu povedati, da se poveže v knjižnico Winsock, ki se običajno imenuje wsock32.lib ali winsock32.lib ali ws2_32.lib za Winsock 2.0. Pod VC ++ lahko to storite v meniju Projekt, pod Nastavitvami…. Kliknite jeziček Povezava in poiščite polje z naslovom »Objekt / knjižnice«. Dodajte »wsock32.lib« (ali katerikoli lib je vaša prednost) na ta seznam.
Ali tako slišim.
Nazadnje morate poklicati WSACleanup (), ko ste končali s knjižnico vtičnic. Za podrobnosti si oglejte spletno pomoč.
Ko to storite, bi morali našteti primeri v tej vadnici na splošno uporabiti z nekaj izjemami. Za eno stvar ne morete uporabiti zapreti (), da zaprete vtičnico – namesto tega morate uporabiti zaprto vtičnico (). Izberi tudi () deluje le z deskriptorji sock, ne z deskriptorji datotek (na primer 0 za stdin).
Obstaja tudi razred vtičnice, ki ga lahko uporabite, CSocket. Preverite svoje strani za pomoč pri prevajalnikih za več informacij.
Če želite več informacij o Winsock, preberite odgovore na Winsock FAQ in pojdite od tam.
Na koncu slišim, da Windows nima sistemskega klica fork (), ki se na žalost uporablja v nekaterih mojih primerih. Morda se morate povezati v knjižnici POSIX ali podobno, da jo lahko delate ali pa uporabite CreateProcess (). fork () ne upošteva nobenih argumentov in CreateProcess () traja približno 48 milijard argumentov. Če niste odvisni od tega, je CreateThread () malo preprosteje prebrati… žal diskusija o večnitnem tisku presega obseg tega dokumenta. O takih pogovorih se lahko pogovarjam, veš!
1.6. Politika e-pošte
Na splošno vam lahko pomagam z e-poštnimi vprašanji, zato se lahko prijavite, vendar ne morem odgovoriti. Vodim precej zasedeno življenje in časi, ko ne morem odgovoriti na vprašanje, ki ga imate. Ko je tako, ponavadi samo izbrišem sporočilo. Nič osebnega; Nikoli ne bom imela dovolj časa za natančen odgovor, ki ga potrebujete.
Praviloma je bolj zapleteno vprašanje manj verjetno, da bi se odzval. Če lahko zožite svoje vprašanje, preden ga pošljete in se prepričajte, da vključite vse pomembne informacije (kot so platforma, prevajalnik, sporočila o napakah, ki ste jih dobili, in kaj drugega, za katere menite, da mi bodo morda pomagale odpraviti težave), boste veliko bolj verjetno dobili Odgovor. Za več kazalcev preberite dokument ESR, kako postavljati vprašanja na pametno pot.
Če ne dobite odgovora, poskusite poiskati odgovor in če je še vedno neizvedljiv, potem mi znova napišite informacije, ki ste jih našli, in upam, da bo dovolj, da vam pomagam.
Zdaj, ko sem vam pomanjkanja pisanja in ne pisanja, bi vam rad povedal, da v celoti cenim vse pohvale, ki jih je vodnik prejela v preteklih letih. To je resnično vzpodbujanje morale in me veseli, da slišim, da se uporablja za dobro! Hvala vam!
1.7. Zrcaljenje
To spletno mesto ste bolj kot dobrodošli, javno ali zasebno. Če javno ogledate spletno stran in želite, da se povezujem z njo z glavne strani, mi vrnite črto na beej@beej.us.
1.8. Opomba za prevajalce
Če želite prevedeti vodnik v drug jezik, napišite me na beej@beej.us in na glavno stran bom povezal vaš prevod. V prevod lahko dodate svoje ime in kontaktne podatke.
Upoštevajte omejitve licenc v razdelku Avtorske pravice in razdelek, spodaj.
Če želite, da gosti prevod, samo vprašajte. Prav tako se bom povezal z njim, če ga želite gostiti; v vsakem primeru je v redu.
1.9. Avtorske pravice in distribucija
Beejov vodnik za omrežno programiranje je Copyright © 2015 Brian “Beej Jorgensen”.
Določene izjeme za izvorno kodo in prevode, spodaj, je to delo pod licenco Creative Commons Priznanje avtorstva-Nekomercialno in brez izvedenih del 3.0. Če si želite ogledati kopijo te licence, obiščite http://creativecommons.org/licenses/by-nc-nd/3.0/ ali pošljite pismo Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, ZDA .
Ena posebna izjema od dela brez izvedenih del licence je naslednja: ta priročnik je lahko prosto preveden v katerikoli jezik, pod pogojem, da je prevod točen in da je priročnik ponatisnjen v celoti. Enake licenčne omejitve veljajo za prevod v zvezi s prvotnim vodnikom. Prevod lahko vključuje tudi ime in kontaktne podatke za prevajalca.
Izvorna koda C, predstavljena v tem dokumentu, se odobri v javni lasti in je popolnoma brez kakršnekoli omejitve licence.
Izobraževalcem se svobodno priporoča, da priporočijo ali pošljejo izvode tega priročnika svojim učencem.
Za več informacij se obrnite na beej@beej.us.
2. Kaj je vtičnica?
Ou, ves čas slišite govor o “vtičnicah”, in morda se sprašujete, kaj so točno. No, to so: način govoriti z drugimi programi z uporabo standardnih deskriptorjev datotek Unixa.
Kaj?
Ok – morda ste že slišali kakšno Unix stanje hacker, “Jeez, vse v Unixu je datoteka!” O tem se je morda pogovarjala dejstvo, da ko programi Unixa izvajajo kakršne koli I/O, to storijo z branjem ali pisanjem v deskriptorju datoteke. Descriptor datoteke je preprosto celo število, povezano z odprto datoteko. Toda (in tukaj je ulov), lahko ta datoteka omrežna povezava, FIFO, cev, terminal, resnična datoteka na disku ali skoraj nič drugega. Vse v Unixu je datoteka! Torej, ko želite komunicirati z drugim programom prek interneta, boste to storili prek deskriptorja datoteke, bolje ji verjite.
“Kje dobim opis datoteke za omrežno komunikacijo, gospod Smarty-Pants?” je verjetno zadnje vprašanje v mislih zdaj, vendar bom vseeno odgovoril: Kličete se v sistemski rutini socket (). Vrne opisni list vtičnice in komunicira prek njega s posebnimi klicnimi številkami za send() in recv() (man send, man recv).
“Ampak, hej!” Morda ste pravkar vznemirljivi. “Če gre za deskriptor datotek, zakaj v imenu Neptuna ne morem samo uporabljati običajnih read () in write () klicev za komunikacijo preko vtičnice?” Kratek odgovor je: “Lahko!” Daljši odgovor je: “Lahko, vendar send() in recv() ponuja veliko večji nadzor nad prenosom podatkov.”
Kaj je naslednje? Kaj pa to: obstajajo vse vrste vtičnic. Obstajajo DARPA internetni naslovi (internetne vtičnice), imena poti na lokalnem vozlišču (Unix Sockets), CCITT X.25 naslovi (X.25 vtičnice, ki jih lahko varno prezrete) in verjetno še veliko drugih, odvisno od tega, kateri okus Unixa uporabljate. Ta dokument obravnava samo prvo: internetne vtičnice.
2.1. Dve vrsti internetnih vtičnic
Kaj je to? Obstajata dve vrsti internetnih vtičnic? Ja. No, ne. Lažem. Obstaja več, vendar nisem hotel prestrašiti. Tukaj bom govoril samo o dveh vrstah. Razen tega stavka, kjer vam bom povedal, da so “Raw Sockets” tudi zelo močni in jih morate pogledati.
V redu, že. Kakšna sta dva tipa? Ena je “Stream Sockets”; Druga pa “Datagram Sockets”, ki se lahko v nadaljevanju imenujejo “SOCK_STREAM” in “SOCK_DGRAM”. Datagramove vtičnice se včasih imenujejo “priključne vtičnice”. (Čeprav jih lahko povežete () ‘d, če si res želite. Glejte connect() spodaj.)
Sokovi za pretok so zanesljivi dvosmerni povezani komunikacijski tokovi. Če v vtičnico izberete dva predmeta v vrstnem redu “1, 2″, bodo prišli v vrstnem redu “1, 2″ na nasprotni strani. Prav tako bodo brez napak. Tako sem prepričan, pravzaprav bodo brez napak, da bom samo prstom ušes in ušel la la la la, če kdorkoli poskuša zahtevati drugače.
Kaj uporablja vtičnice za pretok? No, morda ste že slišali za aplikacijo telnet, ja? Uporablja tokovne vtičnice. Vsi znaki, ki jih vnesete, morajo prispeti v isti vrstici, ki jih vnesete, kajne? Spletni brskalniki uporabljajo tudi protokol HTTP, ki za iskanje strani uporablja slikovne vtiče. Dejansko, če telnet pošljete na spletno mesto v pristanišču 80 in vnesete »GET / HTTP/1.0« in dvakrat pritisnete RETURN, bo HTML vrnil na vas!
Kako vtičnice za tokove dosežejo to visoko raven prenosa podatkov? Uporabljajo protokol, imenovan “Protokol za nadzor prenosa”, sicer znan kot “TCP” (glej RFC 793 za zelo podrobne informacije o TCP-ju). TCP poskrbi, da bodo vaši podatki zaporedoma zaprti in brez napak. Morda ste že slišali »TCP« kot boljšo polovico »TCP/IP«, kjer »IP« pomeni »internetni protokol« (glej RFC 791.). IP se ukvarja predvsem z internetnim usmerjanjem in na splošno ni odgovoren za celovitost podatkov.
Kul. Kaj pa vtičnice Datagram? Zakaj se imenujejo brez povezave? Kakšen je dogovor, tukaj? Zakaj so nezanesljivi? No, tukaj je nekaj dejstev: če pošljete datagram, lahko pride. Morda ne bo narobe. Če pride, bodo podatki v paketu brez napak.
Datagramove vtičnice uporabljajo tudi IP za usmerjanje, vendar ne uporabljajo TCP; Uporabljajo “protokol User Datagram” ali “UDP” (glej RFC 768.)
Zakaj so brez povezave? No, v bistvu, to je zato, ker vam ni treba vzdrževati odprto povezavo, kot delate s vtičnicami za pretok. Samo zgradite paket, na njej udarite glavo glave IP-ja s podatki o destinaciji in ga pošljite. Povezava ni potrebna. Običajno se uporabljajo, kadar stack TCP ni na voljo ali ko nekaj padajočih paketov tukaj in tam ne pomeni konca vesolja. Primeri aplikacij: tftp (trivialni protokol prenosa datotek, manjši brat na FTP), dhcpcd (odjemalec DHCP), igre z več igralci, pretakanje zvoka, videokonference itd.
“Počakajte trenutek!” tftp in dhcpcd se uporabljajo za prenos binarnih aplikacij z enega gostitelja na drugega! Podatkov ni mogoče izgubiti, če pričakujete, da bo aplikacija delovala, ko bo prišla! Kakšna je temna magija?”
No, moj človeški prijatelj, tftp in podobni programi imajo svoj protokol na vrhu UDP-ja. Na primer, protokol tftp pravi, da mora prejemnik za vsak paket, ki ga pošlje, pošiljati paket, ki pravi: “Imam ga!” (Paket “ACK”.) Če pošiljatelj prvotnega paketa ne dobi odgovora, recimo, pet sekund, bo paket preneslo, dokler končno ne dobi ACK. Ta potrditveni postopek je zelo pomemben pri izvajanju zanesljivih aplikacij SOCK_DGRAM.
Za nezanesljive aplikacije, kot so igre, avdio ali video, preprosto ignorirate padajoče pakete ali pa jih poskusite pametno nadomestiti. (Quake igralci bodo vedeli, kakšen učinek ima ta učinek s tehničnim izrazom: prekleto lag. Beseda “prekleti” v tem primeru predstavlja kakršno koli izredno napeto izgovor.)
Zakaj bi uporabili nezanesljiv osnovni protokol? Dva razloga: hitrost in hitrost. To je hitreje, da se ogenj in pozabite, kot je slediti temu, kar je prišlo varno in se prepričajte, da je v redu, in vse to. Če pošiljate klepetna sporočila, je TCP odličen; Če pošiljate 40 lokacijskih posodobitev na sekundo igralcev na svetu, morda ni tako pomembno, če se eden ali dva spustijo, UDP pa je dobra izbira.
2.2. Nizka stopnja nesmisel in omrežna teorija
Ker sem ravnokar omenil strjevanje protokolov, je čas, da se pogovorimo o tem, kako omrežja res delujejo, in pokazati nekaj primerov, kako so izdelani paketi SOCK_DGRAM. Praktično verjetno lahko preskočite ta razdelek. Vendar je dobro ozadje.
Inkapsulacija podatkov.
Hej, otroci, čas je, da se naučimo o Data Encapsulation! To je zelo pomembno. To je tako pomembno, da se lahko samo naučite o tem, če se obrnite na tečaj omrežja tukaj v Chico State ;-). V bistvu piše: paket je rojen, paket je zavit (“enkapsuliran”) v glavi (in redko noga) s prvim protokolom (recimo, TFTP protokol), potem pa celotna stvar (vključen je TFTP glava) je znova zaokrožena z naslednjim protokolom (recimo, UDP), nato pa z naslednjim (IP), nato pa s končnim protokolom na strojni (fizični) sloj (recimo, Ethernet).
Kadar drug računalnik prejme paket, strojna oprema preusmeri glavo ethernet, jedro prekriva naslove IP in UDP, program TFTP preusmeri glavo TFTP in končno vsebuje podatke.
Sedaj lahko končno govorim o zloglasnem modelu Layered Network (aka “ISO/OSI”). Ta mrežni model opisuje sistem omrežnih funkcij, ki imajo veliko prednosti pred drugimi modeli. Na primer, lahko napišete programe, ki so popolnoma enaki brez skrbi, kako se podatki fizično prenašajo (serijski, tanki Ethernet, AUI, karkoli), ker programi na nižjih ravneh obravnavajo to za vas. Dejanska omrežna strojna oprema in topologija sta pregledna za programerja vtičnic.
Brez kakršnih koli nadaljnjih ado, bom predstavil plasti celotnega modela. Zapomni si to za izpite razreda omrežja:
Fizični sloj je strojna oprema (serijska, ethernet, itd.). Aplikacijski sloj je skoraj tako daleč od fizičnega sloja, kot si lahko predstavljate – to je kraj, kjer uporabniki interakcijo z omrežjem.
Zdaj je ta model tako splošen, da ga verjetno lahko uporabite kot vodnik za popravilo avtomobilov, če si res želel. Večplasten model, ki je bolj skladen z Unixom, je lahko:
V tem trenutku lahko verjetno vidite, kako te plasti ustrezajo inkapsulaciji prvotnih podatkov.
Oglejte si, koliko dela pri izdelavi preprostega paketa? Jeez! V glave paketov morate vnašati tudi “cat“! Samo šalim se. Vse, kar morate storiti za vtičnice za pretok, je pošiljanje send(). Vse, kar morate storiti za vtičnike datagram, je pakiranje v paketu po metodi, ki jo izberete, in ga sendto(). Jedro gradi prometni sloj in internetni sloj za vas in strojna oprema omogoča dostop do omrežnega dostopa. Ah, sodobna tehnologija.
Torej konča našo kratko prepletanje v teorijo omrežja. O, da, pozabil sem ti povedati vse, kar želim povedati o poti: nič! Tako je, sploh ne bom govoril o tem. Usmerjevalnik usmerja paket v naslov IP, se posvetuje z usmerjevalno tabelo, blah blah blah. Preverite IP RFC, če vam res skrbi. Če se o tem ne boste nikoli naučili, boste živeli.
3. IP-naslovi, structs in obdelava podatkov
Tukaj je del igre, kjer se pogovorimo o kodi za spremembo.
Najprej pa pogovorimo o več ne-kode! Juhu! Najprej bi rad govoril o IP naslovih in pristaniščih le za tad, zato smo to razvrstili. Nato bomo govorili o tem, kako API vtičnic shrani in manipulira z naslovi IP in drugimi podatki.
3.1. IP naslovi, različice 4 in 6
V dobrih starih časih, ko se je Ben Kenobi še vedno imenoval Obi Wan Kenobi, je bil čudovit mrežni sistem, imenovan Internet Protocol Version 4, imenovana tudi IPv4. Imela so naslove, sestavljene iz štirih bajtov (A.K.A., štiri “okteti”) in so bili pogosto zapisani v obliki “pike in številke”, tako: 192.0.2.111.
Verjetno ste jo videli.
Pravzaprav, od tega pisanja praktično vsako spletno mesto na internetu uporablja IPv4.
Vsi, vključno z Obi Wanom, so bili veseli. Stvari so bile odlične, dokler nekateri naysayer z imenom Vint Cerf niso vsi opozorili, da nam bodo zmanjkalo naslovov IPv4!
(Poleg opozoril vsakogar o Prihajajočem IPv4 Apocalypse Of Doom In Gloom, Vint Cerf je tudi znana po tem, da je oče interneta. Torej, resnično nisem sposoben drugič presoditi njegove sodbe.)
Zmanjkuje naslovov? Kako je to mogoče? Mislim, v naslovu IPv4 je 32-bitni IP-naslov. Imamo resnično milijarde računalnikov tam zunaj?
Ja.
Tudi v začetku, ko je bilo le nekaj računalnikov in vsi mislili, da je bil milijarda nemogoče veliko število, so bile nekatere velike organizacije velikodušno dodeljene milijone naslovov IP za lastno uporabo. (Kot so Xerox, MIT, Ford, HP, IBM, GE, AT & T in nekaj majhnih podjetij, imenovanih Apple, da navedemo nekaj.)
Dejansko bi, če ne bi bilo več ukrepov za zaustavitev, že zdavnaj zmanjkalo.
Toda zdaj živimo v dobi, ko govorimo o vsakem človeku, ki ima naslov IP, vsak računalnik, vsak kalkulator, vsak telefon, vsak parkirni meter in (zakaj ne) vsak psiček.
Tako se je rodil IPv6. Ker je Vint Cerf verjetno nesmrten (čeprav bi moral njegova fizična oblika preiti, nebesa prepovedujejo, verjetno že obstaja kot nekakšen hiper-inteligentni program ELIZA v globinah interneta2), nihče ne želi, da bi ga slišal ponovno spet “sem ti to rekel”, če v naslednji različici internetnega protokola nimamo dovolj naslovov.
Kaj vam to predlaga?
Da potrebujemo veliko več naslovov. Da ne potrebujemo le dvakrat toliko naslovov, ne milijarde krat več, ne tisoč milijard večkrat, vendar 79 milijonov milijard dolarjev toliko možnih naslovov! To jim bo pokazalo!
Pravite: “Beej, ali je to res? Imam vse razloge, zakaj ne verjamem velikih številk.” No, razlika med 32 bitov in 128 bitov morda ne zveni kot veliko; To je samo 96 več bitov, kajne? Vendar ne pozabite, da govorimo o pooblastilih tukaj: 32 bitov predstavlja približno 4 milijarde številk (232), medtem ko 128 bitov predstavlja okoli 340 milijard trilijonov trilijonovih številk (za realno, 2128). To je kot milijon IPv4 Internets za vsako posamezno zvezdo v vesolju.
Pozabite tudi na to piko in številke lookup IPv4, Zdaj imamo heksadecimalno predstavitev, pri čemer je vsak dvobajtni del, ki ga ločuje debelo črevo, takole: 2001: 0db8: c9d2: aee5: 73e3: 934a: a5ae: 9551.
To ni vse! Veliko krat, boste imeli IP naslov z veliko ničle v njem, in jih lahko stisnete med dvema kolonama. In lahko zapustite vodilne ničle za vsak par bajtov. Na primer, vsak od teh parov naslovov je enakovreden:
2001:0db8:c9d2:0012:0000:0000:0000:0051 2001:db8:c9d2:12::51 2001:0db8:ab00:0000:0000:0000:0000:0000 2001:db8:ab00:: 0000:0000:0000:0000:0000:0000:0000:0001 ::1
Naslov :: 1 je povratni naslov. Vedno pomeni “ta stroj, na katerem trenutno deluje”. V IPv4 je naslov zanke 127.0.0.1.
Nazadnje, obstaja tudi način IPv4-združljivosti za naslove IPv6, na katere se morda naletite. Če želite na primer predstaviti naslov IPv4 192.0.2.33 kot naslov IPv6, uporabite naslednji zapis: “::ffff: 192.0.2.33″.
Govorimo resno.
Pravzaprav je tako resno zabavno, da so ustvarjalci IPv6 precej razbremenili trilijone in trilionov naslovov za rezervirano uporabo, vendar imamo toliko, odkrito, kdo celo šteje več? Za vsakega moškega, ženske, otroka, mladička in števca parkirnih mest je veliko na vsem planetu v galaksiji. In verjemi mi, vsak planet v galaksiji ima parkirne števce. Veš, da je res.
3.1.1. Podmnožice
Iz organizacijskih razlogov je včasih primerno razglasiti, da je “ta prvi del tega naslova IP prek tega bita omrežni del IP-naslova, preostanek pa je gostiteljski del.
Na primer, z IPv4 boste morda imeli 192.0.2.12 in lahko bi rekli, da so prvi trije bajti omrežje, zadnji gostitelj pa je bil gostitelj. Ali pa postavite drug način, govorimo o gostitelju 12 v omrežju 192.0.2.0 (poglejte, kako izklopimo bajt, ki je bil gostitelj.)
In zdaj za zastarele informacije! Pripravljen? V Ancient Timesih so obstajali “razredi” podomrežij, kjer je bil prvi, dva ali trije bajt naslova omrežni del. Če ste imeli dovolj sreče, da imate en bajt za omrežje in tri za gostitelja, lahko imate v vašem omrežju 24 bitnih gostiteljev (16 milijonov). To je bila mreža “razreda A”. Na nasprotni strani je bil “razred C”, s tremi bajti omrežja in en bajt gostitelja (256 gostiteljev, minus par, ki je bil rezerviran.)
Tako kot lahko vidite, je bilo le nekaj razreda As, ogromen kup razreda C in nekaj razredov Bs na sredini.
Omrežni del IP-naslova opisuje nekaj, kar imenujemo omrežna maska, ki vam je bitno-IN z naslovom IP, da iz njega pridobite omrežno številko. Netmaskat običajno izgleda nekaj podobnega kot 255.255.255.0. (Npr. S to masko, če je vaš IP 192.0.2.12, potem je vaše omrežje 192.0.2.12 in 255.255.255.0, ki daje 192.0.2.0.)
Na žalost se je izkazalo, da to ni bilo dovolj natančno za morebitne potrebe interneta; Zelo hitro smo zmanjkalo omrežij razreda C, in zagotovo smo bili iz razreda As, zato se ne trudite vprašati. Če želite to odpraviti, so pooblastila, ki omogočajo, da je omrežna maska poljubno število bitov, ne le 8, 16 ali 24. Torej, morda imate mrežno masko, recimo 255.255.255.252, kar je 30 bitov omrežja in 2 bita gostitelja, ki omogočata štiri gostitelje v omrežju. (Upoštevajte, da je omrežna maska VSAKO, 1-bita, ki ji sledi kup 0-bitov.)
Ampak to je malo grobo, da uporabite velik niz številk, kot so 255.192.0.0 kot mrežna maska. Prvič, ljudje nimajo intuitivne ideje o tem, koliko bitov je, in drugič, res ni kompakten. Tako je prišel novi slog in je veliko lepše. Po naslovu IP ste postavili poševnico, nato pa sledite številu omrežnih bitov v decimalni skupini. Všeč mi je: 192.0.2.12/30.
Ali, za IPv6, nekaj takega: 2001:db8::/32 ali 2001:db8:5413:4028::9db9/64.
3.1.2. Pristaniške številke
Če se boste vljudno spomnili, sem vas prej predstavil z modelom Layered Network, ki je imel internetni sloj (IP) ločen od transportnega sloja med gostiteljem in gostiteljem (TCP in UDP). Pojdi na hitrost na to pred naslednjim odstavkom.
Izkazalo se je, da poleg IP-naslova, ki ga uporablja IP-plast, obstaja še en naslov, ki ga uporabljajo TCP (vtičnice za pretok) in, po naključju, UDP (datagramove). To je številka vrat. To je 16-bitna številka, ki je kot lokalni naslov povezave.
Pomislite na naslov IP kot ulico naslova hotela in številko vrat kot številko sobe. To je primerna analogija; morda kasneje bom prišel do tistega, ki vključuje avtomobilsko industrijo.
Recimo, da želite imeti računalnik, ki obravnava dohodno pošto in spletne storitve – kako razlikujete med obema na računalniku z enim samim naslovom IP?
No, različne storitve na internetu imajo različne znane številke vrat. Vse jih lahko vidite na seznamu velikih vrat IANA ali, če ste v polju Unix, v datoteki /etc/services. HTTP (spletna) je pristanišče 80, telnet je pristanišče 23, SMTP je pristanišče 25, igra DOOM uporabljena vrata 666 itd. In tako naprej. Pristanišča pod 1024 se pogosto štejejo za posebna in običajno zahtevajo posebne privilegije OS za uporabo.
In to je približno to!
3.2. Byte Order
Z redom kraljestva! Dva bajtna naročila, v nadaljevanju imenovana Lame in Magnificent!
Šalim se, vendar je eden res boljši od drugega.
V resnici ni preprostega načina za to, da bi to povedal, zato ga bom samo utripal: računalnik je morda shranil bajt v obratnem vrstnem redu za hrbet. Vem! Nihče te ni hotel povedati.
Stvar je v tem, da so vsi v svetovnem spletu splošno strinjali, da če želite predstaviti dvobajtno šestnajstiško številko, recimo b34f, jo shranite v dva zaporedna bajta b3, ki ji sledi 4f. Ima smisel in, kot vam je povedal Wilford Brimley, je to prava stvar. Ta številka, shranjena s prvim velikim koncem, se imenuje Big-Endian.
Na žalost je nekaj računalnikov, razpršenih tu in tam po vsem svetu, in sicer z vsemi procesorji, združljivimi s procesorjem Intel ali Intel, shranjene obrnjene bajtove, zato b34f shranite v pomnilnik kot zaporedni bajt 4f, ki mu sledi b3. Ta način shranjevanja se imenuje Little-Endian.
Ampak počakaj, še nisem končal z terminologijo še! Bolj pameten Big-Endian se imenuje tudi Network Byte Order, ker je to vrsta vrst omrežja, kot so.
Računalnik shrani številke v Host Byte Order. Če gre za Intel 80×86, je Host Byte Order. Če je Motorola 68k, je Host Byte Order Big-Endian. Če je PowerPC, je Host Byte Order… torej, odvisno od tega!
Veliko krat, ko izdelujete pakete ali napolnite podatkovne strukture, boste morali zagotoviti, da sta vaši dve- in štirideset-številčni številki v omrežni vrstni red. Toda kako lahko to storite, če ne poznate domačega Byte Reda?
Dobre novice! Prepričajte se, da je ukaz gostiteljskega bajta neveljaven, vedno pa jo uporabite za funkcijo, da jo nastavite na vrstni red omrežnega bajta. Funkcija bo naredila čarobno pretvorbo, če bo to storila, in tako je vaša koda prenosljiva na stroje različnih konec.
Vse pravice. Obstajata dve vrsti številk, ki jih lahko pretvorite: kratki (dva bajta) in dolga (štirje bajti). Te funkcije delujejo tudi za nepodpisane različice. Recimo, da želite kratek pretvoriti iz naslova Host Byte v omrežni vrstni red. Začnite s »h« za »gostitelj«, sledite ji z »do«, potem »n« za »omrežje« in »s« za »kratko«: h-to-ns ali htons () (preberite: »Host do Network Short “).
Skoraj preprosto je…
Lahko uporabite vsako kombinacijo “n”, “h”, “s” in “l”, ki jo želite, ne računate pa res neumnih. Npr. Na stolpcu ni stolp () (“Kratka do dolga gostiteljica”) – ne na tej strani. Vendar obstajajo:
htons() | host to network short |
htonl() | host to network long |
ntohs() | network to host short |
ntohl() | network to host long |
V bistvu želite pretvoriti številke v omrežni vrstni red, preden gredo ven na žico, in jih pretvorite v Host Byte Order, ko pridejo v žico.
Ne vem za 64-bitno različico, oprosti. Če želite narediti plavajočo točko, si oglejte razdelek Serialization, daleč pod njim.
Predpostavimo, da so številke v tem dokumentu v naročilu Host Byte, če ne rečem drugače.
3.3. Strukture
No, končno smo tukaj. Čas je, da govorimo o programiranju. V tem razdelku bom zajemal različne vrste podatkov, ki jih uporablja vmesnik vtičnic, saj so nekateri med njimi resnično medvedi, ki jih je treba razumeti.
Najprej enostavno: deskriptor vtičnice. Deskriptor socket je naslednji tip:
int
Samo redni int.
Stvari se čudno od tukaj, zato samo preberite in nosite z mano.
My First StructTM – struct addrinfo. Ta struktura je najnovejši izum in se uporablja za pripravo kontaktnih struktur za nadaljnjo uporabo. Uporablja se tudi v lookups ime gostitelja in lookups ime storitve. To bo bolj smiselno kasneje, ko bomo prišli do dejanske uporabe, vendar samo za zdaj vedite, da je to ena od prvih stvari, ki jih boste poklicali pri vzpostavljanju povezave.
struct addrinfo { int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc. int ai_family; // AF_INET, AF_INET6, AF_UNSPEC int ai_socktype; // SOCK_STREAM, SOCK_DGRAM int ai_protocol; // use 0 for "any" size_t ai_addrlen; // size of ai_addr in bytes struct sockaddr *ai_addr; // struct sockaddr_in or _in6 char *ai_canonname; // full canonical hostname struct addrinfo *ai_next; // linked list, next node };
To strukturo boste naložili malo in nato pokličite getaddrinfo(). Vrnil bo kazalec na nov povezani seznam teh struktur, ki se izpolnijo z vsemi potrebami, ki jih potrebujete.
Lahko ga prisilite, da uporabite IPv4 ali IPv6 v polju ai_family ali pa pustite, da je AF_UNSPEC uporabil karkoli. To je kul, ker je vaša koda lahko različica agnostika IP.
Upoštevajte, da je to povezani seznam: ai_next točke na naslednjem elementu – tam bi lahko imeli več rezultatov. Uporabil bi prvi rezultat, ki je deloval, vendar imate morda različne poslovne potrebe; Ne vem vse, človek!
Videli boste, da je polje ai_addr v struct addrinfo kazalec na struct sockaddr. To je, če začnemo pridobivati podrobnosti o tem, kaj je v strukturi IP naslovov.
Morda vam ponavadi ni treba pisati v te strukture; pogosto, klic do getaddrinfo(), da izpolnite vaš struct addrinfo za vas, je vse, kar potrebujete. Vendar boste morali v teh strukturah pregledati, da bi se vrednosti izločile, zato jih predstavljam tukaj.
(Prav tako je bila vsa koda, napisana pred izumljanjem strukture addrinfo, vse te stvari pakirali z roko, zato boste v naravi videli veliko kode IPv4, ki to počne ravno. V starih različicah tega vodnika in tako naprej.)
Nekateri strukturi so IPv4, nekateri so IPv6, nekateri pa sta oba. Zapisal bom, katere so kaj.
Kakorkoli že, struct sockaddr ima informacije o naslovih vtičnic za mnoge vrste vtičnic.
struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address };
sa_family je lahko več stvari, vendar bo za vse, kar počnemo v tem dokumentu, AF_INET (IPv4) ali AF_INET6 (IPv6). sa_data vsebuje ciljni naslov in številko vrat za vtičnico. To je precej grozljivo, saj ne želite, da bi potegnili naslov v sa_data z roko.
Za obravnavo struct sockaddr so programerji ustvarili vzporedno strukturo: struct sockaddr_in (“v” za “internet”), ki se uporablja z IPv4.
In to je pomemben bit: kazalec na struct sockaddr_in se lahko prenese v kazalec na struct sockaddr in obratno. Torej, čeprav connect () hoče strukturo sockaddr *, lahko še vedno uporabite struct sockaddr_in in jo oddate v zadnji minuti!
// (IPv4 only--see struct sockaddr_in6 for IPv6) struct sockaddr_in { short int sin_family; // Address family, AF_INET unsigned short int sin_port; // Port number struct in_addr sin_addr; // Internet address unsigned char sin_zero[8]; // Same size as struct sockaddr };
Ta struktura olajša referenco elementov naslova vtičnice. Upoštevajte, da je treba sin_zero (ki je vključen, da se struktura podloži na dolžino struct sockaddr) nastaviti na vse ničle s funkcijo memset(). Prav tako opazite, da sin_family ustreza sa_family v struct sockaddr in mora biti nastavljen na “AF_INET”. Končno, sin_port mora biti v omrežnem vrstnem redu (z uporabo htons()!)
Krenimo še globlje! Vidiš, da je polje sin_addr struct in_addr. Kaj je to? No, ne biti preveč dramatičen, ampak to je eden od najstrašjih sindikatov vseh časov:
// (IPv4 only--see struct in6_addr for IPv6) // Internet address (struktura iz zgodovinskih razlogov) struct in_addr { uint32_t s_addr; // that's a 32-bit int (4 bytes) };
Vau! No, nekoč je bila sindikat, zdaj pa se zdi, da ti dnevi niso več. Dobro osvoboditev. Torej, če ste prijavili ina, da je tipa struct sockaddr_in, se ina.sin_addr.s_addr sklicuje na 4-bajtni IP-naslov (v omrežnem vrstnem redu). Upoštevajte, da četudi vaš sistem še vedno uporablja Bog-grozno zvezo za struct in_addr, lahko še vedno sklicujete na 4-bajtni IP-naslov na popolnoma enak način, kot sem storil zgoraj (to zaradi #defines.)
Kaj pa IPv6? Za to obstajajo podobne struct:
// (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4) struct sockaddr_in6 { u_int16_t sin6_family; // naslov družine, AF_INET6 u_int16_t sin6_port; // številka vrat, Network Byte Order u_int32_t sin6_flowinfo; // Podatki o toku IPv6 struct in6_addr sin6_addr; // Naslov IPv6 u_int32_t sin6_scope_id; // Področje ID }; struct in6_addr { unsigned char s6_addr[16]; // Naslov IPv6 };
Upoštevajte, da ima IPv6 naslov IPv6 in številko vrat, tako kot IPv4 ima naslov IPv4 in številko vrat.
Upoštevajte tudi, da ne bom govoril o pretočnih podatkih IPv6 ali Field Field ID za trenutek… to je le začetni vodnik.
Nenazadnje, tu je še ena preprosta struktura, struct sockaddr_storage, ki je zasnovana tako, da je dovolj velika, da drži strukture IPv4 in IPv6. (Glej, za nekatere klice včasih ne veste vnaprej, če bo izpolnil vaš struct sockaddr z naslovom IPv4 ali IPv6. Torej, preideš v to vzporedno strukturo, zelo podobna strukturi sockaddr, razen večje, do vrste, ki jo potrebujete:
struct sockaddr_storage { sa_family_t ss_family; // address family // vse to je oblazinjenje, specifično izvajanje, ga ignoriraj: char __ss_pad1[_SS_PAD1SIZE]; int64_t __ss_align; char __ss_pad2[_SS_PAD2SIZE]; };
Pomembno je, da si lahko v polju ss_family ogledate družino naslovov, da preverite, ali je to AF_INET ali AF_INET6 (za IPv4 ali IPv6). Potem ga lahko oddate v struct sockaddr_in ali struct sockaddr_in6, če želite.
3.4. Naslov IP, del Deux
Na srečo imate na voljo številne funkcije, ki vam omogočajo, da manipulirate z IP-adresami. Ni potrebe, da jih ugotovite z roko in jih dolgo dolgo z << operaterjem.
Najprej recimo, da imate struct sockaddr_in ina in imate naslov IP “10.12.110.57” ali “2001: db8: 63b3: 1 :: 3490″, ki ga želite shraniti vanj. Funkcija, ki jo želite uporabiti, inet_pton () pretvori IP naslov v zapisu številk in pik v strukturo in_addr ali strukturo in6_addr, odvisno od tega, ali določite AF_INET ali AF_INET6. (“Pton” pomeni “predstavitev v omrežje” – lahko ga imenujemo “tiskanje v omrežje”, če je to lažje zapomniti.) Pretvorbo lahko naredite na naslednji način:
struct sockaddr_in sa; // IPv4 struct sockaddr_in6 sa6; // IPv6 inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4 inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6
(Hitro opomba: stari način izvajanja stvari je uporabil funkcijo, imenovano inet_addr () ali drugo funkcijo, imenovano inet_aton (), ki so zdaj zastarela in ne delujejo z IPv6.)
Zdaj zgornji del kode ni zelo robusten, ker ni preverjanja napak. Glej, inet_pton() vrne -1 ob napaki ali 0, če je naslov zmeden. Zato preverite, ali je rezultat večji od 0 pred uporabo!
V redu, zdaj lahko pretvorite niza IP naslovov v njihove binarne predstavitve. Kaj pa obratno? Kaj, če imate strukturo in_addr in jo želite natisniti v zapisu številk in pik? (Ali pa strukturo in6_addr, ki jo želite v zapisu »heks in kolone«.) V tem primeru boste želeli uporabiti funkcijo inet_ntop() (“ntop” pomeni “omrežje za predstavitev” – lahko ga poimenujte “omrežje za tiskanje”, če je to lažje zapomniti), na primer:
// IPv4: char ip4[INET_ADDRSTRLEN]; // prostor, da držite niz IPv4 struct sockaddr_in sa; // se pretvarjam, da je to naloženo z nekaj inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN); printf("The IPv4 address is: %s\n", ip4); // IPv6: char ip6[INET6_ADDRSTRLEN]; // prostor, da držite niz IPv6 struct sockaddr_in6 sa6; // se pretvarjam, da je to naloženo z nekaj inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN); printf("The address is: %s\n", ip6);
Ko jo pokličete, boste posredovali vrsto naslova (IPv4 ali IPv6), naslov, kazalec na niz, ki naj ima rezultat, in največjo dolžino tega niza. (Dva makra prikladno držita velikost niza, ki jo boste morali imeti največji naslov IPv4 ali IPv6: INET_ADDRSTRLEN in INET6_ADDRSTRLEN.)
(Druga hitra opomba je še enkrat omeniti stari način opravljanja stvari: zgodovinska funkcija za to konverzijo je bila imenovana inet_ntoa(). Prav tako je zastarela in ne bo delovala z IPv6.)
Nazadnje, te funkcije delujejo samo s številskimi IP-adresami – ne bodo storile nobenega imenskega imenskega imena DNS-ja v imenu gostitelja, na primer “www.example.com”. Za to boste uporabili getaddrinfo(), kot boste videli kasneje.
3.4.1. Zasebna (ali prekinjena) omrežja
Veliko krajev ima požarni zid, ki skriva omrežje od preostalega sveta za lastno zaščito. In pogosto, požarni zid prevaja “notranje” IP naslove na “zunanji” (da vsi drugi na svetu vejo) naslove IP z uporabo procesa, imenovanega Prevajanje omrežnih naslovov ali NAT.
Ste še živčni? “Kje gre s temi čudnimi stvarmi?”
No, sprostite se in kupite brezalkoholno (ali alkoholno) pijačo, ker kot začetnik vam ni treba skrbeti samo za NAT, saj je to storjeno za vas pregledno. Ampak hotel sem govoriti o omrežju za požarnim zidom, če bi začeli zbirati z omrežnimi številkami, ki ste jih videli.
Na primer, doma imam požarni zid. Imam dva statična naslova IPv4, ki mi jih je dodelila družba DSL, vendar imam v omrežju sedem računalnikov. Kako je to mogoče? Dva računalnika ne moreta deliti istega naslova IP, sicer pa podatki ne vedo, kateremu je treba iti!
Odgovor je: nimajo enakih naslovov IP. So v zasebni mreži s 24 milijoni IP naslovov, ki so mu dodeljeni. Vsi so samo zame. No, vse zame, kar zadeva kogarkoli drugega. Tukaj se dogaja:
Če se prijavim v oddaljeni računalnik, mi pove, da sem prijavljen s 192.0.2.33, kar je javni IP-naslov, ki mi ga je ponudil ponudnik internetnih storitev. Ampak, če vprašam svoj lokalni računalnik, kakšen je njegov IP naslov, piše 10.0.0.5. Kdo prevaja naslov IP od enega do drugega? Tako je, požarni zid! To počne NAT!
10.x.x.x je eno od redkih rezerviranih omrežij, ki jih je mogoče uporabiti le v popolnoma ločenih omrežjih ali v omrežjih, ki so za požarnim zidom. Podrobnosti o tem, katere številke zasebnega omrežja so na voljo za uporabo, so opisane v RFC 1918, vendar je nekaj običajnih, ki jih boste videli, 10.x.x.x in 192.168.x.x, kjer je x 0-255. Manj pogosti je 172.y.x.x, kjer y poteka med 16 in 31.
Omrežja za požarnim zidom NATing ni treba biti na enem od teh rezerviranih omrežij, vendar so pogosto.
(Zabavno dejstvo! Moj zunanji IP-naslov ni res 192.0.2.33. Omrežje 192.0.2.x je rezervirano za “realne” IP-naslove, ki se uporabljajo v dokumentaciji, prav tako kot ta vodnik!)
IPv6 ima tudi zasebna omrežja, v določenem smislu. Začeli se bodo s fdxx: (ali morda v prihodnosti fcXX :), kot je navedeno v RFC 4193. NAT in IPv6 se običajno ne mešata (razen če delate IPv6 v IPv4 prehoda, ki presega obseg Ta dokument) – v teoriji boste imeli toliko naslovov, da vam ne bo treba več uporabljati NAT-a. Toda če želite dodeliti naslove sami v omrežju, ki ne bo usmerjal zunaj, je to, kako to storiti.
4. Skakanje iz IPv4 v IPv6
Ampak samo želim vedeti, kaj spremeniti v svoji kodi, da bi prišel z IPv6! Povej mi zdaj!
V redu! V redu!
Skoraj vse, kar sem tukaj, je nekaj, kar sem prešel, zgoraj, toda kratka različica za nestrpno. (Seveda je več kot to, toda to velja za vodnik.)
struct sockaddr_in sa; struct sockaddr_in6 sa6; sa.sin_addr.s_addr = INADDR_ANY; // uporabite svoj naslov IPv4 sa6.sin6_addr = in6addr_any; // uporabite svoj naslov IPv6
Tudi vrednost IN6ADDR_ANY_INIT se lahko uporabi kot inicializator, ko je deklarirana struktura in6_addr, tako:
struct in6_addr ia6 = IN6ADDR_ANY_INIT;
Et voila!
5. Sistemske klice ali prsi
To je odsek, v katerem se vpišemo v sistemske klice (in druge knjižnične klice), ki vam omogočajo dostop do omrežnih funkcij okvira Unix ali katerega koli polja, ki podpira vtičnice API za to zadevo (BSD, Windows, Linux, Mac, Kaj imate.) Ko pokličete eno od teh funkcij, jedro prevzame in vse delo opravi samodejno.
Mesto, kjer se večina ljudi zaljubljuje okoli, je, na kakšen naročilo je treba omeniti. V tem se man strani ne uporabljajo, kot ste verjetno odkrili. No, za pomoč pri tej grozljivi situaciji sem poskušal določiti sistemske klice v naslednjih razdelkih v natančno (približno) enakem vrstnem redu, ki ga boste morali poklicati v svojih programih.
To, skupaj z nekaj kosov vzorčne kode tukaj in tam, nekaj mleka in piškotov (ki se bojim, da se boste morali oskrbeti), in nekaj surovega črevesja in poguma, v internetu pa boste obkrožili podatke, kot je Sin Jon Postela!
(Upoštevajte, da številni odlomki kode spodaj ne vključujejo potrebnega preverjanja napak in zelo pogosto domnevajo, da rezultat iz klicev v getaddrinfo () uspe in vrne veljaven vnos na povezanem seznamu. Oba od teh primerov sta pravilno obravnavana Vendar v samostojnih programih, zato jih uporabite kot model.)
5.1. getaddrinfo() – Pripravite se na začetek!
To je prava delovna sila funkcije z veliko možnosti, vendar je uporaba dejansko precej preprosta. Pomaga pri nastavitvi struktur, ki jih potrebujete kasneje.
Droben zgodovin: zgodilo se je, da bi uporabili funkcijo, imenovano gethostbyname(), da bi naredili pregledovanje DNS-jev. Nato boste ročno naložili te informacije v strukturo sockaddr_in in jo uporabili v svojih klicih.
To ni več potrebno, hvaležen. (Prav tako ni zaželeno, če želite napisati kodo, ki deluje tako za IPv4 kot za IPv6!) V teh sodobnih časih imate zdaj funkcijo getaddrinfo(), ki za vas opravlja vse vrste dobrih stvari, vključno z DNS-ji in imenskimi poizvedbami , In izpolnite vse strukite, ki jih potrebujete, poleg tega!
Oglejmo si!
#include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *node, // e.g. "www.example.com" or IP const char *service, // e.g. "http" or port number const struct addrinfo *hints, struct addrinfo **res);
To funkcijo dajte trije vhodnim parametrom in vam daje kazalec na povezani seznam, res, rezultatov.
Parameter node je ime gostitelja za povezavo ali naslov IP.
Naslednja je service parametrov, ki je lahko številka vrat, na primer »80« ali ime določene storitve (na seznamu vrat IANA ali datoteki /etc/services na vašem Unix računalniku), na primer “http” ali “http”, ftp “ali” telnet “ali” smtp “ali karkoli.
Nazadnje, parameter za hints opozarja na struct addrinfo, ki ste ga že izpolnjevali z ustreznimi informacijami.
Tukaj je vzorec klic, če ste strežnik, ki želi poslušati na IP naslov vašega gostitelja, vrata 3490. Upoštevajte, da to dejansko ne poslušate ali nastavite omrežja; zgolj postavlja strukture, ki jih bomo kasneje uporabljali:
int status; struct addrinfo hints; struct addrinfo *servinfo; // bo pokazal rezultate memset(&hints, 0, sizeof hints); // poskrbite, da je struktura prazna hints.ai_family = AF_UNSPEC; // ne skrbi IPv4 ali IPv6 hints.ai_socktype = SOCK_STREAM; // TCP stream sockets hints.ai_flags = AI_PASSIVE; // izpolnite moj IP za mene if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); exit(1); } // servinfo zdaj prikaže povezani seznam 1 ali več struct addrinfos // ... storite vse, dokler ne potrebujete servinfo več .... freeaddrinfo(servinfo); // free the linked-list
Upoštevajte, da sem nastavil ai_family družbo AF_UNSPEC, s tem pa rekel, da me ne zanima, če uporabljamo IPv4 ali IPv6. Lahko ga nastavite na AF_INET ali AF_INET6, če želite eno ali drugo posebej.
Tudi tam boste videli zastavo AI_PASSIVE; to pove getaddrinfo (), da dodeli naslov lokalnega gostitelja na vtičnice. To je lepo, ker potem vam ni treba strogo kode. (Ali lahko določite svoj naslov kot prvi parameter getaddrinfo (), kjer trenutno imam NULL, tam zgoraj.)
Potem kličemo. Če pride do napake (getaddrinfo () vrne ničlo), ga lahko natisnemo s funkcijo gai_strerror(), kot vidite. Kljub temu, če vse deluje pravilno, bo servinfo pokazal povezani seznam struct addrinfos, od katerih vsak vsebuje nekakšno strukturo sockaddr, ki jo lahko pozneje uporabimo! Čudovito!
Nazadnje, ko smo na koncu vsi naredili s povezanim seznamom, ki se dobi goaddrinfo(), ki je tako milostno dodeljena za nas, jo lahko (in naj) izpustimo s klicem na freeaddrinfo().
Tukaj je vzorec klic, če ste stranka, ki se želi povezati z določenim strežnikom, recimo “port 3490 www.example.net”. Tudi to se dejansko ne poveže, vendar pa postavlja strukture, ki jih bomo kasneje uporabljali:
int status; struct addrinfo hints; struct addrinfo *servinfo; // bo pokazal rezultate memset(&hints, 0, sizeof hints); // poskrbite, da je struct prazna hints.ai_family = AF_UNSPEC; // ne skrbi IPv4 ali IPv6 hints.ai_socktype = SOCK_STREAM; // TCP stream sockets // se pripravite na povezavo status = getaddrinfo("www.example.net", "3490", &hints, &servinfo); // servinfo zdaj kaže na povezani seznam 1 ali več struct addrinfos // etc.
Stalno govorim, da je servinfo povezan seznam z vsemi vrstami naslovnih informacij. Napišite hiter program za prikaz teh informacij. Ta kratek program bo natisnil naslove IP za katerikoli gostitelj, ki ga navedete v ukazni vrstici:
/* ** showip.c -- show IP addresses for a host given on the command line */ #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <arpa/inet.h> #include <netinet/in.h> int main(int argc, char *argv[]) { struct addrinfo hints, *res, *p; int status; char ipstr[INET6_ADDRSTRLEN]; if (argc != 2) { fprintf(stderr,"usage: showip hostname\n"); return 1; } memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version hints.ai_socktype = SOCK_STREAM; if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status)); return 2; } printf("IP addresses for %s:\n\n", argv[1]); for(p = res;p != NULL; p = p->ai_next) { void *addr; char *ipver; // poiščite kazalec na naslov sam, // različna polja v IPv4 in IPv6: if (p->ai_family == AF_INET) { // IPv4 struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; addr = &(ipv4->sin_addr); ipver = "IPv4"; } else { // IPv6 struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); ipver = "IPv6"; } // pretvorite IP v niz in ga natisnite: inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr); printf(" %s: %s\n", ipver, ipstr); } freeaddrinfo(res); // free the linked list return 0; }
Kot vidite, koda pokliče getaddrinfo() na vse, kar vmesnik prenesete v ukazno vrstico, ki izpolni povezani seznam, na katerega se opozarja res, nato pa lahko ponovimo seznam in natisnemo ali naredimo karkoli.
(Tam je nekaj grdega, kjer moramo kopati v različne vrste struct sockaddrs, odvisno od različice IP. Oprostite za to! Nisem prepričan v boljši način okoli nje.)
Sample run! Vsi imajo radi posnetke zaslona:
$ showip www.example.net IP addresses for www.example.net: IPv4: 192.0.2.88 $ showip ipv6.example.com IP addresses for ipv6.example.com: IPv4: 192.0.2.101 IPv6: 2001:db8:8c00:22::171
Zdaj, ko imamo to pod nadzorom, bomo rezultate, ki jih dobimo od getaddrinfo (), prenesli na druge funkcije socket-a in končno vzpostavili našo mrežno povezavo! Nadaljuj branje!
5.2. socket() – dobite opis datoteke!
Mislim, da ga ne morem več odpreti – govoriti moram o sistemskem klicu socket(). Tukaj je razčlenitev:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
Toda kaj so ti argumenti? Omogočajo vam, da poveste, kakšne vrste vtičnice želite (IPv4 ali IPv6, tok ali datagram ter TCP ali UDP).
To so bili ljudje, ki bi težko kodirali te vrednote, in to lahko popolnoma storite. (Domena je PF_INET ali PF_INET6, vrsta je SOCK_STREAM ali SOCK_DGRAM in protokol je lahko nastavljen na 0, da izberete ustrezen protokol za določen tip. Ali pa lahko pokličete getprotobyname (), da poiščete želeni protokol, »tcp« ali » Udp “.)
(Ta stvar PF_INET je bližnji sorodnik AF_INET, ki ga lahko uporabite pri inicializaciji polja sin_family v strukturi sockaddr_in. Pravzaprav so tako tesno povezani, da imajo dejansko enako vrednost, mnogi programerji pa bodo klicali socket() In prepustite AF_INET kot prvi argument namesto PF_INET. Zdaj dobite nekaj mleka in piškotkov, ker je to čas za zgodbo. Nekoč nekoč že davno se je zdelo, da je morda družina naslovov (kaj je “AF” V “AF_INET”) podpira več protokolov, na katere se sklicuje njihova protokolarna družina (kaj pomeni “PF” v “PF_INET”). To se ni zgodilo. In vsi so živeli srečno do konca. Torej, najprimernejša stvar je, da uporabite AF_INET v vašem struct sockaddr_in in PF_INET v vašem klicu socket ().)
Kakorkoli, dovolj tega. Kaj res želite storiti je, da uporabite vrednosti iz rezultatov klica na getaddrinfo() in jih vnesite v socket() neposredno tako:
int s; struct addrinfo hints, *res; // naredi iskanje // [se pretvarjamo, da smo že napolnili "namige" struct] getaddrinfo("www.example.com", "http", &hints, &res); // [spet bi morali preveriti napake na getaddrinfo() in hoditi // seznam "res" povezan z iskanjem veljavnih vnosov namesto samo // Ob predpostavki, da je prvi dober (podobno kot mnogi od teh primerov.) // Oglejte si razdelek o odjemalcu/strežniku za resnične primere.] s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
socket() preprosto vrne k vam soketni deskriptor, ki ga lahko uporabite v kasnejših sistemskih klicih ali -1 pri napaki. Svetovna spremenljivka errno je nastavljena na vrednost napake (za več podrobnosti glejte stran errno man in hitra opomba o uporabi errno v večnitnih programih.)
Dobro, lepo, dobro, ampak kakšna je ta vtičnica? Odgovor je, da v resnici ni nobenega dobrega, in morate prebrati in narediti več sistemskih klicev, da bi imeli smisel.
5.3. bind() – V kakšnem pristanišču sem?
Ko imate vtičnico, boste morda morali to vtičnico povezati z vrati na vašem lokalnem računalniku. (To običajno storite, če boste listen() za dohodne povezave na določenih omrežnih igrah v omrežju za več igralcev, če vam to povedo, da “priključite na vrata 192.168.5.10 3490″.) Številko vrat uporablja jedro, ki se bo ujemal s prihajajočim paketom v določen deskriptor vtičnice procesa. Če boste delali le connect() (ker ste odjemalec in ne strežnik), je to verjetno nepotrebno. Vseeno preberite, samo za udarce.
Tukaj je povzetek za sistemski klic bind():
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd je deskriptor datoteke socket, ki ga vrne socket(). my_addr je kazalec na strukturo sockaddr, ki vsebuje informacije o vašem naslovu, in sicer pristanišču in naslovu IP. addrlen je dolžina v bajtih tega naslova.
Že. To je malo za absorbiranje v enem kosu. Naj navedemo primer, ki veže vtičnico gostitelju, na katerem se izvaja program, pristanišče 3490:
struct addrinfo hints, *res; int sockfd; // najprej z naslovom naložite naslov getaddrinfo(): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // fill in my IP for me getaddrinfo(NULL, "3490", &hints, &res); // naredite vtičnico: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); // bind v pristanišče, na katerega smo šli getaddrinfo(): bind(sockfd, res->ai_addr, res->ai_addrlen);
Z uporabo zastavice AI_PASSIVE povem programu, da se veže na IP gostitelja, na katerem se izvaja. Če se želite povezati z določenim krajevnim naslovom IP, spustite AI_PASSIVE in vnesite naslov IP za prvi argument, da se dobi getaddrinfo ().
bind() vrne tudi -1 pri napaki in nastavi errno na vrednost napake.
Veliko stare kode ročno pakira struct sockaddr_in pred klicem bind (). Očitno je, da je to IPv4-specifična, vendar vas resnično nič ne preprečuje, da bi naredili isto stvar z IPv6, razen da bo z uporabo getaddrinfo () lažje na splošno. Kakorkoli že, stara koda izgleda nekaj takega:
// !!! To je stari način !!! int sockfd; struct sockaddr_in my_addr; sockfd = socket(PF_INET, SOCK_STREAM, 0); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(MYPORT); // kratek, omrežni vrstni red my_addr.sin_addr.s_addr = inet_addr("10.12.110.57"); memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero); bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);
V zgornji kodi lahko dodelite tudi INADDR_ANY v polje s_addr, če bi se želeli povezati z vašim lokalnim IP naslovom (kot je oznaka AI_PASSIVE, zgoraj.) Različica IPv6 INADDR_ANY je globalna spremenljivka in6addr_any, ki je dodeljena v polje sin6_addr vaše strukture sockaddr_in6. (Obstaja tudi makro IN6ADDR_ANY_INIT, ki ga lahko uporabite v spremenljivkem inicializatorju.)
Še ena stvar, ki jo morate paziti pri klicu bind(): ne gredo v podmorsko številko s številkami vrat. Vsa pristanišča pod 1024 so RESERVED (razen če ste nadrejeni)! Lahko imate katero koli številko vrat, ki je nad tem, do 65535 (pod pogojem, da jih še ne uporablja drug program).
Včasih boste morda opazili, poskusite ponovno vzpostaviti strežnik in povezati () neuspešno in zatrjevati, da je »Naslov že v uporabi«. Kaj to pomeni? No, malenkost vtičnice, ki je bila priključena, še vedno visi v jedru, in to je zaskočitev pristanišča. Lahko počakate, da počisti (nekaj minut ali tako) ali pa v program dodajte kodo, ki ji omogoča ponovno uporabo pristanišča, na primer:
int yes=1; //char yes='1'; // Solaris people use this // izgubite napeto sporočilo o napaki »Naslov že v uporabi« if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof yes) == -1) { perror("setsockopt"); exit(1); }
Ena majhna dodatna zadnja opomba o bind(): časi, ko jih ne boste morali nujno poklicati. Če se connect() na oddaljeni računalnik in vam ni všeč, kakšna je vaša lokalna vrata (tako kot pri telnet, kjer vas skrbi samo za oddaljena vrata), lahko preprosto pokličete connect(), preverite, ali je vtičnica nevezana in jo bo bind) na neuporabljeno lokalno vrata, če je potrebno.
5.4. connect() – Hej, ti!
Samo nekaj minut se pretvarjamo, da ste telnet aplikacija. Vaš uporabnik vam ukazuje (tako kot v filmu TRON), da dobite deskriptor socket datoteke. Soglašate in pokličite socket(). Nato vam uporabnik pove, da se priključite na “10.12.110.57” na pristanišču “23” (standardna telnetna vrata.) Yow! Kaj počneš sedaj?
Lucky for you, program, zdaj preučujete razdelek connect() – kako se povezati z oddaljenim gostiteljem. Torej brutalno preberite! Ni časa za izgubo!
Klic connect() je naslednji:
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd je naš prijazen deskriptor datotečnega vtiča soseske, kot ga je vrnil klic socket(), serv_addr je struct sockaddr, ki vsebuje ciljno vrata in IP naslov, in addrlen je dolžina v bajtih strukture naslova strežnika.
Vse te informacije se lahko naučijo iz rezultatov callad-a getaddrinfo(), ki se skriva.
Ali je to bolj smiselno? Ne morem te slišati od tukaj, zato moram samo upati, da je. Oglejmo si primer, kjer povezava vtičnice “www.example.com”, vrata 3490:
struct addrinfo hints, *res; int sockfd; // najprej z naslovom naložite naslov getaddrinfo(): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; getaddrinfo("www.example.com", "3490", &hints, &res); // naredite vtičnico: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); // connect! connect(sockfd, res->ai_addr, res->ai_addrlen);
Še enkrat, programi starih šol so napolnili svoje strukture sockaddr_ins, da bi se connect(). To lahko storite, če želite. Oglejte si podobno opombo v razdelku bind() zgoraj.
Ne pozabite preveriti povratne vrednosti iz bind() – vrnil se bo -1 pri napaki in nastavil spremenljivko errno.
Prav tako opazite, da nismo poklicali bind(). V bistvu nas ne zanima številka naše lokalne številke; skrbimo samo, kje smo (oddaljeno pristanišče). Kernel nam bo izbral lokalna vrata in spletno mesto, s katerim se povezujemo, bo te podatke samodejno dobilo od nas. Brez skrbi.
5.5. listen() – Ali me bo kdo poklical?
Ok, čas za spremembo ritma. Kaj, če se ne želite povezati z oddaljenim gostiteljem. Recimo, samo za udarce, da želite čakati na dohodne povezave in jih na nek način ravnati. Postopek je dva koraka: najprej listen(), nato accept() (glejte spodaj).
Poslušaj klic je precej preprost, vendar zahteva malo pojasnila:
int listen(int sockfd, int backlog);
sockfd je običajen deskriptor datoteke socket iz sistemskega klica. backlog je število dovoljenih povezav v dohodni vrsti. Kaj to pomeni? No, dohodne povezave bodo čakale v tej čakalni vrsti, dokler ne sprejmete () jih (glejte spodaj), in to je omejitev števila, v kolikšni meri lahko čakajo na vrsto. Večina sistemov tiho omeji to število na približno 20; lahko verjetno pobegnete z nastavitvijo na 5 ali 10.
Spet, kot je običajno, listen() vrne -1 in nastavi napako errno.
No, kot verjetno predstavljate, moramo klicati bind(), preden pokličemo listen(), tako da strežnik deluje na določenem pristanišču. (Morate imeti možnost povedati svojim prijateljem, kateremu pristanišču se poveže!) Torej, če boste poslušali dohodne povezave, bo zaporedje sistemskih klicev, ki jih boste naredili, naslednje:
getaddrinfo(); socket(); bind(); listen(); /* accept() gre tukaj */
To bom pustil na mestu vzorčne kode, saj je to precej samoumevno. (Koda v razdelku accept(), spodaj, je bolj popolna.) Zares prefinjen del tega celotnega sha-banga je poziv za accept().
5.6. accept() – “Hvala za klic pristanišča 3490.”
Pripravite se – klic accept() je nekako čuden! Kaj se bo zgodilo, je to: nekdo, daleč daleč, bo poskusil connect() na vaš računalnik na pristanišču, ki ga listen(). Njihova povezava bo v čakalni vrsti, ki čakajo na accept()ed. Pokličete accept() in poveste, da dobite čakajočo povezavo. Vrnila vam bo povsem nov deskriptor deskriptorjev datotek, ki bo uporabljena za to eno povezavo! To je pravica, nenadoma imate dva deskriptorja datotečnih datotek za ceno enega! Izvirnik še vedno posluša nove povezave, novonastalo pa je končno pripravljeno send() in recv(). So bili tam!
Klic je naslednji:
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd je opisni list listen(). Preprosto je. addr bo navadno kazalec na lokalno strukturo sockaddr_storage. Tu se bodo prikazale informacije o dohodni povezavi (in s tem lahko določite, kateri gostitelj vas kliče iz katerega pristanišča). addrlen je lokalna celoštevilska spremenljivka, ki mora biti nastavljena na sizeof (struct sockaddr_storage), preden je njen naslov sprejet, da accept(). accept() ne bo dal več kot toliko bajtov v addr. Če dodaja manj, bo spremenila vrednost dodane vrednosti, da bi to addrlen.
Ugani kaj? accept() vrne -1 in nastavi errno, če pride do napake. Betcha tega ni razumel.
Kot prej, je to kupček, ki se absorbira v enem kosu, zato je tukaj vzorec kode fragment za vaše preiskovanje:
#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MYPORT "3490" // se bodo uporabniki pristanišč povezali #define BACKLOG 10 // koliko čakajočih čakalnih vezij bo int main(void) { struct sockaddr_storage their_addr; socklen_t addr_size; struct addrinfo hints, *res; int sockfd, new_fd; // !! ne pozabite na preverjanje napak pri teh klicih !! // najprej z naslovom naložite naslov getaddrinfo(): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // fill in my IP for me getaddrinfo(NULL, MYPORT, &hints, &res); // naredite vtičnico, jo povežite in poslušajte: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); bind(sockfd, res->ai_addr, res->ai_addrlen); listen(sockfd, BACKLOG); // zdaj sprejmite dohodno povezavo: addr_size = sizeof their_addr; new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size); // pripravljen na komuniciranje socket descriptor new_fd! . . .
Spomnimo se, da bomo za vse klice send() in recv() uporabili deskriptor sokov new_fd. Če dobite samo eno povezavo, lahko close() slušalko sockfd, da preprečite več dohodnih povezav na istem pristanišču, če tako želite.
5.7. send() in recv() – Pogovorite se z mano!
Ti dve funkciji sta namenjena za komunikacijo preko vtičnic s tokom ali povezanih podnožij datagrama. Če želite uporabljati redne nepovezane vtičnice datagrama, boste morali videti spodnji razdelek o sendto() in recvfrom().
Klic send():
int send(int sockfd, const void *msg, int len, int flags);
sockfd je deskriptor soketa, ki ga želite poslati (ali je tisti, ki ga je vrnil socket () ali tisti, ki ste ga dobili s accept().) msg je kazalec na podatke, ki jih želite poslati, in len je dolžina podatke v bajtih. Samo nastavite flags na 0. (Za več informacij o zastavah si oglejte stran za send() man.
Nekatera vzorčna koda je lahko:
char *msg = "Beej je bil tukaj!"; int len, bytes_sent; . . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); . . .
send() vrne število dejansko poslanih bajtov – to je lahko manj od številke, ki ste jo poslali! Glej, včasih mu poveste, da pošljete celotno gobico podatkov in ga preprosto ne morete obvladati. Izginil bo čim več podatkov, kot vam bo uspelo, in vam zaupam, da ostale pošiljate pozneje. Ne pozabite, če se vrednost, ki jo vrne send(), ne ujema z vrednostjo v len, je na vas, da pošljete preostanek niza. Dobra novica je to: če je paket majhen (manj kot 1K ali tako), bo verjetno uspelo poslati celotno stvar vse naenkrat. Ponovno se vrne -1 ob napaki, errno pa je nastavljena na številko napake.
Klic recv() je podoben v mnogih pogledih:
int recv(int sockfd, void *buf, int len, int flags);
sockfd je deskriptor vtičnice za branje, buf je vmesni pomnilnik za prebiranje informacij v, len je največja dolžina vmesnega pomnilnika, znaki pa se znova nastavijo na 0. (Za manjše informacije glejte stran za recv().)
recv() vrne število bajtov, ki se dejansko preberejo v vmesni pomnilnik, ali -1 pri napaki (z ustrezno napako).
Počakajte! recv() lahko vrne 0. To lahko pomeni le eno stvar: oddaljena stran je zaprla povezavo na vas! Povratna vrednost 0 je recv() način, da veste, da se je to zgodilo.
Tam je bilo to enostavno, kajne? Zdaj lahko prenesete podatke naprej in nazaj na vtičnice za pretok! Whee! Ti si Unix Network Programmer!
5.8. sendto() in recvfrom() – Pogovorite se z menoj, slog DGRAM
“To je vse v redu in dandy,” slišim si rekel, “ampak kje me to pusti z nepovezanimi datagram vtičnicami?” Brez problema, amigo. Imamo samo stvar.
Ker datagramovi vtičniki niso povezani z oddaljenim gostiteljem, uganite, kateri del informacij moramo dati pred pošiljanjem paketa? Tako je! Naslov destinacije! Tukaj je škatlica:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, socklen_t tolen);
Kot vidite, je ta klic v bistvu enak kot klic za send() z dodatkom dveh drugih informacij. To je kazalec na struct sockaddr (ki bo verjetno še en struct sockaddr_in ali struct sockaddr_in6 ali struct sockaddr_storage, ki ste ga oddali v zadnji minuti), ki vsebuje ciljni naslov IP in vrata. Tolen, int globoko navzdol, lahko preprosto nastavite na sizeof * to or sizeof (struct sockaddr_storage).
Če želite dobiti svoje roke v strukturi naslovnega naslova, jo boste verjetno dobili bodisi iz getaddrinfo() ali iz recvfrom() spodaj, ali pa jo boste ročno izpolnili.
Tako kot pri send(), sendto() vrne število dejansko poslanih bajtov (kar bi lahko bilo tudi manj kot število bajtov, ki ste mu povedali, da ga pošljete!) Ali -1 pri napaki.
Podobno so tudi recv() in recvfrom(). Povzetek recvfrom() je:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
Again, to je tako kot recv() z dodatkom nekaj polj. iz je kazalec na lokalno strukturo sockaddr_storage, ki bo napolnjena z naslovom IP in pristaniščem naprave za izvirnike. Fromlen je kazalec na lokalni int, ki ga je treba inicializirati v velikost * od ali sizeof (struct sockaddr_storage). Ko se funkcija vrne, bo odlomek vseboval dolžino dejansko shranjenega naslova.
recvfrom() vrne število prejetih bajtov ali -1 pri napaki (z ustrezno nastavitvijo errno).
Torej, tukaj je vprašanje: zakaj uporabimo struct sockaddr_storage kot vrsto vtičnice? Zakaj ne struct sockaddr_in? Ker se ne želite povezati z IPv4 ali IPv6. Torej uporabljamo generični struct sockaddr_storage, za katerega vemo, da bo dovolj velik.
(Torej… tukaj je še eno vprašanje: zakaj struktura sockaddr ni dovolj velika za kateri koli naslov? sockaddr_storage smo celo oddali v splošni namen struct sockaddr! Zdi se, da je tuja in odvečna, to ni dovolj veliko, in mislim, da bi bilo sprememba na tej točki problematična, zato so naredili novo.)
Ne pozabite, če connect() vtičnico datagrama, lahko preprosto uporabite send() in recv() za vse vaše transakcije. Vtičnica je še vedno datagram vtičnica in paketi še vedno uporabljajo UDP, toda vmesniški vmesnik samodejno dodaja ciljne in izvorne podatke za vas.
5.9. close() in shutdown() – Iztegni moj obraz!
Dajte! Cel dan ste send() ing in recv() podatke, ki ste jih dobili. Pripravljeni ste, da zaprete povezavo v deskriptorju sokov. To je enostavno. Uporabite lahko samo navadno funkcijo deskriptorja close():
close(sockfd);
To bo preprečilo nadaljnje branje in pisanje v vtičnico. Kdorkoli poskuša prebrati ali napisati vtičnico na oddaljenem koncu, bo prejela napako.
Samo v primeru, da želite malo več nadzora nad tem, kako se vtičnica zapre, lahko uporabite funkcijo shutdown(). Omogoča vam, da prekinete komunikacijo v določeni smeri ali obeh načinov (tako kot close().) Snemanje:
int shutdown(int sockfd, int how);
sockfd je deskriptor datoteke, ki ga želite zaustaviti, in kako je eden od teh:
0 | Nadaljnje sprejemanje ni dovoljeno |
1 | Nadaljnje pošiljke niso dovoljene |
2 | Nadaljnje pošiljanje in sprejemanje ni dovoljeno (takoclose()) |
shutdown() vrne 0 pri uspehu in -1 pri napaki (z ustrezno nastavitvijo errno.)
Če boste uporabili shutdown() na nepovezanih vtičnicah datagrama, bo sokal preprosto naredil za nadaljnje send() in recv() (ne pozabite, da jih lahko uporabite, če connect() vtičnico datagrama.)
Pomembno je omeniti, da shutdown () dejansko ne zapira datoteke deskriptorja – samo spremeni njegovo uporabnost. Če želite osvoboditi deskriptor vtičnice, morate uporabiti close().
Nič za to.
(Razen, da ne pozabite, da če uporabljate Windows in Winsock, ki bi morali pokličite zaprto closesocket() namesto close().)
5.10. getpeername() – Kdo ste?
Ta funkcija je tako preprosta.
Tako enostavno, skoraj nisem dala svojega oddelka. Toda tukaj je vseeno.
Funkcija getpeername () vam bo povedala, kdo je na drugem koncu priključene vtičnice. Sinopsis:
#include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd je deskriptor priključene vtičnice, addr je kazalec na struct sockaddr (ali struct sockaddr_in), ki bo vseboval podatke o drugi strani povezave in addrlen je kazalec na int, ki bi ga bilo treba inicializirati do velikosti * addr ali sizeof (struct sockaddr).
Funkcija vrne -1 pri napaki in ustrezno določa errno.
Ko imate svoj naslov, lahko z uporabo inet_ntop (), getnameinfo () ali gethostbyaddr () natisnete ali dobite več informacij. Ne, ne morete dobiti svojega uporabniškega imena. (V redu, če je v drugem računalniku nameščen identifikacijski demon, je to mogoče. To pa je izven področja uporabe tega dokumenta. Za več informacij glejte RFC 1413.)
5.11. gethostname() – Kdo sem jaz?
Še lažje kot getpeername() je funkcija gethostname(). Vrne ime računalnika, na katerem se izvaja vaš program. Ime lahko nato uporabi ime gethostbyname() spodaj, da bi določili naslov IP vašega lokalnega računalnika.
Kaj bi bilo lahko bolj zabavno? Lahko bi pomislil na nekaj stvari, vendar se ne nanašajo na programiranje vtičnic. Kakorkoli, tukaj je razčlenitev:
#include <unistd.h> int gethostname(char *hostname, size_t size);
Argumenti so preprosti: hostname je kazalec na niz znakov, ki bodo vsebovali ime gostitelja ob vrnitvi funkcije, velikost pa je dolžina v bajtih matrike hostname.
Funkcija vrne 0 ob uspešnem zaključku in -1 pri napaki, nastavi errno kot običajno.
6. Ozadje odjemalca-strežnik
To je svet odjemalec-strežnik, otrok. Skoraj vse v omrežju se ukvarja s procesi strank, ki se pogajajo s strežniškimi procesi in obratno. Na primer telnet. Ko se povežete z oddaljenim gostiteljem na portu 23 s telnetom (odjemalcem), se program na tem gostitelju (imenovanem telnetd, strežnik) sproži za življenje. Obvlada dohodno telnetno povezavo, nastavi vas s pozivom za prijavo itd.
Interakcija med odjemalcem in strežnikom.
Izmenjava informacij med odjemalcem in strežnikom je povzeta v zgornjem diagramu.
Upoštevajte, da lahko par odjemalec-strežnik govorita SOCK_STREAM, SOCK_DGRAM ali karkoli drugega (dokler govorijo isto stvar). Nekateri dobri primeri pari odjemalca-strežnik so telnet/telnetd, ftp/ftpd ali Firefox/Apache . Vsakič, ko uporabljate ftp, je oddaljen program, ftpd, ki vam služi.
Pogosto bo na napravi obstajal samo en strežnik, ta strežnik pa bo z vilicami () reševal več strank. Osnovna rutina je: strežnik bo počakal na povezavo, accept() in fork() otroški postopek, s katerim se bo ukvarjal. To je tisto, kar naredi naš vzorec strežnik v naslednjem poglavju.
6.1. Preprost strežniški strežnik
Vse to strežnik pošilja niz “Zdravo, svet!” prek povezave s tokom. Vse, kar morate storiti, da preizkusite ta strežnik, je zagnati v enem oknu in telnet z njim iz drugega:
$ telnet remotehostname 3490
kjer je remotehostname ime naprave, na katerem ga uporabljate.
Strežniška koda:
/* ** server.c -- predstavitveni strežnik za strežniške vtičnice */ #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 <netdb.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> #define PORT "3490" // se bodo uporabniki pristanišč povezali #define BACKLOG 10 // koliko čakajočih čakalnih vezij bo void sigchld_handler(int s) { // waitpid() lahko prepisuje errno, zato ga shranimo in obnovimo: int saved_errno = errno; while(waitpid(-1, NULL, WNOHANG) > 0); errno = saved_errno; } // get sockaddr, IPv4 or IPv6: void *get_in_addr(struct sockaddr *sa) { if (sa->sa_family == AF_INET) { return &(((struct sockaddr_in*)sa)->sin_addr); } return &(((struct sockaddr_in6*)sa)->sin6_addr); } int main(void) { int sockfd, new_fd; // poslušaj sock_fd, nova povezava na new_fd struct addrinfo hints, *servinfo, *p; struct sockaddr_storage their_addr; // informacije o naslovu konektorja socklen_t sin_size; struct sigaction sa; int yes=1; char s[INET6_ADDRSTRLEN]; int rv; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // use my IP if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); return 1; } // zanke skozi vse rezultate in se povežite s prvim, kar lahko for(p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("server: socket"); continue; } if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { perror("setsockopt"); exit(1); } if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); perror("server: bind"); continue; } break; } freeaddrinfo(servinfo); // vse je bilo opravljeno s to strukturo if (p == NULL) { fprintf(stderr, "server: failed to bind\n"); exit(1); } if (listen(sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } sa.sa_handler = sigchld_handler; // Žanje vse mrtve procese sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(1); } printf("server: waiting for connections...\n"); while(1) { // glavna accept() zanke sin_size = sizeof their_addr; new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size); if (new_fd == -1) { perror("accept"); continue; } inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr), s, sizeof s); printf("server: got connection from %s\n", s); if (!fork()) { // to je otroški proces close(sockfd); // otrok ne potrebuje poslušalca if (send(new_fd, "Hello, world!", 13, 0) == -1) perror("send"); close(new_fd); exit(0); } close(new_fd); // staršem to ne potrebuje } return 0; }
Če ste radovedni, imam kodo v eni veliki main() funkciji za (čutim) sintaktično jasnost. Če se počutite bolje, se ga lahko razdelite na manjše funkcije.
(Tudi ta celoten sigaction() je morda nov za vas – to je v redu. Koda, ki je tam, je odgovorna za žimerske procese, ki se pojavijo kot izhod za otroke (če so). Če naredite veliko zombijev in ne Jih poželi, bo skrbnik sistema postal vznemirjen.)
Podatke iz tega strežnika lahko dobite z uporabo odjemalca, navedenega v naslednjem poglavju.
6.2. Preprost odjemalec toka
Ta tip je še lažji od strežnika. Vse to odjemalec se poveže z gostiteljem, ki ga navedete v ukazni vrstici, pristanišče 3490. Dobi niz, ki ga pošlje strežnik.
Vir odjemalca:
/* ** client.c -- demo odjemalca toka vtičnice */ #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> #include <arpa/inet.h> #define PORT "3490" // bo pristaniški odjemalec povezal #define MAXDATASIZE 100 // največje število bajtov, ki jih lahko dobimo takoj // get sockaddr, IPv4 or IPv6: void *get_in_addr(struct sockaddr *sa) { if (sa->sa_family == AF_INET) { return &(((struct sockaddr_in*)sa)->sin_addr); } return &(((struct sockaddr_in6*)sa)->sin6_addr); } int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct addrinfo hints, *servinfo, *p; int rv; char s[INET6_ADDRSTRLEN]; if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); return 1; } // loop through all the results and connect to the first we can for(p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("client: socket"); continue; } if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); perror("client: connect"); continue; } break; } if (p == NULL) { fprintf(stderr, "client: failed to connect\n"); return 2; } inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof s); printf("client: connecting to %s\n", s); freeaddrinfo(servinfo); // vse je bilo opravljeno s to strukturo if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("client: received '%s'\n",buf); close(sockfd); return 0; }
Upoštevajte, da če ne izvajate strežnika pred zagonom odjemalca, connect() vrne “Povezava zavrnjena”. Zelo uporabno.
6.3. Datagramove vtičnice
Zgoraj smo že pokrivali osnove podatkovnih vtičnic UDP z našo razpravo o sendto() in recvfrom() zgoraj, zato bom predstavil le nekaj vzorčnih programov: talker.c in listener.c.
poslušalec sedi na stroju, ki čaka dohodni paket na vratih 4950. govornik pošlje paket v to vrata na določenem računalniku, ki vsebuje vse, kar uporabnik vnese v ukazno vrstico.
Tukaj je vir za listener.c:
/* ** listener.c -- predstavitev datagramovih vtičnic "strežnik" */ #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" // se bodo uporabniki pristanišč povezali #define MAXBUFLEN 100 // get sockaddr, IPv4 or IPv6: void *get_in_addr(struct sockaddr *sa) { if (sa->sa_family == AF_INET) { return &(((struct sockaddr_in*)sa)->sin_addr); } return &(((struct sockaddr_in6*)sa)->sin6_addr); } int main(void) { int sockfd; struct addrinfo hints, *servinfo, *p; int rv; int numbytes; struct sockaddr_storage their_addr; char buf[MAXBUFLEN]; socklen_t addr_len; char s[INET6_ADDRSTRLEN]; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // nastavite na AF_INET, da prisilite IPv4 hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_PASSIVE; // uporabite moj IP if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); return 1; } // zanke skozi vse rezultate in se povežite s prvim, kar lahko for(p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("listener: socket"); continue; } if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); perror("listener: bind"); continue; } break; } if (p == NULL) { fprintf(stderr, "listener: failed to bind socket\n"); return 2; } freeaddrinfo(servinfo); printf("listener: waiting to recvfrom...\n"); addr_len = sizeof their_addr; if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN-1 , 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("listener: got packet from %s\n", inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr), s, sizeof s)); printf("listener: packet is %d bytes long\n", numbytes); buf[numbytes] = '\0'; printf("listener: packet contains \"%s\"\n", buf); close(sockfd); return 0; }
Obvestilo, da v našem klicu getaddrinfo() končno uporabljamo SOCK_DGRAM. Upoštevajte tudi, da ni potrebe po listen() ali accept(). To je ena od prednosti uporabe nepovezanih vtičnic datagrama!
Naslednji vir je vir talker.c:
/* ** talker.c -- predstavitev datagrama "odjemalec" */ #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 SERVERPORT "4950" // se bodo uporabniki pristanišč povezali int main(int argc, char *argv[]) { int sockfd; struct addrinfo hints, *servinfo, *p; int rv; int numbytes; if (argc != 3) { fprintf(stderr,"usage: talker hostname message\n"); exit(1); } memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); return 1; } // Zanke skozi vse rezultate in naredite vtičnico for(p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("talker: socket"); continue; } break; } if (p == NULL) { fprintf(stderr, "talker: failed to create socket\n"); return 2; } if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0, p->ai_addr, p->ai_addrlen)) == -1) { perror("talker: sendto"); exit(1); } freeaddrinfo(servinfo); printf("talker: sent %d bytes to %s\n", numbytes, argv[1]); close(sockfd); return 0; }
In to je vse, kar ima! Zaženite poslušalca na določenem računalniku, nato pogovorite z drugim. Pazite, da komunicirajo! Zabavno G-razburjenje za celotno jedrsko družino!
Tokrat vam sploh ni treba zagnati strežnika! Sami lahko pogovarjate s pogovori in samo srečno požre pakete v eter, kjer izginejo, če nihče ni pripravljen z recvfrom() na drugi strani. Ne pozabite: podatki, poslani s podnožji datagrama UDP, ne bodo zagotovo prišli!
Razen še en majhen detajl, ki sem ga v preteklosti že omenil: priključene datagramove vtičnice. O tem se moram pogovoriti, ker smo v razdelku datagrama dokumenta. Recimo, da talker kliče connect() in določi naslov listener’ca. Od tega trenutka naprej se lahko talker pošlje samo in prejme z naslova, ki ga je določil connect(). Zaradi tega vam ni treba uporabljati sendto() in recvfrom(); lahko preprosto uporabite send() in recv().
7. Nekoliko napredne tehnike
Te niso preveč napredne, vendar so izstopile iz bolj osnovnih ravni, ki smo jih že pokrili. Dejansko, če ste prišli tako daleč, bi morali biti sami pošteno izpolnjeni v osnovah Unix omrežja programiranje! Čestitke!
Torej greva v pogumen novi svet nekaterih bolj ezoteričnih stvari, ki bi jih morda želeli naučiti o vtičnicah. Na to!
7.1. Blokiranje
Blokiranje. Slišali ste o tem – kaj je zdaj vraga? Na kratko, “blok” je techie žargon za “spanje”. Verjetno ste opazili, da ob zagonu listener zgolj sedi tu, dokler ne pride paket. Kaj se je zgodilo je, da je imenovano recvfrom(), ni bilo podatkov, zato je recvfrom() rečeno, da “blokira” (to je spati tam), dokler ne pridejo nekateri podatki.
Veliko funkcij blokira. accept() bloki. Vse funkcije recv() blokirajo. Razlog, da to lahko storijo, je, ker jim je dovoljeno. Ko prvič ustvarite deskriptor vtiča s socket(), jedro nastavi na blokado. Če ne želite blokirati vtičnice, morate poklicati fcntl():
#include <unistd.h> #include <fcntl.h> . . . sockfd = socket(PF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); . . .
Z nastavitvijo vtičnice na ne-blokiranje, lahko učinkovito “ankete” vtičnico za informacije. Če poskušate prebrati iz nevključne vtičnice in tam ni podatkov, ga ni dovoljeno blokirati – vrnila se bo -1, errno pa bo nastavljena na EAGAIN ali EWOULDBLOCK.
(Počakajte – lahko vrne EAGAIN ali EWOULDBLOCK? Katero preverite? Specifikacija dejansko ne določa, kateri sistem se bo vrnil, zato je za prenosljivost preverite oba.)
Na splošno pa je ta vrsta glasovanja slaba ideja. Če program postavite v zaseden počakaj, iščete podatke v vtičnici, boste porabili čas CPU-ja, kot da se je izogibal slogu. Več elegantna rešitev za preverjanje, če želite preveriti, ali so podatki, ki čakajo na branje, najdete v naslednjem razdelku v select().
7.2. select() – Sinhrono V/I multipleksiranje
Ta funkcija je nekoliko čudna, vendar je zelo uporabna. Izvedite naslednjo situacijo: ste strežnik in želite poslušati dohodne povezave, kot tudi branje od povezav, ki jih že imate.
Ni problema, pravite samo, accept() in nekaj recv () s. Ne tako hitro, baster! Kaj, če blokirate klic accept()? Kako boste hkrati priklicali podatke za recv()? “Uporabljajte nepomembne vtičnice!” Ni šans! Ne želiš biti CPU. Kaj potem?
select() omogoča hkratno spremljanje več vtičnic. Povedal vam bo, katere so pripravljene za branje, ki so pripravljene za pisanje in katere vtičnice so postavile izjeme, če to res želite izvedeti.
V današnjem času je select(), čeprav zelo prenosljiva, ena od najmanjših načinov spremljanja vtičnic. Ena možna alternativa je neuravnotežena ali podobna, ki zajema vse sistemsko odvisne stvari, povezane z obveščanjem socket.
Brez kakršnega koli dodatka, bom ponudil povzetek 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);
Funkcija spremlja “sets” deskriptorjev datotek; zlasti readfds, writefds in exceptfdsj. Če želite videti, ali lahko berete iz standardnega vhoda in nekega deskriptorja sock, sockfd, samo dodajte deskriptorje datotek 0 in sockfd v nastavljene vrednosti readfds. Parameter numfds mora biti nastavljen na vrednosti najvišjega deskriptorja datoteke plus eno. V tem primeru mora biti nastavljen na sockfd+1, saj je gotovo višji od standardnega input (0).
Ko select() vrne, se bo readfds spremenil, da bo odražal, kateri od opisnih datotek, ki ste jih izbrali, pripravljen za branje. Lahko jih preskusite z makrom FD_ISSET() spodaj.
Preden nadaljujem veliko več, se bom pogovoril o tem, kako manipulirati s temi kompleti. Vsak niz je tipa fd_set. Na tej vrsti delujejo naslednji makri:
Končno, kaj je to čudno iz strukture timeval? No, včasih nočeš večno čakati, da ti nekdo pošlje nekaj podatkov. Morda vsakih 96 sekund, ki jih želite natisniti “Still Going …” na terminal, čeprav se ni nič zgodilo. Ta časovna struktura vam omogoča, da določite časovno omejitev. Če je čas presežen in select() še vedno ni našel nobenih deskriptorjev za pripravljene datoteke, se bo vrnil, da boste lahko nadaljevali z obdelavo.
Strukturni timeval ima naslednja polja:
struct timeval { int tv_sec; // sekund int tv_usec; // mikrosekunde };
Samo nastavite tv_sec na število sekund, ki jih želite čakati, in nastavite tv_usec na število mikrosekund, ki čakajo. Ja, to je mikrosekundo, ne milisekund. V milisekundi je 1.000 mikrosekund, v sekundi pa 1.000 milisekund. Tako je v sekundi 1.000.000 mikrosekund. Zakaj je “usec”? “U” naj bi izgledal kot grška črka μ (Mu), ki jo uporabljamo za “mikro”. Ko se funkcija vrne, se lahko posodobitev časovne omejitve posodobi, da se prikaže še preostali čas. To je odvisno od kakšne arome Unixa teče.
Juhu! Imamo časovno ločljivost za mikrosekundo! No, ne računajte na to. Verjetno boste morali počakati na del svojega standardnega Unix timeslice, ne glede na to, kako majhen ste postavili strukturo timeval.
Druge zanimive stvari: če ste polja v struct struct timeval nastavili na 0, select() bo takoj začasno prekinil, kar bo v poljih učinkovito preiskovalo vse deskriptorje datotek. Če nastavite zakasnitev parametra na NULL, ne bo nikoli prekinila čakanja, dokler ne bo pripravljen prvi deskriptor datoteke. Nazadnje, če vam ni všeč čakati na določen niz, ga lahko nastavite na NULL v klicu, da select().
Naslednji delček kode čaka 2,5 sekunde za nekaj, kar se prikaže na standardnem vhodu:
/* ** select.c -- a select() demo */ #include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define STDIN 0 // deskriptor datoteke za standardni vhod int main(void) { struct timeval tv; fd_set readfds; tv.tv_sec = 2; tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds); // ne skrbi writefds in 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; }
Če ste na linijskem vmesnem terminalu, mora biti ključ, ki ste ga zadeli, RETURN ali pa bo čas tako dolgo.
Zdaj bi nekateri morda mislili, da je to odličen način, da počakate na podatke v vtičnici datagram-in ste prav: mogoče je. Nekateri Unices lahko izberejo na ta način in nekateri ne morejo. Morate vedeti, kaj vaša lokalna stran pravi o zadevi, če jo želite poskusiti.
Nekateri Unices posodabljajo čas v strukturnem času, tako da odražajo čas, ki še vedno preostane pred časovnim zamikom. Toda drugi ne. Ne zanašajte se na to, če želite biti prenosni. (Uporabite gettimeofday(), če želite slediti času, ki je pretekel. To je bummer, vem, vendar je tako.)
Kaj se zgodi, če vtičnica v branju zapre povezavo? No, v tem primeru select() vrne s tem deskriptorjem nastavljenim kot “pripravljen za branje”. Ko dejansko storite recv() iz nje, se bo vrnila 0. (recv()). Tako veste, da je stranka zaprta povezavo.
Še ena zanimivost o select(): če imate vtičnico, ki jo listen(), lahko preverite, ali obstaja nova povezava, tako da v opisni vrstici za branje vstavite deskriptor datoteke readfds.
In to, moji prijatelji, je hiter pregled funkcije almighty select().
Ampak, po priljubljenem povpraševanju, tukaj je poglobljen primer. Na žalost je razlika med preprostim primerom umazanije, zgoraj in tem tukaj pomembna. Ampak poglejte, nato pa preberite opis, ki sledi temu.
Ta program deluje kot preprost večuporabniški klepet. Zaženite jo v enem oknu, nato pa telnet (“telnet hostname 9034“) iz več drugih oken. Ko vnesete nekaj v eni telnet seji, se mora prikazati v vseh drugih.
/* ** selectserver.c -- sirast večstranski 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> #include <netdb.h> #define PORT "9034" // pristanišče, na katerem poslušamo // get sockaddr, IPv4 or IPv6: void *get_in_addr(struct sockaddr *sa) { if (sa->sa_family == AF_INET) { return &(((struct sockaddr_in*)sa)->sin_addr); } return &(((struct sockaddr_in6*)sa)->sin6_addr); } int main(void) { fd_set master; // glavni seznam deskriptorjev datotek fd_set read_fds; // seznam deskriptorjev datotek za select() int fdmax; // največja številka deskriptorja datoteke int listener; // slišan deskriptor int newfd; // nov accept()ed deskriptor sokov struct sockaddr_storage remoteaddr; // client address socklen_t addrlen; char buf[256]; // pufru za podatke o strankah int nbytes; char remoteIP[INET6_ADDRSTRLEN]; int yes=1; // za setsockopt() SO_REUSEADDR, spodaj int i, j, rv; struct addrinfo hints, *ai, *p; FD_ZERO(&master); // počistite glavne in temp nizove FD_ZERO(&read_fds); // prinesi vtičnico in jo veže memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) { fprintf(stderr, "selectserver: %s\n", gai_strerror(rv)); exit(1); } for(p = ai; p != NULL; p = p->ai_next) { listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (listener < 0) { continue; } // izgubite napeto sporočilo o napaki "že v uporabi" setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) { close(listener); continue; } break; } // če pridemo sem, to pomeni, da se nam ni bilo treba povezati if (p == NULL) { fprintf(stderr, "selectserver: failed to bind\n"); exit(2); } freeaddrinfo(ai); // all done with this // poslušaj if (listen(listener, 10) == -1) { perror("listen"); exit(3); } // dodajte poslušalca na glavno enoto FD_SET(listener, &master); // sledi največji deskriptor datotek fdmax = listener; // so far, it's this one // glavna zanka for(;;) { read_fds = master; // copy it if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); exit(4); } // potekajo prek obstoječih povezav, ki iščejo podatke za branje for(i = 0; i <= fdmax; i++) { if (FD_ISSET(i, &read_fds)) { // we got one!! if (i == listener) { // handle new connections addrlen = sizeof remoteaddr; newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen); if (newfd == -1) { perror("accept"); } else { FD_SET(newfd, &master); // dodajte glavni nastavitvi if (newfd > fdmax) { // sledite največjim fdmax = newfd; } printf("selectserver: new connection from %s on " "socket %d\n", inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET6_ADDRSTRLEN), newfd); } } else { // obravnava podatke od odjemalca if ((nbytes = recv(i, buf, sizeof buf, 0)) <= 0) { // je prišlo do napake ali povezave, ki jo je zaprt odjemalec if (nbytes == 0) { // povezava je zaprta printf("selectserver: socket %d hung up\n", i); } else { perror("recv"); } close(i); // bye! FD_CLR(i, &master); // remove from master set } else { // od stranke dobimo nekaj podatkov for(j = 0; j <= fdmax; j++) { // poslati vsem! if (FD_ISSET(j, &master)) { // razen poslušalca in nas samih if (j != listener && j != i) { if (send(j, buf, nbytes, 0) == -1) { perror("send"); } } } } } } // END obdeluje podatke od odjemalca } // END je dobil novo dohodno povezavo } // END kroženje skozi deskriptorje datotek } // END for(;;)--in si mislil, da se ne bo nikoli končalo! return 0; }
Obvestilo V kodi: master in read_fds imam dva kompleta deskriptorjev datotek. Prvi, master, ima vse deskripte sokov, ki so trenutno priključeni, in deskriptor sokov, ki posluša nove povezave.
Razlog za moj master set je, da select() dejansko changes nabor, ki ga vnesete vanj, da odražajo, katere vtičnice so pripravljene za branje. Ker moram slediti povezavam z enim klicem select() na drugo, jih moram varno shraniti nekam. V zadnjem trenutku kopiram master v read_fds in nato pokličem select().
Ampak to ne pomeni, da moram vsakič, ko dobim novo povezavo, dodati v master komplet? Yup! In vsakič, ko se povezava zapre, jo moram odstraniti iz glavnega sklopa? Ja, res.
Obvestilo preverim, če je vtičnica listener pripravljena za branje. Ko je, to pomeni, da imam novo povezavo, ki je v teku, in jo accept() in jo dodam glavni nastavitvi. Podobno, ko je povezava odjemalca pripravljena za branje in recv() vrne 0, vem, da je stranka zaprta povezavo in jo odstraniti iz master nabora.
Če stranka recv() vrne ničlo, čeprav vem, da so bili nekateri podatki prejeti. Tako dobim, nato pa pojdem po glavnem seznamu in pošljem te podatke vsem ostalim povezanim strankam.
In to, moji prijatelji, je manj kot preprost pregled funkcije almighty select().
Poleg tega je tukaj naknadni bonus: obstaja še ena funkcija, imenovana poll(), ki se obnaša na enak način, kot select(), vendar z drugačnim sistemom za upravljanje deskriptorjev datotek. Preverite!
7.3. Ravnanje z delnim send()
Se spomniš nazaj v razdelku o send(), zgoraj, ko sem rekel, da send() morda ne pošilja vseh bajtov, ki ste jih zahtevali? To pomeni, da želite poslati 512 bajtov, vendar vrne 412. Kaj se je zgodilo s preostalimi 100 bajtov?
No, še vedno so v vašem blažilniku, ki čakajo na pošiljanje. Zaradi okoliščin, na katere ne morete vplivati, se je jedro odločilo, da ne bo poslalo vseh podatkov v enem kosu, zdaj pa, prijatelj moj, vam je na voljo podatke.
Tako lahko naredite tako funkcijo:
#include <sys/types.h> #include <sys/socket.h> int sendall(int s, char *buf, int *len) { int total = 0; // koliko bajtov smo poslali int bytesleft = *len; // koliko smo jih pustili poslati int n; while(total < *len) { n = send(s, buf+total, bytesleft, 0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; // vrnjena številka, ki je bila dejansko poslana tukaj return n==-1?-1:0; // vrnitev -1 ob neuspehu, 0 na uspeh }
V tem primeru je s vtičnica, na katero želite poslati podatke, buf je pufr, ki vsebuje podatke, in len je kazalec na int, ki vsebuje število bajtov v pufru.
Funkcija vrne -1 pri napaki (in errno je še nastavljen iz klica za send().) Prav tako je število dejansko poslanih bajtov vrnjeno v len. To bo enako število bajtov, ki ste jih zahtevali za pošiljanje, razen če je prišlo do napake. sendall() bo naredil, da je najboljše, huffing in puffing, da pošljete podatke ven, če pa je napaka, se takoj vrne k vam.
Za popolnost, tukaj je vzorec poziv k funkciji:
char buf[10] = "Beej!"; int len; len = strlen(buf); if (sendall(s, buf, &len) == -1) { perror("sendall"); printf("Poslali smo le %d bajtov zaradi napake!\n", len); }
Kaj se zgodi na koncu sprejemnika, ko pride del paketa? Če so paketi spremenljive dolžine, kako sprejemnik ve, kdaj se en paket konča, drugi pa se začne? Da, realni scenariji so kraljevska bolečina v osleh. Verjetno morate obkrožiti (zapomnite si, da je odsek za zaprtje podatkov na začetku poti že na začetku?) Preberite za podrobnosti!
Hitro si zapomnite vse ljubitelje Linuxa: včasih se v redkih primerih lahko select() Linuxa vrne “pripravljeno za branje” in potem dejansko ni pripravljena za branje! To pomeni, da bo blokirano na read() po select() pravi, da to ne bo! Zakaj ti malo – ! Kakorkoli že, rešitev za rešitev je, da nastavite zastavo O_NONBLOCK v sprejemno vtičnico, zato napaka z EWOULDBLOCK (ki jo lahko preprosto prezrete, če se zgodi). Oglejte si referenčno stran fcntl() za več informacij o nastavitvi vtičnice na ne-blokiranje.
7.4. Serializacija – Kako pakirati podatke
Dovolj je preprosto pošiljati besedilne podatke po omrežju, ki jih najdete, ampak kaj se zgodi, če želite poslati nekaj “binarnih” podatkov, kot so ints ali floats? Izkazalo se je, da imate nekaj možnosti.
Predogled! Samo zvečer!
[Zavese se dvignejo]
Beej pravi: “Raje uporabljam 3. metodo zgoraj!”
[KONEC]
(Preden začnem to poglavje resno, bi vam moral povedati, da obstajajo knjižnice za to, da se to zgodi in da se sami premikate, preostali prenosni in brez napak pa je zelo izziv. Torej poiščite svojo domačo nalogo, preden se odločite za izvajanje Te stvari sami. V to informacijo vključim tiste radovedneže o tem, kako stvari, kot je ta, delujejo.)
Pravzaprav vse metode, zgoraj, imajo svoje pomanjkljivosti in prednosti, ampak, kot sem dejal na splošno, imam raje tretjo metodo. Najprej pa pogovorimo o nekaterih pomanjkljivostih in prednostih za druga dva.
Prva metoda, ki kodira številke kot besedilo pred pošiljanjem, ima prednost, da lahko enostavno natisnete in preberete podatke, ki prihajajo preko žice. Včasih je človeško berljiv protokol odličen za uporabo v razmerah, ki niso širokopasovno intenzivne, na primer z Internet Relay Chat (IRC). Vendar pa je v slabšem položaju, da se pretvori v pretvorbo in rezultati skoraj vedno zajemajo več prostora kot prvotna številka!
Druga metoda: posredovanje neobdelanih podatkov. Ta je precej enostavna (vendar nevarna!): Vzemite kazalec na podatke, ki jih želite poslati, in pokličite z njim.
double d = 3490.15926535; send(s, &d, sizeof d, 0); /* NEVARNOST--nepremično! */
Sprejemnik dobi takole:
double d; recv(s, &d, sizeof d, 0); /* NEVARNOST--nepremično! */
Hitro, preprosto – kaj ni všeč? No, se izkaže, da vse arhitekture predstavljajo dvojno (ali int za to zadevo) z enako bitno predstavitvijo ali celo enako bajtno naročanje! Koda je nedvomno neprenosljiva. (Hej, morda ne potrebujete prenosljivosti, v tem primeru je to lepo in hitro.)
Pri pakiranju vrst integerov smo že videli, kako lahko funkcijo htons() – funkcije funkcij pomagajo ohraniti stvari prenosljive tako, da pretvorite številke v omrežni vrstni red in kako to storiti. Na žalost ni podobnih funkcij za tipe plovcev. Je vse upanje izgubljeno?
Ne bojte se! (Ali se bojite tam za trenutek? Ne? Niti malo?) Obstaja nekaj, kar lahko storimo: lahko paketno (ali “marshal” ali “serialize” ali eno od tisoč milijonov drugih imen) podatke v znano binarno obliko, ki jo sprejemnik lahko odpošlje na oddaljeni strani.
Kaj mislim z “znanim binarnim formatom”? No, že videli smo primer htons(), kajne? Spreminja (ali “kodira”, če si to tako misliš) številko iz katere koli oblike gostitelja je v omrežni vrstni red. Če želite številko preusmeriti (unencode), sprejemnik pokliče ntohs().
Ampak, nisem samo dokončal rekel, da takšne funkcije ni bilo za druge vrste, ki niso celovite? Ja. Jaz sem. In ker v C ni standardnega načina, je to malo piščanca (to je brezplačno punvo za vas Python fans).
Stvari je, da se podatki pakirajo v znano obliko in pošljejo preko žice za dekodiranje. Na primer, za pakiranje plovcev, tukaj je nekaj hitrega in umazanega z veliko prostora za izboljšave:
#include <stdint.h> uint32_t htonf(float f) { uint32_t p; uint32_t sign; if (f < 0) { sign = 1; f = -f; } else { sign = 0; } p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // celoten del in znak p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // frakcija return p; } float ntohf(uint32_t p) { float f = ((p>>16)&0x7fff); // celoten del f += (p&0xffff) / 65536.0f; // fraction if (((p>>31)&0x1) == 0x1) { f = -f; } // označite bitni niz return f; }
Zgornja koda je vrsta naivnega izvajanja, ki shranjuje float v 32-bitno številko. Visoki bit (31) se uporablja za shranjevanje znaka števila (“1″ pomeni negativno), in naslednji sedem bitov (30-16) se uporabljajo za shranjevanje celotnega števila delov plovca. Končno se preostali bitovi (15-0) uporabijo za shranjevanje delnega dela števila.
Uporaba je dokaj enostavna:
#include <stdio.h> int main(void) { float f = 3.1415926, f2; uint32_t netf; netf = htonf(f); // pretvoriti v "mrežno" obliko f2 = ntohf(netf); // pretvorite nazaj v test printf("Original: %f\n", f); // 3.141593 printf(" Network: 0x%08X\n", netf); // 0x0003243F printf("Unpacked: %f\n", f2); // 3.141586 return 0; }
Na strani plus je majhna, preprosta in hitro. Na minus strani, to ni učinkovita uporaba prostora in obseg je močno omejen – poskusite shraniti številko več kot 32767 tam in to ne bo zelo veselo! V zgornjem primeru lahko vidite tudi, da zadnjih nekaj decimalnih mest ni pravilno shranjenih.
Kaj lahko storimo namesto tega? No, standard za shranjevanje številk s plavajočo vejico je znan kot IEEE-754. Večina računalnikov uporablja to obliko interno za to, da se počne s plavajočo matematično točko, zato v teh primerih strogo gledano ni treba pretvoriti. Ampak, če želite, da je vaša izvorna koda prenosljiva, je to predpostavka, ki je ne morete nujno narediti. (Po drugi strani, če želite, da stvari postanejo hitro, morate to optimizirati na platformah, ki jih ne potrebujete! To je tisto, kar htons() in ilk do.)
Tukaj je nekaj kode, ki kodira floats in podvoji v format IEEE-754. (V glavnem – ne kodira NaN ali Infinity, vendar bi se lahko spremenil, da bi to naredil.)
#define pack754_32(f) (pack754((f), 32, 8)) #define pack754_64(f) (pack754((f), 64, 11)) #define unpack754_32(i) (unpack754((i), 32, 8)) #define unpack754_64(i) (unpack754((i), 64, 11)) uint64_t pack754(long double f, unsigned bits, unsigned expbits) { long double fnorm; int shift; long long sign, exp, significand; unsigned significandbits = bits - expbits - 1; // -1 for sign bit if (f == 0.0) return 0; // get this special case out of the way // preverite znak in začnite normalizirati if (f < 0) { sign = 1; fnorm = -f; } else { sign = 0; fnorm = f; } // dobite normalizirano obliko f in sledite eksponentu shift = 0; while(fnorm >= 2.0) { fnorm /= 2.0; shift++; } while(fnorm < 1.0) { fnorm *= 2.0; shift--; } fnorm = fnorm - 1.0; // izračuna binarno obliko (neplavajočo) pomembnih podatkov significand = fnorm * ((1LL<<significandbits) + 0.5f); // dobite pristranskega eksponenta exp = shift + ((1<<(expbits-1)) - 1); // shift + bias // vrnite končni odgovor return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand; } long double unpack754(uint64_t i, unsigned bits, unsigned expbits) { long double result; long long shift; unsigned bias; unsigned significandbits = bits - expbits - 1; // -1 for sign bit if (i == 0) return 0.0; // pull the significand result = (i&((1LL<<significandbits)-1)); // mask result /= (1LL<<significandbits); // convert back to float result += 1.0f; // add the one back on // obravnavati eksponenta bias = (1<<(expbits-1)) - 1; shift = ((i>>significandbits)&((1LL<<expbits)-1)) - bias; while(shift > 0) { result *= 2.0; shift--; } while(shift < 0) { result /= 2.0; shift++; } // podpišite result *= (i>>(bits-1))&1? -1.0: 1.0; return result; }
Na vrhu sem dal nekaj priročnih makrov za pakiranje in razpakiranje 32-bitnih (verjetno float) in 64-bitnih (verjetno dvojnih) številk, vendar pa lahko funkcija pack754() pokliče neposredno in se kodira, Podatkov (katerih izpiski so rezervirani za normalno število števila).
Tukaj je uporaba vzorca:
#include <stdio.h> #include <stdint.h> // definira vrste uintN_t #include <inttypes.h> // definira makroje PRIx int main(void) { float f = 3.1415926, f2; double d = 3.14159265358979323, d2; uint32_t fi; uint64_t di; fi = pack754_32(f); f2 = unpack754_32(fi); di = pack754_64(d); d2 = unpack754_64(di); printf("float before : %.7f\n", f); printf("float encoded: 0x%08" PRIx32 "\n", fi); printf("float after : %.7f\n\n", f2); printf("double before : %.20lf\n", d); printf("double encoded: 0x%016" PRIx64 "\n", di); printf("double after : %.20lf\n", d2); return 0; }
Zgornja koda proizvaja ta izhod:
float before : 3.1415925 float encoded: 0x40490FDA float after : 3.1415925 double before : 3.14159265358979311600 double encoded: 0x400921FB54442D18 double after : 3.14159265358979311600
Drugo vprašanje, ki ga imate, je, kako sestavite pakete? Na žalost, za vas, lahko prevajalnik prosto doda oblazinjenje po vsem mestu v strukturo, kar pomeni, da ne morete prenesti celotne stvari preko žice v enem kosu. (Ali se vam ne moti, da to ne morete storiti, to ne morete storiti?) Oprostite, da citiram prijatelja: “Kadarkoli gre kaj narobe, vedno kriva Microsoft.” Ta morda ni Microsoftova Krivda, seveda, vendar izjava prijatelja je povsem resnična.)
Nazaj na to: najboljši način pošiljanja strukture preko žice je, da vsako polje shranite neodvisno, nato pa jih razpakirate v strukturo, ko pridejo na drugo stran.
To je veliko dela, kaj misliš. Da, je. Ena stvar, ki jo lahko naredite, je, da napišete pomočno funkcijo, ki vam pomaga pri pakiranju podatkov. To bo zabavno! Resnično!
V knjigi “Praksa programiranja” Kernighana in Pikea izvajajo printf () podobne funkcije, imenovane pack () in unpack (), ki to storijo ravno to. Povezal bi se z njimi, vendar očitno te funkcije niso na spletu z ostalim vira iz knjige.
(Praksa programiranja je odlično branje. Zeus shrani koščico vsakič, ko ga priporočam.)
Na tej točki bom zapustil kazalec na API jezikov C API za tipizirane parametre BSD, ki ga nisem nikoli uporabljal, vendar se zdi popolnoma ugleden. Programatorji Python in Perl bodo želeli preveriti, ali so za jezikovne funkcije pack() in unpack() izpolnjene iste stvari. In Java ima velik-ol Serializable vmesnik, ki ga lahko uporabljate na podoben način.
Toda, če želite napisati lasten pripomoček za pakiranje v C, je K & P trik uporabljati spremenljive liste argumentov, da naredite printf() -like funkcije za izdelavo paketov. Tukaj je različica, ki sem jo sam skuhala na podlagi tega, kar upam, da bo dovolj, da vam predstavim, kako lahko takšna stvar deluje.
(Ta koda se nanaša na funkcije paketa 755 () zgoraj. Funkcije packi * () delujejo kot znana družina htons(), razen če se pakirajo v char znaki namesto na drugo celo število.)
#include <stdio.h> #include <ctype.h> #include <stdarg.h> #include <string.h> /* ** packi16() -- shranite 16-bitni int v char bufer (podobno htons()) */ void packi16(unsigned char *buf, unsigned int i) { *buf++ = i>>8; *buf++ = i; } /* ** packi32() -- shranite 32-bitni int v char buffer (podobno htonl()) */ void packi32(unsigned char *buf, unsigned long int i) { *buf++ = i>>24; *buf++ = i>>16; *buf++ = i>>8; *buf++ = i; } /* ** packi64() -- shranite 64-bitni int v char buffer (podobno htonl()) */ void packi64(unsigned char *buf, unsigned long long int i) { *buf++ = i>>56; *buf++ = i>>48; *buf++ = i>>40; *buf++ = i>>32; *buf++ = i>>24; *buf++ = i>>16; *buf++ = i>>8; *buf++ = i; } /* ** unpacki16() -- razpakirajte 16-bitni int iz čarobnega pufra (podobno ntohs()) */ int unpacki16(unsigned char *buf) { unsigned int i2 = ((unsigned int)buf[0]<<8) | buf[1]; int i; // change unsigned numbers to signed if (i2 <= 0x7fffu) { i = i2; } else { i = -1 - (unsigned int)(0xffffu - i2); } return i; } /* ** unpacku16() -- razpakirajte 16-bitno nepodpisano iz vmesnega pomnilnika (podobno ntohs()) */ unsigned int unpacku16(unsigned char *buf) { return ((unsigned int)buf[0]<<8) | buf[1]; } /* ** unpacki32() -- razpakirajte 32-bitni int iz vmesnega pomnilnika (podobno ntohl()) */ long int unpacki32(unsigned char *buf) { unsigned long int i2 = ((unsigned long int)buf[0]<<24) | ((unsigned long int)buf[1]<<16) | ((unsigned long int)buf[2]<<8) | buf[3]; long int i; // change unsigned numbers to signed if (i2 <= 0x7fffffffu) { i = i2; } else { i = -1 - (long int)(0xffffffffu - i2); } return i; } /* ** unpacku32() -- razpakirajte 32-bitno nepodpisano iz vmesnega pomnilnika (podobno ntohl()) */ unsigned long int unpacku32(unsigned char *buf) { return ((unsigned long int)buf[0]<<24) | ((unsigned long int)buf[1]<<16) | ((unsigned long int)buf[2]<<8) | buf[3]; } /* ** unpacki64() -- razpakirajte 64-bitni int iz čarobnega pufra (podobno ntohl()) */ long long int unpacki64(unsigned char *buf) { unsigned long long int i2 = ((unsigned long long int)buf[0]<<56) | ((unsigned long long int)buf[1]<<48) | ((unsigned long long int)buf[2]<<40) | ((unsigned long long int)buf[3]<<32) | ((unsigned long long int)buf[4]<<24) | ((unsigned long long int)buf[5]<<16) | ((unsigned long long int)buf[6]<<8) | buf[7]; long long int i; // spremenite nepodpisane številke v podpisano if (i2 <= 0x7fffffffffffffffu) { i = i2; } else { i = -1 -(long long int)(0xffffffffffffffffu - i2); } return i; } /* ** unpacku64() -- razpakirajte 64-bitno nepodpisano iz vmesnega pomnilnika (kot ntohl()) */ unsigned long long int unpacku64(unsigned char *buf) { return ((unsigned long long int)buf[0]<<56) | ((unsigned long long int)buf[1]<<48) | ((unsigned long long int)buf[2]<<40) | ((unsigned long long int)buf[3]<<32) | ((unsigned long long int)buf[4]<<24) | ((unsigned long long int)buf[5]<<16) | ((unsigned long long int)buf[6]<<8) | buf[7]; } /* ** pack() -- shrani podatke, ki jih narekuje formatni niz v medpomnilniku ** ** bits |signed unsigned float string ** -----+---------------------------------- ** 8 | c C ** 16 | h H f ** 32 | l L d ** 64 | q Q g ** - | s ** ** (16-bitna nepodpisana dolžina se samodejno pripne na nizov) */ unsigned int pack(unsigned char *buf, char *format, ...) { va_list ap; signed char c; // 8-bit unsigned char C; int h; // 16-bit unsigned int H; long int l; // 32-bit unsigned long int L; long long int q; // 64-bit unsigned long long int Q; float f; // floats double d; long double g; unsigned long long int fhold; char *s; // strings unsigned int len; unsigned int size = 0; va_start(ap, format); for(; *format != '\0'; format++) { switch(*format) { case 'c': // 8-bit size += 1; c = (signed char)va_arg(ap, int); // promoted *buf++ = c; break; case 'C': // 8-bit unsigned size += 1; C = (unsigned char)va_arg(ap, unsigned int); // promoted *buf++ = C; break; case 'h': // 16-bit size += 2; h = va_arg(ap, int); packi16(buf, h); buf += 2; break; case 'H': // 16-bit unsigned size += 2; H = va_arg(ap, unsigned int); packi16(buf, H); buf += 2; break; case 'l': // 32-bit size += 4; l = va_arg(ap, long int); packi32(buf, l); buf += 4; break; case 'L': // 32-bit unsigned size += 4; L = va_arg(ap, unsigned long int); packi32(buf, L); buf += 4; break; case 'q': // 64-bit size += 8; q = va_arg(ap, long long int); packi64(buf, q); buf += 8; break; case 'Q': // 64-bit unsigned size += 8; Q = va_arg(ap, unsigned long long int); packi64(buf, Q); buf += 8; break; case 'f': // float-16 size += 2; f = (float)va_arg(ap, double); // spodbujati fhold = pack754_16(f); // convert to IEEE 754 packi16(buf, fhold); buf += 2; break; case 'd': // float-32 size += 4; d = va_arg(ap, double); fhold = pack754_32(d); // convert to IEEE 754 packi32(buf, fhold); buf += 4; break; case 'g': // float-64 size += 8; g = va_arg(ap, long double); fhold = pack754_64(g); // convert to IEEE 754 packi64(buf, fhold); buf += 8; break; case 's': // string s = va_arg(ap, char*); len = strlen(s); size += len + 2; packi16(buf, len); buf += 2; memcpy(buf, s, len); buf += len; break; } } va_end(ap); return size; } /* ** unpack() -- razpakirajte podatke, ki jih narekuje formatni niz v pufru ** ** bits |signed unsigned float string ** -----+---------------------------------- ** 8 | c C ** 16 | h H f ** 32 | l L d ** 64 | q Q g ** - | s ** ** (String je pridobljen na podlagi shranjene dolžine, vendar je lahko "s" ** napolnjena z maksimalno dolžino) */ void unpack(unsigned char *buf, char *format, ...) { va_list ap; signed char *c; // 8-bit unsigned char *C; int *h; // 16-bit unsigned int *H; long int *l; // 32-bit unsigned long int *L; long long int *q; // 64-bit unsigned long long int *Q; float *f; // floats double *d; long double *g; unsigned long long int fhold; char *s; unsigned int len, maxstrlen=0, count; va_start(ap, format); for(; *format != '\0'; format++) { switch(*format) { case 'c': // 8-bit c = va_arg(ap, signed char*); if (*buf <= 0x7f) { *c = *buf;} // re-sign else { *c = -1 - (unsigned char)(0xffu - *buf); } buf++; break; case 'C': // 8-bit unsigned C = va_arg(ap, unsigned char*); *C = *buf++; break; case 'h': // 16-bit h = va_arg(ap, int*); *h = unpacki16(buf); buf += 2; break; case 'H': // 16-bit unsigned H = va_arg(ap, unsigned int*); *H = unpacku16(buf); buf += 2; break; case 'l': // 32-bit l = va_arg(ap, long int*); *l = unpacki32(buf); buf += 4; break; case 'L': // 32-bit unsigned L = va_arg(ap, unsigned long int*); *L = unpacku32(buf); buf += 4; break; case 'q': // 64-bit q = va_arg(ap, long long int*); *q = unpacki64(buf); buf += 8; break; case 'Q': // 64-bit unsigned Q = va_arg(ap, unsigned long long int*); *Q = unpacku64(buf); buf += 8; break; case 'f': // float f = va_arg(ap, float*); fhold = unpacku16(buf); *f = unpack754_16(fhold); buf += 2; break; case 'd': // float-32 d = va_arg(ap, double*); fhold = unpacku32(buf); *d = unpack754_32(fhold); buf += 4; break; case 'g': // float-64 g = va_arg(ap, long double*); fhold = unpacku64(buf); *g = unpack754_64(fhold); buf += 8; break; case 's': // string s = va_arg(ap, char*); len = unpacku16(buf); buf += 2; if (maxstrlen > 0 && len > maxstrlen) count = maxstrlen - 1; else count = len; memcpy(s, buf, count); s[count] = '\0'; buf += len; break; default: if (isdigit(*format)) { // track max str len maxstrlen = maxstrlen * 10 + (*format-'0'); } } if (!isdigit(*format)) maxstrlen = 0; } va_end(ap); }
In tukaj je demonstracijski program zgornje kode, ki pakira nekaj podatkov v buf in ga nato razdeli v spremenljivke. Upoštevajte, da pri klicanju unpack () z argumentom niza (določite format “s”) je pametno, da pred njim nastavite največje število dolžin, da preprečite prekoračitev presežka, npr. “96s”. Bodite pozorni pri razpakiranju podatkov, ki jih dobite prek omrežja – zlonamerni uporabnik lahko pošlje napačno izdelane pakete v naporu, da napadejo vaš sistem!
#include <stdio.h> // različni bitji za vrste s plavajočo vejico-- // se razlikuje za različne arhitekture typedef float float32_t; typedef double float64_t; int main(void) { unsigned char buf[1024]; int8_t magic; int16_t monkeycount; int32_t altitude; float32_t absurdityfactor; char *s = "Veliko neomejeno Zot! Našli ste Runestaff!"; char s2[96]; int16_t packetsize, ps2; packetsize = pack(buf, "chhlsf", (int8_t)'B', (int16_t)0, (int16_t)37, (int32_t)-5, s, (float32_t)-3490.6677); packi16(buf+1, packetsize); // store packet size in packet for kicks printf("packet is %" PRId32 " bytes\n", packetsize); unpack(buf, "chhl96sf", &magic, &ps2, &monkeycount, &altitude, s2, &absurdityfactor); printf("'%c' %" PRId32" %" PRId16 " %" PRId32 " \"%s\" %f\n", magic, ps2, monkeycount, altitude, s2, absurdityfactor); return 0; }
Ne glede na to, ali imate svojo kodo ali pa uporabite nekoga drugega, je dobra zamisel, da imate splošen nabor paketov za pakiranje podatkov, da bi obdržali hrošče in ne vsakič zapakirali vsakega bitnika ročno.
Pri pakiranju podatkov, kakšen je dober format za uporabo? Odlično vprašanje. Na srečo RFC 4506, standard za predstavitev zunanjih podatkov, že definira binarne formate za množico različnih tipov, kot so vrste s plavajočo vejico, vrste integerov, nizi, neobdelani podatki itd. Predlagam, da se sklada s tem, če boste Podatke sami. Toda niste dolžni. Paketna policija ni ravno zunaj vaših vrat. Vsaj mislim, da niso.
Vsekakor pa kodiranje podatkov nekako ali drugače, preden ga pošljete, je pravi način za stvari!
7.5. Sindrom enkapsulacije podatkov
Kaj resnično pomeni zaokrožiti podatke? V najpreprostejšem primeru to pomeni, da boste na njej držali glavo z nekaterimi identifikacijskimi podatki ali dolžino paketa ali obema.
Kako bi moral biti vaš glava videti? No, to je samo nekaj binarnih podatkov, ki predstavljajo, kar menite, da je potrebno za dokončanje projekta.
Vau. To je nejasno.
V redu. Recimo, da imate program za večkratno uporabo, ki uporablja SOCK_STREAM. Ko uporabnik tipa (“pravi”) nekaj, je treba na strežnik prenesti dve informaciji: kaj je bilo rečeno in kdo je to rekel.
Do sedaj je tako dobro? “V čem je problem?” sprašujete.
Težava je, da so lahko sporočila različnih dolžin. Ena oseba, imenovana “tom”, bi lahko rekla: “Živjo” in lahko bi rekla še ena oseba, imenovana “Benjamin”: “Hej, kaj se dogaja?”
Torej send() vse te stvari strankam, ko pridejo. Vaš odhodni tok podatkov izgleda takole:
t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?
In tako naprej. Kako stranka ve, kdaj se začne eno sporočilo in drugo ustavi? Lahko bi, če želite, vsa sporočila enake dolžine in samo pokličite sendall(), ki smo jo izvedli zgoraj. Toda to propada pasovno širino! Ne želimo send () 1024 bajtov tako, da lahko “tom” pove “Hi”.
Torej, podatke zajemamo v majhni glavi in strukturi paketov. Tako stranka kot strežnik vedo, kako pakirati in razpakirati (včasih imenovane “marshal” in “unmarshal”) te podatke. Ne poglej zdaj, vendar začenjamo definirati protokol, ki opisuje, kako se odjemalec in strežnik komunicirajo!
V tem primeru predpostavimo, da je uporabniško ime fiksna dolžina 8 znakov, označena z ‘\0′. In potem predpostavimo, da so podatki spremenljiva, do največ 128 znakov. Oglejmo si vzorčno strukturo paketov, ki bi jo lahko uporabili v tej situaciji:
Zakaj sem izbral 8-bajtne in 128-bajtne omejitve za polja? Izstrelil sem jih iz zraka, ob predpostavki, da bodo dovolj dolgo. Morda je 8 bajtov preveč omejujoče za vaše potrebe, lahko pa imate 30-byte ime polja ali karkoli. Izbira je odvisna od vas.
Z uporabo zgornje opredelitve paketa bi prvi paket vseboval naslednje podatke (v hex in ASCII):
0A 74 6F 6D 00 00 00 00 00 48 69 (length) T o m (padding) H i
In druga je podobna:
18 42 65 6E 6A 61 6D 69 6E 48 65 79 20 67 75 79 73 20 77 ... (length) B e n j a m i n H e y g u y s w ...
(Dolžina je shranjena v vrstnem redu omrežne baze, seveda. V tem primeru je samo en bajt, tako da ni pomembno, vendar na splošno želite, da se vsa binarna cela števila shranijo v vrstni red omrežnega bajta v svojih paketih. )
Ko pošiljate te podatke, bi morali biti varni in uporabiti ukaz, podoben sendall(), zgoraj, zato veste, da so vsi podatki poslani, tudi če je za pošiljanje () potrebnih več klicev, da bi jih vse prenesli.
Prav tako, ko prejemate te podatke, morate opraviti nekaj dodatnega dela. Če želite biti varni, morate domnevati, da boste morda prejeli delni paket (kot bi morda prejeli “18 42 65 6E 6A” iz Benjamina zgoraj, toda to je vse, kar smo dobili v tem pozivu za recv ()). Poklicati moramo recv () znova in znova, dokler paket ni v celoti sprejet.
Ampak kako? No, vemo, koliko bajtov moramo skupno prejeti, da je paket popolen, saj je to število na sprednji strani paketa. Vemo tudi, da je največja velikost paketa 1 + 8 + 128 ali 137 bajtov (ker smo tako opredelili paket).
Tu je dejansko nekaj stvari, ki jih lahko naredite tukaj. Ker veste, da se vsak paket začne z dolžino, lahko pokličete recv () samo, da dobite dolžino paketa. Potem, ko imate to, jo lahko pokličete znova, natančno določite preostalo dolžino paketa (po možnosti večkrat, da dobite vse podatke), dokler ne dobite celotnega paketa. Prednost te metode je, da potrebujete samo dovolj velik pufer, ki je dovolj velik za en paket, medtem ko je pomanjkljivost, da morate vsaj dvakrat poklicati recv (), da bi dobili vse podatke.
Druga možnost je, da pokličete recv () in recite, da je znesek, ki ste ga pripravljeni sprejeti, največje število bajtov v paketu. Potem karkoli dobite, jo držite na zadnji strani vmesnika in na koncu preverite, ali je paket končan. Seveda lahko dobite nekaj naslednjega paketa, zato boste morali imeti prostor za to.
Kaj lahko storite, je razglasiti dovolj velik obseg za dva paketa. To je vaša delovna paleta, kjer boste ob prihodu rekonstruirali pakete.
Vsakič, ko si podatke recv (), jo dodate v delovno pufru in preverite, ali je paket končan. To pomeni, da je število bajtov v pufru večje ali enako dolžini, navedeni v glavi (+1, ker dolžina v glavi ne vključuje bajta za dolžino same.) Če je število bajtov v varnostni pas je manjši od 1, paket ni popolnoma popoln, očitno. Za to morate narediti poseben primer, čeprav je prvi bajt smeti in se ne morete zanašati na pravilno dolžino paketa.
Ko je paket končan, lahko s tem storite, kaj boste. Uporabite ga in ga odstranite iz delovnega pufra.
Dajte! Ali ste že žongli v vaši glavi? No, tukaj je drugi od ene dva punch: morda ste prebrali mimo konca enega paketa in na naslednji v enem pozivu recv(). To pomeni, da imate delovni vmesni pomnilnik z enim celotnim paketom in nepopoln del naslednjega paketa! Prekleto vraga. (Ampak to je razlog, zakaj ste naredili delovni pufra, ki je dovolj velika, da imate dva paketa – v primeru, da se je to zgodilo!)
Ker poznate dolžino prvega paketa iz glave in ste spremljali število bajtov v delovnem pufru, lahko odštejete in izračunate, koliko bajtov v delovnem pufru spada v drugo (nepopolno ) paket. Ko ste ravnali s prvim, ga lahko izbrišete iz delovnega vmesnega pomnilnika in premaknete delni drugi paket navzdol do sprednje strani vmesnega pomnilnika, tako da je vse pripravljeno za naslednjo recv().
(Nekateri od vas bralci bodo opozorili, da dejansko premikanje delnega drugega paketa na začetek delovnega medpomnilnika potrebuje čas, program pa je lahko kodiran, da ga ne potrebujete z uporabo krožnega pufra. Na žalost, za ostale vas je razprava o Krožni odbojniki so izven področja uporabe tega članka. Če ste še vedno radovedni, zgrabi knjigo podatkovnih struktur in pojdite od tam.)
Nikoli nisem rekel, da je bilo enostavno. Ok, rekla sem, da je bilo enostavno. In to je; Potrebuješ samo prakso in kmalu bo prišla k vam naravno. Z Excaliburjem prisežem!
7.6. Oddajni paketi – Pozdravljeni, Svet!
Do sedaj je ta vodnik govoril o pošiljanju podatkov z enega gostitelja na drugega gostitelja. Vendar je mogoče, vztrajam, da lahko z ustreznim pooblastilom istočasno pošljete podatke več gostiteljem!
Z UDP (samo UDP, ne TCP) in standardnim protokolom IPv4, se to izvaja s pomočjo mehanizma, imenovanega broadcasting. Z IPv6 radiodifuzija ni podprta, zato se moraš zateči k pogosto boljši tehniki multicasting, ki se žal ne bom pogovarjal v tem trenutku. Ampak zadostna zvezdna očesna prihodnost – mi smo zaljubljeni v 32-bitni prisotni.
Ampak počakaj! Ne moreš samo zbežati in začeti oddajati Willy-nilly; Nastaviti morate vtičnico SO_BROADCAST, preden lahko v omrežju pošljete oddajo paketov. To je kot eden tistih majhnih plastičnih prevlek, ki so jih postavili prek stikala za izstrelitev izstrelkov! To je količina moči, ki jo držite v rokah!
Toda resno je, da obstaja nevarnost za uporabo oddajnih paketov, in sicer: vsak sistem, ki sprejema oddajni paket, mora razveljaviti vse čebulastne sloje podatkovne enkapsulacije, dokler ne ugotovi, na katerem pristanišču so podatki namenjeni. In potem prenese podatke ali jih zavrže. V obeh primerih je veliko dela za vsak stroj, ki sprejema oddajni paket, in ker je vse v lokalnem omrežju, je to lahko veliko strojev, ki opravljajo veliko nepotrebnega dela. Ko je prva igra Doom prišla ven, je bila pritožba glede njegove omrežne kode.
Zdaj, obstaja več kot en način, da se koža poči… Počakaj malo. Ali obstaja resnično več kot en način kože mačke? Kakšen izraz je to? Uh, in prav tako obstaja več kot en način pošiljanja oddajnega paketa. Torej, da bi prišli do mesa in krompirja celotne stvari: kako določite naslovni naslov za oddajno sporočilo? Obstajata dva načina:
Torej, kaj se zgodi, če poskušate poslati podatke na oddajni naslov, ne da bi najprej nastavili vtičnico SO_BROADCAST? No, poberimo dober stari talker in listener in poglejmo, kaj se zgodi.
$ talker 192.168.1.2 foo sent 3 bytes to 192.168.1.2 $ talker 192.168.1.255 foo sendto: Permission denied $ talker 255.255.255.255 foo sendto: Permission denied
Da, sploh ni srečen… ker nismo nastavili vtičnice SO_BROADCAST. Naredite to, in zdaj lahko sendto() kjerkoli želite!
Dejansko je to edina razlika med aplikacijo UDP, ki se lahko odda in ki ne more. Torej, vzemimo staro talker strežnik in dodamo še en razdelek, ki nastavi vtičnico SO_BROADCAST. Mi bomo poklicali ta program broadcaster.c:
/* ** broadcaster.c -- a datagram "client" like talker.c, except ** this one can broadcast */ #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 SERVERPORT 4950 // se bodo uporabniki pristanišč povezali int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // informacije o naslovu konektorja struct hostent *he; int numbytes; int broadcast = 1; //char broadcast = '1'; // če to ne deluje, poskusite to if (argc != 3) { fprintf(stderr,"usage: broadcaster hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // dobite informacije o gostitelju perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } // ta klic je tisto, kar omogoča pošiljanje oddajnih paketov: if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof broadcast) == -1) { perror("setsockopt (SO_BROADCAST)"); exit(1); } their_addr.sin_family = AF_INET; // host byte order their_addr.sin_port = htons(SERVERPORT); // short, network byte order their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(their_addr.sin_zero, '\0', sizeof their_addr.sin_zero); if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof their_addr)) == -1) { perror("sendto"); exit(1); } printf("sent %d bytes to %s\n", numbytes, inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; }
Kaj se razlikuje med tem in “normalno” situacijo odjemalca/strežnika UDP? Nič! (Razen, da je v tem primeru dovoljeno pošiljati oddaje paketov.) Kot tak, nadaljujte in zaženite stari programski listener UDP v enem oknu in broadcaster v drugem. Sedaj bi morali biti sposobni storiti vse tiste, ki niso uspeli, zgoraj.
$ broadcaster 192.168.1.2 foo sent 3 bytes to 192.168.1.2 $ broadcaster 192.168.1.255 foo sent 3 bytes to 192.168.1.255 $ broadcaster 255.255.255.255 foo sent 3 bytes to 255.255.255.255
In morate videti, da se listener odzove, da je dobil pakete. (Če se listener ne odzove, se lahko zgodi, ker je vezan na naslov IPv6. Poskusite spremeniti AF_UNSPEC v seznamu listener.c v AF_INET in sili IPv4.)
No, to je nekaj vznemirljivo. Zdaj pa sprožite listener na drugem računalniku poleg vas v istem omrežju, da boste imeli dve kopiji, po eno na vsakem računalniku in spet zagnati broadcaster s svojim oddajnim naslovom… Hej! Oba listener dobita paket, čeprav ste enkrat poklicali samo sendto()! Cool!
Če listener dobi podatke, ki jih pošljete neposredno nanj, ne pa tudi podatki o oddajnem naslovu, lahko pride do požarnega zidu na vašem lokalnem računalniku, ki blokira pakete. (Da, Pat in Bapper, hvala, ker ste se zavedali, preden sem to storila, zato moja vzorčna koda ni delovala. Rekel sem, da vas bom omenil v vodniku, in tu ste. nyah.)
Spet bodite previdni pri oddajnih paketih. Ker bo vsak stroj na LANu prisiljen obravnavati paket, ne glede na to, ali ga recvfrom() ali ne, lahko predstavlja precej tovora celotnemu računalniškemu omrežju. Vsekakor jih je treba uporabljati zmerno in ustrezno.
8. Pogosta vprašanja
Kje lahko dobim datoteke z glavo?
Če jih še nimate v sistemu, jih verjetno ne potrebujete. Preverite priročnik za svojo platformo. Če gradite v sistemu Windows, morate vključiti samo #include <winsock.h>.
Kaj naj storim, ko bind() poroča “Naslov že v uporabi”?
Uporabiti morate setsockopt () z možnostjo SO_REUSEADDR v vtičnici za poslušanje. Oglejte si razdelek o povezovanju () in razdelek o izbiri () za primer.
Kako dobim seznam odprtih vtičnic v sistemu?
Uporabite netstat. Preverite stran man za vse podrobnosti, vendar morate vnesti nekaj dobrih rezultatov, samo natipkate:
$ netstat
Edini trik določa, katera vtičnica je povezana s katerim programom.
Kako si lahko ogledam usmerjevalno tabelo?
Zaženite ukaz za pot (v /sbin na večini Linuxov) ali ukaz netstat -r.
Kako lahko zagnavam odjemalske in strežniške programe, če imam samo en računalnik? Ali ne potrebujem omrežja za pisanje omrežnih programov?
Na srečo, skoraj vsi stroji izvajajo loopback omrežno “napravo”, ki sedi v jedru in se pretvarja, da je omrežna kartica. (To je vmesnik, naveden kot “lo” v usmerjevalni tabeli.)
Pretvarjaj se, da si prijavljen v stroj, imenovan “goat”. Zaženite stranko v enem oknu in strežnik v drugem. Ali pa zaženite strežnik v ozadju (“server &“) in zaženite odjemalca v istem oknu. Povzetek naprave za povratne zanke je, da lahko bodisi client goat ali client localhost (saj je “localhost” verjetno opredeljen v vaši datoteki /etc/hosts), stranko pa se bo pogovarjal s strežnikom brez omrežja!
Skratka, za katero koli kodo ni potrebna nobena sprememba, ki bi jo lahko izvajali na enem samem omrežju brez omrežja! Huzah!
Kako naj povem, ali je oddaljena stran zaprta?
Poveste lahko, ker bo recv() vrnil 0.
Kako izvajam pripomoček “ping”? Kaj je ICMP? Kje lahko najdem več o surovih vtičnicah in SOCK_RAW?
Vsa vaša vprašanja o surovih vtičnicah bodo odgovorjena v W. Richard Stevensovih programskih knjigah UNIX Networking. Oglejte si tudi ping/ podimenik v izvorni kodi programa Stevens UNIX Networking, ki je na voljo na spletu.
Kako spremeniti ali skrajšati časovno omejitev klica za connect()?
Namesto da bi vam povedal ravno enak odgovor, ki bi vam ga dal W. Richard Stevens, vas bom lib/ connect_nonb.c v izvorni kodi UNIX omrežnega programja.
Bistvo tega je, da naredite vtičnico socket z socket(), jo nastavite na ne-blokiranje, pokličite connect() in če vse gre dobro connect() bo takoj vrnil -1, errno pa bo nastavljen na EINPROGRESS. Nato pokličete select() s katerim koli časovnim časom, ki ga želite, pri čemer deskriptor vtičnice v bralnih in zapisovalnih nizih. Če ne prekine, pomeni, da je klic vzpostavljen. Na tej točki boste morali uporabiti getockopt() z možnostjo SO_ERROR, da dobite povratno vrednost iz klica connect(), ki bi morala biti nič, če ni bilo napake.
Nazadnje boste verjetno želeli, da se vtičnica znova blokira, preden začnete prenašati podatke prek nje.
Upoštevajte, da ima to dodatno prednost, ker program omogoča, da naredi nekaj drugega, medtem ko se povezuje. Lahko bi na primer nastavili časovno omejitev na nekaj nizkega, na primer 500 ms, in vsakič posodobite indikator na zaslonu, nato ponovno pokličite select(). Ko pokličete select() in časovno omejitev, recimo 20-krat, boste vedeli, da je čas, da prekinete povezavo.
Kot sem rekel, preverite izvor Stevensa za odličen primer.
Kako lahko izdelam sistem Windows?
Najprej izbrišite Windows in namestite Linux ali BSD. } ;-). Ne, pravzaprav samo v razdelku o stavbi za Windows v uvodu.
Kako zgradim za Solaris/SunOS? Nadaljujem z napakami povezovalca, ko poskušam sestaviti!
Napake povezovalca se zgodijo, ker Sunbox-i ni samodejno sestavljen v knjižnicah vtičnic. Oglejte si poglavje o zgradbi za Solaris/SunOS v uvodu za primer, kako to storiti.
Zakaj select() še naprej pada na signal?
Signali pogosto povzročijo blokirane sistemske klice za vrnitev -1 z errno nastavljenim na EINTR. Ko nastavite obdelovalec signala s sigaction (), lahko nastavite zastavo SA_RESTART, ki naj bi znova zagnala sistemski klic po prekinitvi.
Seveda to ne deluje vedno.
Moja najljubša rešitev za to vključuje izjavo goto. Saj veš, da to razdira svoje profesorje do konca, zato pojdi na to!
select_restart: if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) { if (errno == EINTR) { // neki signal nas je prekinil, zato ga ponovno zaženite goto select_restart; } // tukaj je resnična napaka: perror("select"); }
Seveda vam v tem primeru ni treba uporabljati goto; Lahko uporabite druge strukture za nadzor. Ampak mislim, da je izjava goto dejansko čistejša.
Kako lahko izvajam časovno omejitev za klic na recv()?
Uporabite select()! Omogoča vam, da določite časovni parameter za deskriptorje sokov, iz katerega želite brati. Lahko pa celotno funkcijo zavijete v eno samo funkcijo, na primer:
#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; // nastavite datotečni deskriptor FD_ZERO(&fds); FD_SET(s, &fds); // nastavite struct timeval za časovno omejitev tv.tv_sec = timeout; tv.tv_usec = 0; // wait until timeout or data received n = select(s+1, &fds, NULL, NULL, &tv); if (n == 0) return -2; // timeout! if (n == -1) return -1; // error // podatki morajo biti tukaj, zato naredite normalno recv() return recv(s, buf, len, 0); } . . . // Vzorec klic v recvtimeout(): n = recvtimeout(s, buf, sizeof buf, 10); // 10 second timeout if (n == -1) { // prišlo je do napake perror("recvtimeout"); } else if (n == -2) { // prišlo je do zakasnitve } else { // dobil nekaj podatkov v buf } . . .
Upoštevajte, da se recvtimeout() v primeru časovne omejitve vrne -2. Zakaj ne bi vrnili 0? No, če se spomnite, je povratna vrednost 0 na pozivu za recv() pomeni, da je oddaljena stran zaprla povezavo. Torej, za to vrnjeno vrednost že govorimo, in -1 pomeni “napako”, zato sem izbral -2 kot svoj časovni indikator.
Kako šifriram ali stisnem podatke, preden jih pošljem skozi vtičnico?
Eden preprostih načinov za šifriranje je uporaba SSL-ja (varna vtičnica), vendar to ni v obsegu tega vodnika. (Za več informacij si oglejte projekt OpenSSL.)
Toda ob predpostavki, da želite priključiti ali implementirati svoj kompresor ali šifrirni sistem, je samo razmišljanje o vaših podatkih, ki potekajo skozi zaporedje korakov med obema koncema. Vsak korak na nek način spremeni podatke.
Zdaj pa obratno:
Če boste stisnili in šifrirali, najprej ne zapišite stiskanja.
Dokler stranka pravilno razveljavi, kaj stori, bo stanje na koncu konec, ne glede na to, koliko vmesnih korakov dodate.
Torej, vse, kar morate storiti, da uporabite mojo kodo, je najti mesto med tem, kje se podatki berejo in podatki se pošiljajo (z uporabo send()) preko omrežja, in vnesite kodo, ki tam šifrira.
Kaj je to “PF_INET”, ki ga vidim? Je to povezano z AF_INET?
Ja, ja. Za podrobnosti si oglejte razdelek socket().
Kako lahko napišem strežnik, ki sprejema ukaze lupine od odjemalca in jih izvaja?
Za poenostavitev lahko rečemo, da sta povezava connect() s, send() s in close() s povezava (to pomeni, da poznejših sistemskih klicev brez ponovnega povezovanja odjemalca ni.)
Postopek, ki mu ga sledi, je naslednji:
Medtem strežnik obdeluje podatke in jih izvaja:
Pazite! Obstajati, da strežnik izvede, kar pravi odjemalec, je, kot da daje oddaljeni dostop do lupine, in ljudje lahko v računu delajo stvari, ko se povezujejo s strežnikom. Na primer, v zgornjem primeru, kaj če odjemalec pošlje “rm -rf ~“? Izbriše vse v računu, to je tisto!
Torej dobite pametno in strankam preprečite, da bi uporabljali katerokoli drugo, razen za nekaj pripomočkov, za katere veste, da so varni, na primer foobar korist:
if (!strncmp(str, "foobar", 6)) { sprintf(sysstr, "%s > /tmp/server.out", str); system(sysstr); }
Toda še vedno ste nevarni, žal: kaj če stranka vstopi v “foobar; rm -rf ~“? Najvarnejša stvar je, da napišemo malo rutino, ki v argumentih za ukaz namesti znak za izhod v sili (“\”) pred vsemi ne-alfanumeričnimi znaki (vključno s presledki, če je to primerno).
Kot vidite, je varnost zelo velika težava, ko strežnik začne izvajati stvari, ki jih odjemalec pošilja.
Pošiljam množico podatkov, a ko sem recv(), prejmejo samo 536 bajtov ali 1460 bajtov hkrati. Ampak, če ga zaganjam na lokalnem računalniku, hkrati dobim vse podatke. Kaj se dogaja?
Hitreje pritisnete na MTU-največjo velikost, ki jo lahko nosi fizični medij. Na lokalnem računalniku uporabljate napravo za povratno zanko, ki lahko obdeluje 8K ali več brez problema. Toda na ethernetu, ki lahko obvlada le 1500 bajtov z glavo, si zadel to omejitev. Nad modemom, s 576 MTU (spet z glavo), ste zadeli še nižjo mejo.
Najprej morate poskrbeti, da bodo vsi podatki poslani. (Za podrobnosti glej izvedbo funkcije sendall(). Ko ste prepričani o tem, potem morate poklicati recv() v zanki, dokler niso vsi vaši podatki brati.
Preberite poglavje Son of Data Encapsulation za podrobnosti o prejemu popolnih paketov podatkov z več klicev na recv().
Jaz sem na okencu Windows in nimam sistemskega klica fork () ali kakršnega koli strukturnega pomena. Kaj storiti?
Če so kje drugje, bodo v knjižnicah POSIX, ki so morda poslane s svojim prevajalnikom. Ker nimam okenca Windows, vam resnično ne morem povedati odgovora, vendar se mi zdi, da se spomnimo, da Microsoft ima sloj združljivosti POSIX in to je, če bi bila fork (). (In morda celo sigaction.)
Poiščite pomoč, ki je bila priložena VC ++ za “vilice” ali “POSIX”, in preverite, ali vam poda kakršne koli sledi.
Če to sploh ne deluje, odklopite fork()/sigaction stvari in ga nadomestite z ekvivalentom Win32: CreateProcess (). Ne vem, kako uporabljati CreateProcess() – potrebuje bazilionske argumente, vendar jih je treba vključiti v dokumente, ki so bili priloženi VC ++.
Jaz sem za požarni zid, kako naj ljudem zunaj požarnega zidu poznam svoj naslov IP, da se lahko povežejo z mojim računalnikom?
Na žalost je namen požarnega zidu preprečiti, da bi se ljudje zunaj požarnega zidu povezali z napravami znotraj požarnega zidu, zato jim je omogočeno, da se to v bistvu šteje za kršitev varnosti.
To ne pomeni, da je vse izgubljeno. Na eni strani lahko še vedno connect() preko požarnega zidu, če gre za nekakšno masquerading ali NAT ali kaj podobnega. Samo oblikujte svoje programe, tako da boste vedno tisti, ki sproži povezavo, in vam bo v redu.
Če to ni zadovoljivo, lahko zaprosite, da vaši sysadmini zapreti luknjo v požarnem zidu, da se bodo ljudje lahko povezali z vami. Požarni zid vam lahko pošlje bodisi prek svoje programske opreme NAT bodisi preko proxyja ali podobnega.
Zavedajte se, da luknja v požarnem zidu ni nič, kar je treba vzeti rahlo. Zagotoviti morate, da ljudem ne daste dostopa do notranjega omrežja; če ste začetnik, je veliko težje narediti programsko opremo varno, kot si mislite.
Ne skrbi za tvojega sysadmina.
Kako napišem sniffer v paketu? Kako vnesem svoj ethernet vmesnik v brezpotjujoči način?
Za tiste, ki ne vedo, ko je omrežna kartica v “promiskuni način”, bo posredovala VSE pakete operacijskemu sistemu, ne samo tistim, ki so bili naslovljeni na ta določen računalnik. (Govorimo o eternetskih naslovih, ne naslovih IP, ampak ker je ethernet nižji od IP-ja, so tudi vsi IP-naslovi učinkovito posredovani. Za več informacij si oglejte poglavje Nizka raven nesmisel in mrežna teorija.)
To je osnova za delovanje souporabnika paketa. Vmesnik postavi v promiskuni način, nato pa OS dobi vsak posamezen paket, ki gre na žico. Imeli boste vtičnico neke vrste, iz katere lahko preberete te podatke.
Na žalost se odgovor na vprašanje razlikuje glede na platformo, vendar če ste Google za, na primer, “windows promiscuous ioctl”, boste verjetno dobili nekje. Obstaja tisto, kar izgleda kot dostojno pisanje v Linux Journal, kot tudi.
Kako lahko nastavim časovno omejitev po meri za vtičnico TCP ali UDP?
Odvisno je od vašega sistema. Morda iščete omrežje za SO_RCVTIMEO in SO_SNDTIMEO (za uporabo s setsockopt()), da vidite, ali vaš sistem podpira takšno funkcionalnost.
Stran za mano Linuxa predlaga uporabo alarm() ali setitimer() kot nadomestka.
Kako naj povem, katera vrata so na voljo za uporabo? Ali obstaja seznam “uradnih” številk vrat?
Ponavadi to ni težava. Če pišete, recimo, spletni strežnik, potem je dobro, da uporabite znana vrata 80 za vašo programsko opremo. Če pišete samo svoj lasten specializiran strežnik, nato izberite naključno izbrano pristanišče (vendar večje od 1023) in poskusite ga.
Če je pristanišče že v uporabi, boste pri poskusu bind() dobili napako “Naslov že v uporabi”. Izberite drugo pristanišče. (Dobra ideja je, da uporabniku programske opreme omogočite, da določi nadomestna vrata s konfiguracijsko datoteko ali stikalom v ukazni vrstici.)
Obstaja seznam uradnih številk vrat, ki jih vzdržuje organ za dodeljene številke na internetu (IANA). Samo zato, ker nekaj (več kot 1023) v tem seznamu ne pomeni, da ne morete uporabljati pristanišča. Na primer, DOOM-ov program DOOM uporablja isto pristanišče kot “mdqs”, karkoli je to. Vse je pomembno, da nihče drug na istem računalniku ne uporablja tega pristanišča, kadar ga želite uporabiti.
9. Man strani
V Unixovem svetu obstaja veliko priročnikov. Imajo majhne dele, ki opisujejo posamezne funkcije, ki jih imate na voljo.
Seveda bi bilo priročno prevec stvar, ki bi jo bilo treba natipati. Mislim, nihče v svetu Unixa, vključno z mano, rad všeč. Dejansko bi lahko nadaljeval in v veliki meri o tem, koliko raje raje, ampak na kratko in ne bom vam dolgčas z dolgimi vetrovi o tem, kako popolnoma iznenađujuće bi bilo, da sem v skoraj vseh okoliščinah v celoti.
[Aplavz]
Hvala vam. O čem prihajajo, je, da se te strani v Unix svetu imenujejo »man strani« in tu sem vključil svojo osebno skrajšano različico za vaše branje užitek. Stvar je v tem, da so mnoge od teh funkcij bolj splošni namen, kot sem jih pustil, vendar bom predstavil samo dele, ki so pomembni za programiranje internetnih vtičnic.
Ampak počakaj! To ni vse, kar je narobe z mojimi stranmi:
Če želite resnične informacije, preverite svoje lokalne strani Unixa, tako da vnesete man karkoli, kjer je “karkoli” nekaj, za kar ste neverjetno zainteresirani, na primer “sprejem”. (Prepričan sem, da ima Microsoft Visual Studio nekaj podobnega v svojem oddelku za pomoč. Toda “človek” je boljši, ker je en bajt bolj jedrnat kot “pomoč”.
Torej, če so to napačne, zakaj jih celo sploh vključite v vodnik? No, obstaja nekaj razlogov, vendar je najboljše, da (a) so te različice posebej usmerjene v mrežno programiranje in jih je lažje prebaviti kot resnične, in (b) te različice vsebujejo primere!
Oh! Če govorim o primerih, ne skušam v celoti preveriti napake, ker resnično poveča dolžino kode. Vendar morate popolnoma preveriti napake, kadar koli delate sistemske klice, razen če ste popolnoma 100% prepričani, da to ne bo uspelo, verjetno pa bi morali storiti tudi takrat!
9.1. accept()
Sprejemajte dohodno povezavo v vtičnici za poslušanje
Prototipi
#include <sys/types.h> #include <sys/socket.h> int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
Opis
Ko enkrat preizkusite težave pri pridobivanju vtičnice SOCK_STREAM in ga nastavite za dohodne povezave s listen (), potem pokličete accept(), da dejansko dobite nov deskriptor sokov, ki ga boste uporabili za nadaljnjo komunikacijo z novo povezanim odjemalcem.
Stara vtičnica, ki jo uporabljate za poslušanje, je še vedno prisotna in bo uporabljena za nadaljnje sprejemanje accept(), ko pridejo.
accept () bo ponavadi blokiral, lahko pa uporabite select () in si ogledate, ali je pripravljen za branje. Če je tako, potem obstaja nova povezava, ki čaka na accept()! Juhu! Lahko pa nastavite tudi zastavo O_NONBLOCK v vtičnico za poslušanje z uporabo fcntl() in potem ne bo nikoli blokirala, namesto da bi se vrnila -1 z errno nastavljeno na EWOULDBLOCK.
Deskriptor vtičnice, ki ga vrne accept(), je deskriptor bona fide, ki je odprt in povezan z oddaljenim gostiteljem. Zapreti jo close(), ko končate z njo.
Vrnjena vrednost
accept() vrne novo povezani deskriptor vtiča ali -1 pri napaki, z ustrezno nastavitvijo errno.
Primer
struct sockaddr_storage their_addr; socklen_t addr_size; struct addrinfo hints, *res; int sockfd, new_fd; // najprej dodajte naslove struktur z getaddrinfo(): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // fill in my IP for me getaddrinfo(NULL, MYPORT, &hints, &res); // naredite vtičnico, jo povežite in poslušajte: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); bind(sockfd, res->ai_addr, res->ai_addrlen); listen(sockfd, BACKLOG); // zdaj sprejmite dohodno povezavo: addr_size = sizeof their_addr; new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size); // pripravljen za komunikacijo na deskriptorju socketa new_fd!
Poglej tudi
socket(), getaddrinfo(), listen(), struct sockaddr_in
9.2. bind()
Pridružite vtičnico z naslovom IP in številko vrat
Prototipi
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
Opis
Ko se oddaljeni računalnik želi povezati s strežniškim programom, potrebuje dva podatka: naslov IP in številko vrat. Klic bind() vam omogoča, da storite samo to.
Najprej pokličete getaddrinfo(), da naložite struct sockaddr z naslovnim naslovom in podatki o pristaniščih. Potem pokličete socket(), da dobite deskriptor vtiča in nato prenesete vtičnico in naslov v povezavo(), IP naslov in vrata pa so čarobno (z uporabo dejanske magije), ki se vežejo v vtičnico!
Če ne poznate svojega IP-naslova ali če v napravi imate samo en naslov IP-ja ali vam ni všeč, kateri od naslovov IP-naslova naprave je uporabljen, lahko preprosto prenesete zastavico AI_PASSIVE v parameter za namige na getaddrinfo(). Kaj to naredi je, da izpolnite del IP-naslov struct sockaddr s posebno vrednostjo, ki pove bind(), da mora samodejno izpolniti IP-naslov tega gostitelja.
Kaj kaj? Kakšna posebna vrednost je naložena v IP naslov struct sockaddr, da bo samodejno izpolnjevala naslov s trenutnim gostiteljem? Povedal vam bom, vendar upoštevajte, da je to le, če ste ročno izpolnili struct sockaddr; če ne, uporabite rezultate getaddrinfo(), kot je navedeno zgoraj. V IPv4 je polje sin_addr.s_addr strukture struct sockaddr_in nastavljeno na INADDR_ANY. V IPv6 je polje sin6_addr strukture struct sockaddr_in6 dodeljeno iz globalne spremenljivke in6addr_any. Ali pa, če deklarirate novo strukturo in6_addr, jo lahko inicializirate na IN6ADDR_ANY_INIT.
Nazadnje, parameter addrlen mora biti nastavljen na sizeof my_addr.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
// sodoben način opravljanja stvari z getaddrinfo() struct addrinfo hints, *res; int sockfd; // najprej dodajte naslove struktur z getaddrinfo(): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // uporabite IPv4 ali IPv6, kar koli hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // izpolnite moj IP za mene getaddrinfo(NULL, "3490", &hints, &res); // naredite vtičnico: // (morate dejansko hoditi po "res" povezanem seznamu in preverjanju napak!) sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); // ga povežite s pristaniščem, v katerega smo vstopili, da dobimo getaddrinfo(): bind(sockfd, res->ai_addr, res->ai_addrlen);
// primer pakiranja strukture z roko, IPv4 struct sockaddr_in myaddr; int s; myaddr.sin_family = AF_INET; myaddr.sin_port = htons(3490); // lahko določite naslov IP: inet_pton(AF_INET, "63.161.169.137", &(myaddr.sin_addr)); // ali ga lahko samodejno izberete: myaddr.sin_addr.s_addr = INADDR_ANY; s = socket(PF_INET, SOCK_STREAM, 0); bind(s, (struct sockaddr*)&myaddr, sizeof myaddr);
Poglej tudi
getaddrinfo(), socket(), struct sockaddr_in, struct in_addr
9.3. connect()
Povežite vtičnico s strežnikom
Prototipi
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
Opis
Ko naredite vtičnico socket s klicem socket(), lahko connect() to vtičnico z oddaljenim strežnikom z uporabo imenovanega sistemskega klica connect(). Vse, kar morate storiti, je, da mu pošljete deskriptor vtičnice in naslov strežnika, ki vas zanima bolje spoznati. (Oh in dolžina naslova, ki se običajno prenese na takšne funkcije.)
Običajno se te informacije pojavijo kot rezultat klica za getaddrinfo(), vendar lahko, če želite, izpolnite svoj lastni struct sockaddr.
Če v deskriptorju vtičnice še niste klicali bind(), se samodejno poveže z vašim naslovom IP in naključnim lokalnim vratom. To je ponavadi v redu z vami, če niste strežnik, saj vam res ne zanima, kakšna je vaša lokalna vrata; samo skrbi, kaj je oddaljena vrata, zato jo lahko postavite v parameter serv_addr. Lahko pokličete bind(), če resnično želite, da je vaša vtičnica za stranke na določenem IP-naslovu in pristanišču, vendar je to zelo redko.
Ko je vtičnica connect() ed, lahko send() in recv() podatke o njem na vsebino vašega srca.
Posebna opomba: če connect() UOCK vtič SOCK_DGRAM na oddaljeni gostitelj, lahko uporabite send() in recv() ter sendto() in recvfrom(). Če želiš.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
// connect to www.example.com port 80 (http) struct addrinfo hints, *res; int sockfd; // najprej z naslovom getaddrinfo () naložite naslovne strukture(): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_STREAM; // lahko namesto "http" v naslednji vrstici postavimo "80": getaddrinfo("www.example.com", "http", &hints, &res); // naredite vtičnico: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); // ga povežite z naslovom in pristaniščem, ki smo ga prenesli v getaddrinfo(): connect(sockfd, res->ai_addr, res->ai_addrlen);
Poglej tudi
socket(), bind()
9.4. close()
Zaprite deskriptor vtičnice
Prototipi
#include <unistd.h> int close(int s);
Opis
Ko končate s pomočjo vtičnice za kakršnokoli dementno shemo, ki ste jo sestavili, in ne želite send() ali recv(), ali pa sploh karkoli naredite s vtičnico, jo lahko close() in osvobojena bo, nikoli več ne bo uporabljena.
Oddaljena stran lahko pove, ali se to zgodi na dva načina. Ena: če oddaljena stran pokliče recv(), bo vrnila 0. Dva: če oddaljena stran kliče send(), bo prejela signal SIGPIPE in send() se bo vrnil -1 in errno bo nastavljen na EPIPE.
Uporabniki operacijskega sistema Windows: funkcija, ki jo potrebujete, se imenuje closesocket (), ne pa close(). Če poskušate uporabiti close() v deskriptorju vtičnice, je mogoče, da se bo Windows razjezil… In vam ni všeč, ko je jezen.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
s = socket(PF_INET, SOCK_DGRAM, 0); . . . // veliko lotta stvari...*BRRRONNNN!* . . . close(s); // res ne.
Poglej tudi
socket(), shutdown()
9.5. getaddrinfo(), freeaddrinfo(), gai_strerror()
Pridobite informacije o imenu in/ali storitvi gostitelja in naložite struct sockaddr z rezultatom.
Prototipi
#include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res); void freeaddrinfo(struct addrinfo *ai); const char *gai_strerror(int ecode); struct addrinfo { int ai_flags; // AI_PASSIVE, AI_CANONNAME, ... int ai_family; // AF_xxx int ai_socktype; // SOCK_xxx int ai_protocol; // 0 (auto) or IPPROTO_TCP, IPPROTO_UDP socklen_t ai_addrlen; // dolžina ai_addr char *ai_canonname; // kanonično ime za nodename struct sockaddr *ai_addr; // binarni naslov struct addrinfo *ai_next; // naslednjo strukturo na povezanem seznamu };
Opis
getaddrinfo() je izvrstna funkcija, ki bo vrnila podatke o določenem gostitelju (na primer njenemu IP naslovu) in naložila strukturo sockaddr za vas, skrbela za podrobne podatke (npr. Če je to IPv4 ali IPv6.) Nadomesti Stare funkcije gethostbyname () in getservbyname (). Opis spodaj vsebuje veliko informacij, ki bi lahko bile malo zastrašujoče, dejanska uporaba pa je precej preprosta. Morda bi bilo vredno, da najprej preverite primere.
Ime gostitelja, ki vas zanima, gre v parameter nodename. Naslov je lahko ime gostitelja, na primer “www.example.com” ali naslov IPv4 ali IPv6 (posredovan kot niz). Ta parameter je lahko tudi NULL, če uporabljate zastavico AI_PASSIVE (glejte spodaj).
Parameter servname je v bistvu številka vrat. Lahko je številka vrat (sprejeta kot niz, npr. “80”), ali pa je lahko ime storitve, na primer “http” ali “tftp” ali “smtp” ali “pop” itd. lahko najdete na seznamu pristanišč IANA ali v datoteki /etc/services.
Končno, za vhodne parametre imamo namige. To je res, kjer lahko določite, kaj bo storila funkcija getaddrinfo(). Nič celotne strukture pred uporabo z memset(). Oglejmo si polja, ki jih morate nastaviti pred uporabo.
Ai_flags je mogoče nastaviti na različne stvari, vendar je nekaj pomembnih. (Večkratne zastavice lahko določite z bitno-OR jih skupaj z operaterjem.) Preverite svojo stran s strani za celoten seznam zastavic.
AI_CANONNAME povzroči, da se ai_canonname rezultata izpopolni s kanonskim (dejanskim) imenom gostitelja. AI_PASSIVE povzroči, da se IP-naslov rezultata zapolni z INADDR_ANY (IPv4) ali in6addr_any (IPv6); to povzroči poznejši klic pri povezovanju (), da samodejno izpolni naslov IP strukture sockaddr z naslovom trenutnega gostitelja. To je odlično za nastavitev strežnika, ko ne želite trdno kodirati naslova.
Če uporabljate zastavo AI_PASSIVE, lahko v nodename prenese NULL (ker ga bo bind() za vas pozneje izpolnil.)
Če nadaljujete z vnosnimi parametri, boste verjetno želeli nastaviti ai_family na AF_UNSPEC, ki pove, da getaddrinfo() poišče naslove IPv4 in IPv6. Lahko se tudi omejite na enega ali drugega z AF_INET ali AF_INET6.
Nato je treba polje socktype nastaviti na SOCK_STREAM ali SOCK_DGRAM, odvisno od katere vrste vtičnice želite.
Nazadnje, pustite ai_protocol na 0, da samodejno izberete vrsto protokola.
Zdaj, ko dobite vse te stvari tam, lahko končno pokličete na getaddrinfo()!
Seveda se tukaj začne zabava. Res bo zdaj usmeril na povezani seznam struct addrinfos in skozi ta seznam lahko preberete vse naslove, ki se ujemajo s tem, kar ste prenesli z namigi.
Zdaj je mogoče dobiti nekaj naslovov, ki ne delujejo zaradi enega ali drugega razloga, zato je to, kaj naredi Linux man page, z listanjem klicanje socket() in connect() (ali bind()), ponovno nastavite strežnik z zastavico AI_PASSIVE), dokler ne uspe.
Končno, ko končate s povezanim seznamom, morate pokličite freeaddrinfo(), da sprostite pomnilnik (ali bo ušel, nekateri ljudje pa bodo vznemirjeni.)
Vrnjena vrednost
Vrne nič na uspeh, ali nenavadno pri napaki. Če vrne nenavadno, lahko uporabite funkcijo gai_strerror(), da v povratni vrednosti dobite različico, ki jo je mogoče natisniti.
Primer
// kodo za odjemalca, ki se povezuje s strežnikom // in sicer vtičnica za pretok www.example.com na port 80 (http) // either IPv4 or IPv6 int sockfd; struct addrinfo hints, *servinfo, *p; int rv; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use AF_INET6 to force IPv6 hints.ai_socktype = SOCK_STREAM; if ((rv = getaddrinfo("www.example.com", "http", &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); exit(1); } // zopet prek vseh rezultatov in se povežite s prvim, kar lahko for(p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("socket"); continue; } if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { perror("connect"); close(sockfd); continue; } break; // če pridemo sem, smo se morali uspešno povezati } if (p == NULL) { // na koncu seznama ni bilo povezave fprintf(stderr, "failed to connect\n"); exit(2); } freeaddrinfo(servinfo); // vse je bilo opravljeno s to strukturo
// kodo za strežnik, ki čaka na povezave // in sicer tokovno vtičnico na pristanišču 3490, na tem gostiteljevem IP-naslovu // bodisi IPv4 ali IPv6. int sockfd; struct addrinfo hints, *servinfo, *p; int rv; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // uporabite AF_INET6 za silo IPv6 hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // uporabite moj naslov IP if ((rv = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); exit(1); } // zanke skozi vse rezultate in se povežite s prvim, kar lahko for(p = servinfo; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("socket"); continue; } if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); perror("bind"); continue; } break; // če pridemo sem, smo se morali uspešno povezati } if (p == NULL) { // zanemarili konec seznama brez uspešnega povezovanja fprintf(stderr, "failed to bind socket\n"); exit(2); } freeaddrinfo(servinfo); // vse je bilo opravljeno s to strukturo
Poglej tudi
gethostbyname(), getnameinfo()
9.6. gethostname ()
Vrne ime sistema
Prototipi
#include <sys/unistd.h> int gethostname(char *name, size_t len);
Opis
Vaš sistem ima ime. Vsi delajo. To je nekoliko bolj Unixy stvar kot ostalo mrežno stvari, o katerih smo govorili, vendar še vedno ima svoje uporabe.
Na primer, lahko dobite ime gostitelja in pokličite gethostbyname(), če želite izvedeti svoj naslov IP.
Ime parametra bi moralo usmeriti na varovalo, ki bo imelo ime gostitelja, len pa je velikost tega vmesnika v bajtih. Gethostname () ne bo prepisal konca vmesnega pomnilnika (lahko bi vrnil napako ali pa bi le prenehal pisati), NUL pa bo prekinil niz, če bi bilo prostora v pufru.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
char hostname[128]; gethostname(hostname, sizeof hostname); printf("My hostname: %s\n", hostname);
Poglej tudi
gethostbyname()
9.7. gethostbyname(), gethostbyaddr()
Pridobite naslov IP za ime gostitelja ali obratno
Prototipi
#include <sys/socket.h> #include <netdb.h> struct hostent *gethostbyname(const char *name); // DEPRECATED! struct hostent *gethostbyaddr(const char *addr, int len, int type);
Opis
OPOZORILO: ti dve funkciji nadomeščata getaddrinfo() in getnameinfo()! Zlasti gethostbyname() ne deluje dobro z IPv6.
Te funkcije preslikavajo med imena gostiteljev in naslove IP. Na primer, če imate »www.example.com«, lahko uporabite gethostbyname(), da dobite njen IP naslov in ga shranite v strukturo in_addr.
Nasprotno, če imate strukturo in_addr ali strukturo in6_addr, lahko uporabite gethostbyaddr(), da dobite ime gostitelja nazaj. gethostbyaddr() je združljiv z IPv6, vendar morate namesto tega uporabiti novejšo shinier getnameinfo().
(Če imate niz, ki vsebuje naslov IP v formatu dots-and-numbers, za katerega želite poiskati ime gostitelja, bi bilo bolje, če bi uporabili getaddrinfo() z zastavico AI_CANONNAME.)
gethostbyname () traja niz, kot je “www.yahoo.com”, in vrne strukturni hostent, ki vsebuje tone informacij, vključno z naslovom IP. (Drugi podatki so uradno ime gostitelja, seznam vzdevkov, vrsta naslova, dolžina naslovov in seznam naslovov – to je splošna struktura, ki je zelo enostavna za uporabo za naše posebne namene, ko vidite, kako. )
gethostbyaddr() vzame strukturo in_addr ali struct in6_addr in vam prinese ustrezno ime gostitelja (če obstaja), zato je nekako v obratni smeri gethostbyname(). Kar zadeva parametre, čeprav je addr char *, dejansko želite prenesti kazalec na strukturo in_addr. len mora biti sizeof (struct in_addr), tip naj bo AF_INET.
Torej, kaj je ta struktura gostiteljica, ki se vrne? Ima številna polja, ki vsebujejo informacije o zadevnem gostitelju.
char *h_name | Resnično kanonično ime gostitelja. |
char **h_aliases | Seznam vzdevkov, do katerih lahko dostopate nizi – zadnji element je NULL |
int h_addrtype | Vrsto naslova rezultata, ki bi res moral biti AF_INET za naše namene. |
int length | Dolžina naslovov v bajtih, ki je 4 za IP naslov (različica 4). |
char **h_addr_list | Seznam naslovov IP za ta gostitelj. Čeprav to je char**, to je res vrsta struct in_addr*s v preobleki. Zadnji element elementa je NULL. |
h_addr | Splošno opredeljen vzdevek za h_addr_list[0]. Če želite le za stari naslov IP gostitelja (ja, lahko imajo več kot enega) samo uporabite to polje. |
Vrnjena vrednost
Vrne kazalec na rezultat strukturnega gostitelja na uspeh ali NULL pri napaki.
Namesto običajnega perror() in vseh tistih stvari, ki bi jih običajno uporabljali za poročanje o napakah, imajo te funkcije vzporedne rezultate v spremenljivki h_errno, ki jo lahko natisnete z uporabo funkcije herror() ali hstrerror(). To deluje tako kot klasične funkcije errno, perror() in strerror(), na katere ste navajeni.
Primer
// TO JE NAPRAVANA METODA ZA PRIDOBITEV HOSTNIH IMEN // namesto tega uporabite getaddrinfo ()! #include <stdio.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[]) { int i; struct hostent *he; struct in_addr **addr_list; if (argc != 2) { fprintf(stderr,"usage: ghbn hostname\n"); return 1; } if ((he = gethostbyname(argv[1])) == NULL) { // dobite informacije o gostitelju herror("gethostbyname"); return 2; } // natisne informacije o tem gostitelju: printf("Official name is: %s\n", he->h_name); printf(" IP addresses: "); addr_list = (struct in_addr **)he->h_addr_list; for(i = 0; addr_list[i] != NULL; i++) { printf("%s ", inet_ntoa(*addr_list[i])); } printf("\n"); return 0; }
// To je bilo podhranjeno // namesto tega uporabite getnameinfo ()! struct hostent *he; struct in_addr ipv4addr; struct in6_addr ipv6addr; inet_pton(AF_INET, "192.0.2.34", &ipv4addr); he = gethostbyaddr(&ipv4addr, sizeof ipv4addr, AF_INET); printf("Host name: %s\n", he->h_name); inet_pton(AF_INET6, "2001:db8:63b3:1::beef", &ipv6addr); he = gethostbyaddr(&ipv6addr, sizeof ipv6addr, AF_INET6); printf("Host name: %s\n", he->h_name);
Poglej tudi
getaddrinfo(), getnameinfo(), gethostname(), errno, perror(), strerror(), struct in_addr
9.8. getnameinfo()
Poiščite ime gostitelja in ime storitve za dano struct sockaddr.
Prototipi
#include <sys/socket.h> #include <netdb.h> int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);
Opis
Ta funkcija je nasprotna od getaddrinfo(), to pomeni, da ta funkcija vzame že naložen struct sockaddr in na njej išče ime in ime storitve. Zamenja stare funkcije gethostbyaddr() in getservbyport().
Morate prenesti kazalec na struct sockaddr (ki je v resnici verjetno struktura sockaddr_in ali struct sockaddr_in6, ki ste jo oddali) v parametru sa, in dolžino te strukture v salenu.
Posledično ime gostitelja in ime storitve se zapišejo na območje, na katerega se opozarjajo parametri gostitelja in servisa. Seveda morate navesti maksimalne dolžine teh pufrov v hostlen in servlen.
Končno lahko pretehtate več zastave, vendar pa nekaj dobrih. NI_NOFQDN bo povzročil, da gostitelj vsebuje samo ime gostitelja, ne celotno ime domene. NI_NAMEREQD povzroči, da funkcija ne uspe, če ni mogoče najti imena z iskanjem po DNS-ju (če ne navedete te zastavice in ne najdete imena, getnameinfo() bo v gostiteljico postavil nizno različico IP-naslova .)
Kot vedno preverite lokalne strani strani za celotno zajemanje.
Povratna vrednost
Vrne nič na uspeh, ali pa ni nič na napaki. Če je povratna vrednost nič, se lahko prenese na gai_strerror(), da dobi človeško berljiv niz. Za več informacij glejte getaddrinfo.
Primer
struct sockaddr_in6 sa; // could be IPv4 if you want char host[1024]; char service[20]; // pretvarjaj se, da je poln dobre informacije o gostitelju in pristanišču... getnameinfo(&sa, sizeof sa, host, sizeof host, service, sizeof service, 0); printf(" host: %s\n", host); // e.g. "www.example.com" printf("service: %s\n", service); // e.g. "http"
Poglej tudi
getaddrinfo(), gethostbyaddr()
9.9. getpeername()
Vrnite naslov informacije o oddaljeni strani povezave
Prototipi
#include <sys/socket.h> int getpeername(int s, struct sockaddr *addr, socklen_t *len);
Opis
Ko accept() ed oddaljeno povezavo ali connect() ed s strežnikom, imate zdaj tisto, kar je znano kot peer. Vaš kolega je preprosto računalnik, s katerim ste povezani, označen z naslovom IP in vmesnikom. Torej …
getpeername() preprosto vrne strukturo sockaddr_in, napolnjeno z informacijami o računalniku, s katerim ste povezani.
Zakaj se imenuje “ime”? No, obstaja veliko različnih vrst vtičnic, ne samo internetnih vtičnic, kot jih uporabljamo v tem vodniku, zato je bilo “ime” dober splošni izraz, ki je zajel vse primere. V našem primeru pa je “ime” vrstnika, to je IP naslov in vrata.
Čeprav funkcija vrne velikost nastalega naslova v len, morate prednaložiti len z velikostjo addr.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
// predpostavimo, da je s povezana vtičnica socklen_t len; struct sockaddr_storage addr; char ipstr[INET6_ADDRSTRLEN]; int port; len = sizeof addr; getpeername(s, (struct sockaddr*)&addr, &len); // se ukvarjajo z IPv4 in IPv6: if (addr.ss_family == AF_INET) { struct sockaddr_in *s = (struct sockaddr_in *)&addr; port = ntohs(s->sin_port); inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof ipstr); } else { // AF_INET6 struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; port = ntohs(s->sin6_port); inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr); } printf("Peer IP address: %s\n", ipstr); printf("Peer port : %d\n", port);
Poglej tudi
gethostname(), gethostbyname(), gethostbyaddr()
9.10. errno
Drži kodo napake za zadnji sistemski klic
Prototipi
#include <errno.h> int errno;
Opis
To je spremenljivka, ki vsebuje podatke o napaki za veliko sistemskih klicev. Če se boste spomnili, stvari, kot so socket() in listen(), vrnejo -1 na napako in nastavijo točno vrednost errno, da bi vas posebej obvestili o napaki.
V naslovni datoteki errno.h so naštete kup stalnih simboličnih imen za napake, kot so EADDRINUSE, EPIPE, ECONNREFUSED itd. Strani vašega lokalnega človeka vam bodo povedali, katere kode lahko vrnete kot napako, in jih lahko uporabite ob času zagona Da na različne načine obravnavajo različne napake.
Ali pa bolj pogosto lahko pokličete perror() ali strerror(), da dobite človeško berljivo različico napake.
Ena stvar, ki je za vas večnitne navdušence, je, da je na večini sistemov errno definirana na nitno varen način. (To pomeni, da to dejansko ni globalna spremenljivka, ampak se obnaša tako, kot bi bila globalna spremenljivka v okolju z enojnim navojem.)
Povratna vrednost
Vrednost spremenljivke je najnovejša napaka, ki se je pojavila, kar je lahko koda za “uspeh”, če je zadnje dejanje uspelo.
Primer
s = socket(PF_INET, SOCK_STREAM, 0); if (s == -1) { perror("socket"); // or use strerror() } tryagain: if (select(n, &readfds, NULL, NULL) == -1) { // prišlo je do napake!! // če smo bili samo prekinjeni, preprosto ponovno zaženite klic select(): if (errno == EINTR) goto tryagain; // AAAA! goto!!! // drugače je resnejša napaka: perror("select"); exit(1); }
Poglej tudi
perror (), strerror ()
9.11. fcntl()
Kontrolni deskriptorji
Prototipi
#include <sys/unistd.h> #include <sys/fcntl.h> int fcntl(int s, int cmd, long arg);
Opis
Ta funkcija se običajno uporablja za zaklepanje datotek in druge datoteke, ki so usmerjene v datoteko, vendar ima tudi nekaj povezanih funkcij, ki jih morda občasno vidite ali uporabljate.
Parameter s je deskriptor vtičnice, na katerega želite delovati, mora biti cmd nastavljen na F_SETFL, arg pa lahko eden od naslednjih ukazov. (Kot sem rekel, obstaja več za fcntl (), kot sem pustil tukaj, vendar poskušam ostati usmerjen v vtičnico.)
O_NONBLOCK | Set the socket to be non-blocking. See the section on blocking for more details. |
O_ASYNC | Nastavite vtičnico za asinhroni vhod / izhod. Ko so podatki pripravljen biti recv()‘d na vtičnici, signal SIGIO bo dvignjen. To je redko videti, in izven obsega priročnika. Mislim, da je na voljo samo nekatere sisteme. |
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Različne uporabe sistemskega klica fcntl() dejansko imajo različne vračilne vrednosti, vendar jih tukaj nisem pokril, ker niso povezani s sokovniki. Več informacij najdete na strani lokalne strani fcntl().
Primer
int s = socket(PF_INET, SOCK_STREAM, 0); fcntl(s, F_SETFL, O_NONBLOCK); // nastavljeno na ne-blokiranje fcntl(s, F_SETFL, O_ASYNC); // nastavljen na asinhroni V / I
Poglej tudi
Blocking, send()
9.12. htons(), htonl(), ntohs(), ntohl()
Pretvori več-bajtne celoštevilske vrste iz vrstnega reda gostitelja v omrežni vrstni red
Prototipi
#include <netinet/in.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
Opis
Samo zato, da bi bili res nesrečni, različni računalniki uporabljajo različna bajtna naročila za svoje multibyte celo število (to je vsako celo število, ki je večje kot znak.) Povzetek tega je, da če send() dvomestni kratki int iz Intel okvira na Mac (preden so postali tudi Intel škatle, mislim tudi) mislim, da je eden od računalnikov številka 1, druga pa bo pomislila na 256 in obratno.
Način, kako se lotiti tega problema je, da vsi razkrijejo svoje razlike in se strinjajo, da sta Motorola in IBM imela prav, Intel pa je to naredil čuden način, zato smo vse svoje pretvorbe bajtov pretvorili v “velikansko”, preden jih pošljemo Ven. Ker je Intel maloštevilen stroj, je veliko bolj politično pravilna, da pokličete našo prednostno bajtno naročanje “Network Byte Order”. Torej te funkcije pretvorijo iz vašega domačega bajtnega naloga v omrežni vrstni red in nazaj.
(To pomeni, da te funkcije na Intelih te funkcije zamenjajo vse bajte okoli sebe, pri PowerPC pa ne storijo ničesar, ker so bajti že v omrežnem vrstnem redu, vendar jih morate vedno uporabljati v kodi, saj bi ga nekdo želel zgraditi na Intel stroj in še vedno delujejo pravilno.)
Upoštevajte, da so vključene vrste 32-bitne (4-bajtne, verjetno int) in 16-bitne (2-bajtne, zelo verjetno kratke) številke. 64-bitni stroji imajo lahko htonll() za 64-bitne ints, vendar jih nisem videl. Samo napisati svoje.
Kakorkoli, način delovanja teh funkcij je, da se najprej odločite, ali pretvarjate iz naročila bajtov gostitelja (vaše naprave) ali iz omrežnega vrstnega reda. Če je »gostitelj« prva črka funkcije, ki jo želite poklicati, “h”. V nasprotnem primeru je “n” za “omrežje”. Sredina imena funkcije je vedno “do”, ker pretvarjate iz enega v drugo, predzadnje pismo pa prikazuje, na kaj pretvarjate. Zadnja črka je velikost podatkov, “s” za kratko ali “l” za dolgo. Tako:
htons() | host to network short |
htonl() | host to network long |
ntohs() | network to host short |
ntohl() | network to host long |
Vrnjena vrednost
Vsaka funkcija vrne pretvorjeno vrednost.
Primer
uint32_t some_long = 10; uint16_t some_short = 20; uint32_t network_byte_order; // pretvoriti in poslati network_byte_order = htonl(some_long); send(s, &network_byte_order, sizeof(uint32_t), 0); some_short == ntohs(htons(some_short)); // this expression is true
9.13. inet_ntoa (), inet_aton (), inet_addr
Pretvorite naslove IP iz niza pik in števila v strukturo in_addr in nazaj
Prototipi
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // Vsi so zmedeni! Use inet_pton() or inet_ntop() instead!! char *inet_ntoa(struct in_addr in); int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp);
Opis
Te funkcije so zastarele, ker se ne ukvarjajo z IPv6! Namesto tega uporabite inet_ntop() ali inet_pton()! Tu so vključeni, ker jih še vedno najdemo v naravi.
Vse te funkcije se pretvorijo iz strukture in_addr (del strukture sockaddr_in, najverjetneje) v niz v formatu dots-and-numbers (npr. “192.168.5.10”) in obratno. Če imate v ukazni vrstici ali na drugem naslovu IP naslov, je to najlažji način, da dobite strukturo in_addr za povezavo() ali karkoli. Če potrebujete več moči, poskusite nekatere od funkcij DNS, na primer gethostbyname() ali poskušajte državni udar v vaši državi.
Funkcija inet_ntoa() pretvori omrežni naslov v strukturo in_addr v niz formatov dots-and-numbers. “N” v “ntoa” pomeni omrežje, a “a” pomeni ASCII iz zgodovinskih razlogov (tako je “suffix” omrežje v ASCII “toa” podoben prijatelj v knjižnici C, ki se imenuje atoi() pretvori ASCII niz v celo število.)
Funkcija inet_aton () je nasprotna, pretvorba iz niza pik in številk v in_addr_t (kar je tip polja s_addr v vaši strukturi in_addr.)
Nazadnje, funkcija inet_addr () je starejša funkcija, ki v bistvu ista stvar kot inet_aton (). Teoretično je zastarelo, vendar ga boste videli veliko in policija ne bo prišla, če jo boste uporabljali.
Vrnjena vrednost
inet_aton() vrne ničlo, če je naslov veljaven in vrne nič, če je naslov neveljaven.
inet_ntoa() vrne niz pik in številk v statični pufer, ki je prepisan z vsakim klicem v funkcijo.
inet_addr() vrne naslov kot in_addr_t ali -1, če je prišlo do napake. (To je enak rezultat, kot če ste skušali pretvoriti niz “255.255.255.255”, ki je veljaven IP-naslov. Zato je inet_aton() boljši.)
Primer
struct sockaddr_in antelope; char *some_addr; inet_aton("10.0.0.1", &antelope.sin_addr); // shranite IP v antilopa some_addr = inet_ntoa(antelope.sin_addr); // return the IP printf("%s\n", some_addr); // prints "10.0.0.1" // in ta klic je enak kot klic inet_aton () zgoraj: antelope.sin_addr.s_addr = inet_addr("10.0.0.1");
Poglej tudi
inet_ntop(), inet_pton(), gethostbyname(), gethostbyaddr()
9.14. inet_ntop(), inet_pton()
Pretvorite naslove IP v človeško berljivo obliko in nazaj.
Prototipi
#include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); int inet_pton(int af, const char *src, void *dst);
Opis
Te funkcije so za obravnavanje človeško berljivih naslovov IP in njihovo pretvorbo v njihovo binarno predstavitev za uporabo z različnimi funkcijami in sistemskimi klici. “N” pomeni “omrežje” in “p” za “predstavitev”. Ali “predstavitev besedila”. Toda to lahko misliš kot “tiskati”. “ntop” je “omrežje za tiskanje”. Vidiš?
Včasih ne gledate na kup binarnih številk, ko pogledate naslov IP. Želite to v lepi natisljivi obliki, na primer 192.0.2.180 ali 2001: db8: 8714: 3a90 :: 12. V tem primeru je inet_ntop() za vas.
inet_ntop() prevzame družino naslovov v parametru af (bodisi AF_INET ali AF_INET6). Parameter src bi moral biti kazalec na strukturo in_addr ali struct in6_addr, ki vsebuje naslov, ki ga želite pretvoriti v niz. Končno, dst in velikost sta kazalec na ciljni niz in največja dolžina tega niza.
Kakšna naj bo največja dolžina dst stringa? Kakšna je največja dolžina za naslove IPv4 in IPv6? Na srečo obstaja nekaj makrov, ki vam bodo pomagali. Največje dolžine so: INET_ADDRSTRLEN in INET6_ADDRSTRLEN.
V drugih primerih lahko imate v nizu berljive črke, ki vsebujejo naslov IP in ga želite zapakirati v struct sockaddr_in ali struct sockaddr_in6. V tem primeru je nasprotna funkcija inet_pton() tista, za katero ste.
inet_pton() ima tudi družino naslovov (AF_INET ali AF_INET6) v parametru af. Parameter src je kazalec na niz, ki vsebuje naslov IP v obliki, ki jo je mogoče natisniti. Nenazadnje parameter dst kaže, kje je treba shraniti rezultat, kar je verjetno struktura in_addr ali struct in6_addr.
Te funkcije ne opravljajo DNS lookups-za to potrebujete getaddrinfo().
Vrnjena vrednost
inet_ntop() vrne parameter dst za uspeh ali NULL ob neuspehu (in errno je nastavljen).
inet_pton() vrne 1 na uspeh. Vrne -1, če je prišlo do napake (errno je nastavljeno) ali 0, če vhod ni veljaven IP-naslov.
Primer
// IPv4 demo of inet_ntop() and inet_pton() struct sockaddr_in sa; char str[INET_ADDRSTRLEN]; // ta naslov IP shranite v sa: inet_pton(AF_INET, "192.0.2.33", &(sa.sin_addr)); // sedaj ga vrni in natisni inet_ntop(AF_INET, &(sa.sin_addr), str, INET_ADDRSTRLEN); printf("%s\n", str); // prints "192.0.2.33"
// predstavitev IPv6 inet_ntop () in inet_pton () // (v bistvu enako, razen s kupom 6-ih vrženih okoli) struct sockaddr_in6 sa; char str[INET6_ADDRSTRLEN]; // ta naslov IP shranite v sa: inet_pton(AF_INET6, "2001:db8:8714:3a90::12", &(sa.sin6_addr)); // sedaj ga vrni in natisni inet_ntop(AF_INET6, &(sa.sin6_addr), str, INET6_ADDRSTRLEN); printf("%s\n", str); // prints "2001:db8:8714:3a90::12"
// funkcijo pomočnika, ki jo lahko uporabite: //Pretvorite naslov struct sockaddr v niz, IPv4 in IPv6: char *get_ip_str(const struct sockaddr *sa, char *s, size_t maxlen) { switch(sa->sa_family) { case AF_INET: inet_ntop(AF_INET, &(((struct sockaddr_in *)sa)->sin_addr), s, maxlen); break; case AF_INET6: inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)sa)->sin6_addr), s, maxlen); break; default: strncpy(s, "Unknown AF", maxlen); return NULL; } return s; }
Poglej tudi
getaddrinfo()
9.15. listen()
Povejte vtičnico, da poslušate dohodne povezave
Prototipi
#include <sys/socket.h> int listen(int s, int backlog);
Opis
Lahko uporabite deskriptor vtičnice (izdelan s sistemskim klicem socket()) in povejte, da poslušate dohodne povezave. To je tisto, kar razlikuje strežnike od strank, fantje.
backlog lahko pomeni nekaj različnih stvari, odvisno od sistema, na katerem ste, vendar je rahlo toliko, kolikšne čakalne povezave lahko imate, preden jedro začne zavračati nove. Torej, ko pridejo nove povezave, jih morate hitro accept(), tako da se zaostanek ne napolni. Poskusite ga nastaviti na 10 ali tako, in če vaše stranke začnejo prejemati »Povezava zavrnjena« pod obremenitvijo, jo nastavite višje.
Pred klicem listen() mora vaš strežnik klicati bind(), da se priključi na določeno številko vrat. Ta številka vrat (na naslovu strežnika) bo tista, s katero se bodo stranke povezale.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
struct addrinfo hints, *res; int sockfd; // najprej z naslovom getaddrinfo () naložite naslovne strukture (): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // fill in my IP for me getaddrinfo(NULL, "3490", &hints, &res); // naredite vtičnico: sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); // ga povežite s pristaniščem, v katerega smo vstopili, da dobimo getaddrinfo(): bind(sockfd, res->ai_addr, res->ai_addrlen); listen(sockfd, 10); // nastavite s strežnik (poslušanje) vtičnice // Potem na nekem mestu sprejmete accept()
Poglej tudi
accept(), bind(), socket()
9.16. perror(), strerror()
Natisnite napako kot človeško berljiv niz
Prototipi
#include <stdio.h> #include <string.h> // for strerror() void perror(const char *s); char *strerror(int errnum);
Opis
Ker toliko funkcij vrne -1 pri napaki in nastavite vrednost spremenljivke errno kot nekaj števila, bi bilo lepo, če bi ga lahko enostavno natisnili v obliki, ki je bila smiselna za vas.
Usmiljeno, perror() naredi to. Če želite, da se pred napako natisne večji opis, ga lahko usmerite s parametrom s (ali lahko pustite s kot NULL in nič dodatnega ne bo natisnjeno.)
Na kratko, ta funkcija zajema errno vrednosti, na primer ECONNRESET, in jih lepo natisne, na primer “Ponastavitev povezave s peerjem”.
Funkcija strerror() je zelo podobna perror(), razen če vrne kazalec na niz sporočil o napaki za dano vrednost (navadno vnesete spremenljivko errno.)
Vrnjena vrednost
strerror() vrne kazalec na niz sporočil o napaki.
Primer
int s; s = socket(PF_INET, SOCK_STREAM, 0); if (s == -1) { // some error has occurred // natisne "napaka vtičnice:" + sporočilo o napaki: perror("socket error"); } // podobno: if (listen(s, 10) == -1) { // to natisne "napaka:" + sporočilo o napaki iz errno: printf("an error: %s\n", strerror(errno)); }
Poglej tudi
errno
9.17. poll()
Preizkusite dogodke v več vtičnicah hkrati
Prototipi
#include <sys/poll.h> int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
Opis
Ta funkcija je zelo podobna select(), saj oba gledata nizi deskriptorjev datotek za dogodke, kot so vhodni podatki pripravljeni za recv(), podatkovni vmesnik, pripravljen za send(), izven podatkov o stanju, pripravljen za recv( ), napake itd.
Osnovna ideja je, da prenese niz nfds struct pollfds v ufds, skupaj s časovnim zamikom v milisekundah (1000 milisekund v sekundi). Časovna omejitev je lahko negativna, če želite čakati večno. Če se dogodek ne zgodi na nobenem od deskriptorjev sokov, se bo poll() vrnila.
Vsak element v matriki struct pollfds predstavlja en deskriptor soketov in vsebuje naslednja polja:
struct pollfd { int fd; // deskriptor vtičnice short events; // bitna slika dogodkov, ki nas zanimajo short revents; // ko se glasovalo () vrne, bitna slika dogodkov, ki so se zgodila };
Preden pokličete poll(), naložite fd s deskriptorjem soketa (če nastavite fd na negativno število, se ta struktura pollfd ne upošteva, njegovo polje reventov pa je nastavljeno na nič), nato pa poljubno zgradite polje dogodkov z bitno -Izberite naslednje makre:
POLLIN | Opozorite me, ko so podatki pripravljeni recv() na tej vtičnici. |
POLLOUT | Opozorite me, ko lahko send() podatke v to vtičnica brez blokiranja. |
POLLPRI | Opozorite me, ko so podatki izven pasu pripravljeni recv() na tej vtičnici. |
Ko se bo klic povratnega poll() vrnil, bo polje revnih zgrajeno kot bitno-ALI zgornjih polj, ki vam pove, kateri deskriptorji so dejansko imeli ta dogodek. Poleg tega so lahko tudi ta druga polja:
POLLERR | V tej vtičnici je prišlo do napake. |
POLLHUP | Oddaljena stran povezave je prekinjena. |
POLLNVAL | Nekaj je bilo napačno s deskriptorjem sokov fd—mogoče je to neinicializirano? |
Vrnjena vrednost
Vrne število elementov v matrico ufds, ki so imele dogodek na njem; to je lahko nič, če je prišlo do časovne omejitve. Vrne tudi -1 po napaki (in ustrezno bo nastavljeno errno.)
Primer
int s1, s2; int rv; char buf1[256], buf2[256]; struct pollfd ufds[2]; s1 = socket(PF_INET, SOCK_STREAM, 0); s2 = socket(PF_INET, SOCK_STREAM, 0); // pretend we've connected both to a server at this point //connect(s1, ...)... //connect(s2, ...)... // set up the array of file descriptors. // // in this example, we want to know when there's normal or out-of-band // data ready to be recv()'d... ufds[0].fd = s1; ufds[0].events = POLLIN | POLLPRI; // check for normal or out-of-band ufds[1].fd = s2; ufds[1].events = POLLIN; // check for just normal data // Čakati na dogodke v vtičnicah, 3,5 sekunde rv = poll(ufds, 2, 3500); if (rv == -1) { perror("poll"); // prišlo je do napake v poll() } else if (rv == 0) { printf("Timeout occurred! No data after 3.5 seconds.\n"); } else { // preveri dogodke na s1: if (ufds[0].revents & POLLIN) { recv(s1, buf1, sizeof buf1, 0); // receive normal data } if (ufds[0].revents & POLLPRI) { recv(s1, buf1, sizeof buf1, MSG_OOB); // out-of-band data } // preveri dogodke na s2: if (ufds[1].revents & POLLIN) { recv(s1, buf2, sizeof buf2, 0); } }
Poglej tudi
select()
9.18. recv(), recvfrom()
Prejemajte podatke v vtičnici
Prototipi
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int s, void *buf, size_t len, int flags); ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
Opis
Ko imate vtičnico in priključeno, lahko preberete dohodne podatke z oddaljene strani z uporabo recv() (za vtičnice TCP SOCK_STREAM) in recvfrom() (za UDP SOCK_DGRAM vtičnice).
Obe funkciji sta opisni socket s, kazalec na pufer buf, velikost (v bajtih) vmesnika in niz zastavic, ki nadzirajo delovanje funkcij.
Poleg tega recvfrom() vzpostavi struct sockaddr*, od tega vam bo povedal, od kod prihajajo podatki, in bo izpolnil fromlen z velikostjo struct sockaddr. (Poleg tega morate inicializirati fromlen velikosti od ali struct sockaddr.)
Kakšne čudne zastave lahko preidete v to funkcijo? Tukaj je nekaj od njih, vendar bi morali preveriti svoje lokalne človeške strani za več informacij in kaj je dejansko podprto v vašem sistemu. Pojasnili ste jih – ali skupaj, ali pa nastavite flags na 0, če želite, da je redna vanilla recv().
Ko pokličete recv(), bo blokiral, dokler ne bo nekaj podatkov za branje. Če ne želite blokirati, nastavite vtičnico za neblokiranje ali preverite z select() ali poll(), da preverite, ali so vhodni podatki pred klicem recv() ali recvfrom().
Vrnjena vrednost
Vrne število dejansko prejetih bajtov (kar je lahko manj kot ste zahtevali v parametru len) ali -1 pri napaki (in ustrezno bo nastavljen errno).
Če je oddaljena stran zaprta povezavo, se recv() vrne 0. To je običajna metoda za ugotavljanje, ali je oddaljena stran zaprla povezavo. Normalna je dobra, uporniška!
Primer
// stream sockets and recv() struct addrinfo hints, *res; int sockfd; char buf[512]; int byte_count; // dobite informacije o gostitelju, naredite vtičnico in jo povežite memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_STREAM; getaddrinfo("www.example.com", "3490", &hints, &res); sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); connect(sockfd, res->ai_addr, res->ai_addrlen); // v redu! Zdaj, ko smo povezani, lahko prejmemo nekaj podatkov! byte_count = recv(sockfd, buf, sizeof buf, 0); printf("recv()'d %d bytes of data in buf\n", byte_count);
// datagram sockets and recvfrom() struct addrinfo hints, *res; int sockfd; int byte_count; socklen_t fromlen; struct sockaddr_storage addr; char buf[512]; char ipstr[INET6_ADDRSTRLEN]; // dobite informacije o gostitelju, naredite vtičnico, ga priklopite na vrata 4950 memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_PASSIVE; getaddrinfo(NULL, "4950", &hints, &res); sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); bind(sockfd, res->ai_addr, res->ai_addrlen); // ni treba sprejeti (), samo recvfrom (): fromlen = sizeof addr; byte_count = recvfrom(sockfd, buf, sizeof buf, 0, &addr, &fromlen); printf("recv()'d %d bytes of data in buf\n", byte_count); printf("from IP address %s\n", inet_ntop(addr.ss_family, addr.ss_family == AF_INET? ((struct sockadd_in *)&addr)->sin_addr: ((struct sockadd_in6 *)&addr)->sin6_addr, ipstr, sizeof ipstr);
Poglej tudi
send(), sendto(), select(), poll(), Blocking
9.19. select()
Preverite, ali so deskriptorji vtičnic pripravljeni za branje / pisanje
Prototipi
#include <sys/select.h> int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); FD_SET(int fd, fd_set *set); FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_ZERO(fd_set *set);
Opis
Funkcija select() vam omogoča, da istočasno preverite več vtičnic in ugotovite, ali imajo podatke, ki čakajo, da se jih prikaže recv() d, ali če jim lahko send() podatke brez blokiranja ali če je prišlo do neke izjeme.
Napolnite svoje skupine deskriptorjev s pomočjo makrov, na primer FD_SET() zgoraj. Ko imate nastavljeno, ga prenesete v funkcijo kot eden od naslednjih parametrov: readfds , če želite vedeti, kdaj je katera od vtičnic v nizu pripravljena na podatke recv(), writefds, če je writefds koli vtičnica pripravljena na send() podatke in/ali exceptfds, če morate vedeti, kdaj se pojavijo izjeme (napake) na kateri koli vtičnici. Vsi ali vsi ti parametri so lahko NULL, če te vrste dogodkov ne zanima. Ko select() vrne, se bodo vrednosti v nizih spremenile, da bodo prikazane, ki so pripravljene za branje ali pisanje, in ki imajo izjeme.
Prvi parameter, n je najštevilčen deskriptor sokov (samo ints, zapomni si?) In eno.
Nazadnje, struct timeval, timeout, na koncu – to vam omogoča, da poveste, izberite (), kako dolgo naj preverijo te nastavitve. Vrnila se bo po preteku časa ali ko pride do dogodka, kar nastopi prej. Strukturni timeval ima dva polja: tv_sec je število sekund, ki mu je dodano tv_usec, število mikrosekund (1.000.000 mikrosekund na sekundo).
Pomožni makri naredijo naslednje:
Opomba za uporabnike Linuxa: izbira Linuxa() v Linuxu lahko vrne “pripravljeno za branje” in ni dejansko pripravljena za branje, kar povzroči blokiranje poznejšega poziva za read(). O tej bugi lahko delate tako, da nastavite zastavico O_NONBLOCK v sprejemni vtičnici, tako da napaka pri EWOULDBLOCK, nato pa se ta napaka ne upošteva. Oglejte si referenčno stran fcntl() za več informacij o nastavitvi vtičnice na ne-blokiranje.
Vrnjena vrednost
Vrne število deskriptorjev v nizu za uspeh, 0, če je čas dosegel, ali -1 pri napaki (in ustrezno bo nastavljen errno.) Poleg tega so nastavki spremenjeni, da prikažejo, katere vtičnice so pripravljene.
Primer
int s1, s2, n; fd_set readfds; struct timeval tv; char buf1[256], buf2[256]; // se pretvarjamo, da smo oba strežnika povezali na tej točki //s1 = socket(...); //s2 = socket(...); //connect(s1, ...)... //connect(s2, ...)... // Čisto nastavite pred časom FD_ZERO(&readfds); // dodajte naše deskriptorje v niz FD_SET(s1, &readfds); FD_SET(s2, &readfds); // ker smo dobili s2 sekundo, je "večja", zato jo uporabimo za // the n param in select() n = s2 + 1; // počakajte, da je katerakoli vtičnica pripravljena za recv() d (časovna omejitev 10,5 sekund) tv.tv_sec = 10; tv.tv_usec = 500000; rv = select(n, &readfds, NULL, NULL, &tv); if (rv == -1) { perror("select"); // error occurred in select() } else if (rv == 0) { printf("Timeout occurred! No data after 10.5 seconds.\n"); } else { // eden ali oba deskriptorja imajo podatke if (FD_ISSET(s1, &readfds)) { recv(s1, buf1, sizeof buf1, 0); } if (FD_ISSET(s2, &readfds)) { recv(s2, buf2, sizeof buf2, 0); } }
Poglej tudi
poll()
9.20. setsockopt(), getsockopt()
Nastavite različne možnosti za vtičnico
Prototipi
#include <sys/types.h> #include <sys/socket.h> int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
Opis
Sockets so precej nastavljive zveri. Pravzaprav so tako konfigurirni, sploh ne bom pokrivala vse tukaj. Kakorkoli že, odvisno od sistema. Ampak govoril bom o osnovah.
Očitno te funkcije dobijo in določijo določene možnosti v vtičnici. V okencu Linux so vse informacije o vtičnicah na strani s strani za vtičnico v razdelku 7. (Vpišite: “man 7 socket“, da dobite vse te dobrote.)
Kar zadeva parametre, s je vtičnica, o kateri govorite, raven je treba nastaviti na SOL_SOCKET. Nato nastavite optname na ime, ki vas zanima. Ponovno si oglejte svojo stran s podatki o vseh možnostih, vendar je tukaj nekaj najbolj zabavnih:
Kar zadeva parameter optval, je običajno kazalec na int, ki označuje zadevno vrednost. Za booleans je nič enačen, neveljavni pa je resničen. In to je absolutno dejstvo, razen če je drugačno v vašem sistemu. Če ni nobenega parametra, je lahko optval NULL.
Končni parameter, optlen, mora biti nastavljen na dolžino optval, verjetno sizeof (int), vendar se razlikuje glede na možnost. Upoštevajte, da je v primeru getsockopt () to kazalec na socklen_t in določa največji objekt velikosti, ki bo shranjen v optval (za preprečitev prelivanja prekrival). In getsockopt () bo spremenil vrednost optlen, da bo odražal število dejansko nastavljenih bajtov.
Opozorilo: v nekaterih sistemih (predvsem v Sun in Windows) je možnost lahko char, namesto int, in je nastavljena na primer na vrednost znaka ‘1’ namesto na int vrednost 1. Ponovno preverite lastne človeške strani za več informacij z “man setsockopt” in “man 7 socket“!
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
int optval; int optlen; char *optval2; // Nastavite SO_REUSEADDR v vtičnici na true (1): optval = 1; setsockopt(s1, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval); // povezavo vtičnice z imenom naprave (morda ne bo delovala na vseh sistemih): optval2 = "eth1"; // 4 bytes long, so 4, below: setsockopt(s2, SOL_SOCKET, SO_BINDTODEVICE, optval2, 4); // Glejte, če je postavljena zastavica SO_BROADCAST: getsockopt(s3, SOL_SOCKET, SO_BROADCAST, &optval, &optlen); if (optval != 0) { print("SO_BROADCAST enabled on s3!\n"); }
Poglej tudi
fcntl ()
9.21. send(), sendto()
Pošljite podatke preko vtičnice
Prototipi
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int s, const void *buf, size_t len, int flags); ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
Opis
Te funkcije pošljejo podatke v vtičnico. Na splošno velja, da se send() uporablja za priključene vtičnice TCP SOCK_STREAM, in sendto() se uporablja za nepovezane vtičnice datagrama UDP SOCK_DGRAM. Pri nepovezanih vtičnicah morate vsakič, ko ga pošljete, določiti cilj paketa, zato zadnji parametri sendto() določajo, kam gre paket.
Z obema send() in sendto() je parameter s vtičnica, buf je kazalec na podatke, ki jih želite poslati, len je število bajtov, ki jih želite poslati, in zastave vam omogočajo, da določite več informacij o tem, kako podatke je treba poslati. Označite zastave na nič, če želite, da so “normalni” podatki. Tukaj je nekaj najpogosteje uporabljenih zastav, vendar za več podrobnosti preverite lokalne strani za send().
MSG_OOB | Pošlji kot podatki izven pasu. TCP to podpira, in to je način, da sporočite prejemnemu sistemu, da so ti podatki višji prednost pred običajnimi podatki. Sprejemnik bo prejel signal SIGURG In nato lahko prejme te podatke brez predhodnega prejema vse ostale običajne podatke v čakalni vrsti. |
MSG_DONTROUTE | Ne pošiljajte teh podatkov prek usmerjevalnika, samo ga hranite lokalno. |
MSG_DONTWAIT | Če send() blokirati, ker je izhoden promet je zamašen, ga vrnite EAGAIN. To je kot a “omogoči ne blokira samo za to pošiljanje.” Oglejte si oddelek o blocking za več podrobnosti. |
MSG_NOSIGNAL | Če ti send() do oddaljenega gostitelja, ki je nič več recv()ing, običajno boste dobili signal SIGPIPE. Dodajanje te zastavice to preprečuje signal, da se dvigne. |
Vrnjena vrednost
Vrne število dejansko poslanih bajtov ali -1 pri napaki (in ustrezno bo nastavljeno errno.) Upoštevajte, da je število dejansko poslanih bajtov lahko manjše od števila, ki ste jo zahtevali za pošiljanje! Oglejte si poglavje o ravnanju z delnim send() za funkcijo pomočnika, da se obrnete k temu.
Tudi če je vtičnica zaprla obe strani, bo proces, ki kliče send(), dobil signal SIGPIPE. (Razen če je bila send() pošljite z oznako MSG_NOSIGNAL.)
Primer
int spatula_count = 3490; char *secret_message = "The Cheese is in The Toaster"; int stream_socket, dgram_socket; struct sockaddr_in dest; int temp; // najprej s vtičnimi vtičnicami TCP: // predpostavimo, da so vtičnice izdelane in priključene //stream_socket = socket(... //connect(stream_socket, ... // pretvori v omrežni vrstni red temp = htonl(spatula_count); // send data normally: send(stream_socket, &temp, sizeof temp, 0); // pošljite tajno sporočilo iz pasu: send(stream_socket, secret_message, strlen(secret_message)+1, MSG_OOB); // zdaj s podnožji datagrama UDP: //getaddrinfo(... //dest = ... // assume "dest" holds the address of the destination //dgram_socket = socket(... // običajno pošljete tajno sporočilo: sendto(dgram_socket, secret_message, strlen(secret_message)+1, 0, (struct sockaddr*)&dest, sizeof dest);
Poglej tudi
recv(), recvfrom()
9.22. shutdown()
Ustavi še naprej pošiljanje in sprejemanje v vtičnico
Prototipi
#include <sys/socket.h> int shutdown(int s, int how);
Opis
To je to! Imam ga! V tej vtičnici ni dovoljeno pošiljati send(), vendar pa še vedno želim podatke o njej objaviti! Ali obratno! Kako lahko to storim?
Ko close() deskriptor vtičnice, za bralno in pisno zaprta obe strani vtičnice in osvobodi deskriptor vtičnice. Če želite samo eno ali drugo zapreti, lahko uporabite ta poziv za shutdown().
Kar zadeva parametre s, je s očitno socket, na katero želite izvedeti to dejanje, in kakšno dejanje je mogoče določiti s parametrom, kako. Kako je lahko SHUT_RD, da preprečite nadaljnje recv() s, SHUT_WR, da prepove nadaljnje send() s ali SHUT_RDWR, če želite to storiti.
Upoštevajte, da shutdown() ne sprosti deskriptorja vtičnice, zato morate še vedno zapreti () vtičnico, tudi če je bila popolnoma zaprta.
To je redko uporabljen sistemski klic.
Vrnjena vrednost
Vrne nič na uspeh, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
int s = socket(PF_INET, SOCK_STREAM, 0); // ...naredi nekaj pošiljk () in stvari tukaj... // in zdaj, ko smo končali, ne dovolite več pošiljati () s: shutdown(s, SHUT_WR);
Poglej tudi
close()
9.23. socket()
Dodelite deskriptor vtičnice
Prototipi
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
Opis
Vrne nov deskriptor sokov, s katerim lahko uporabite sockety stvari. To je na splošno prvi klic v neverjetnem procesu pisanja vtičnice in lahko uporabite rezultat za poznejše klice listen(), bind(), accept() ali različne druge funkcije.
Pri običajni uporabi dobite vrednosti teh parametrov iz klica na getaddrinfo(), kot je prikazano v spodnjem primeru. Vendar jih lahko izpolnite z roko, če res želite.
Vrnjena vrednost
Novi deskriptor vtičnice, ki bo uporabljen pri naslednjih klicih, ali -1 pri napaki (in errno bo ustrezno nastavljen.)
Primer
struct addrinfo hints, *res; int sockfd; // najprej z naslovom getaddrinfo () naložite naslovne strukture (): memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // AF_INET, AF_INET6, or AF_UNSPEC hints.ai_socktype = SOCK_STREAM; // SOCK_STREAM or SOCK_DGRAM getaddrinfo("www.example.com", "3490", &hints, &res); // naredite vtičnico z informacijami, pridobljenimi iz getaddrinfo (): sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
Poglej tudi
accept(), bind(), getaddrinfo(), listen()
9.24. struct sockaddr in prijatelji
Strukture za ravnanje z internetnimi naslovi
Prototipi
#include <netinet/in.h> // Vsi kazalci za naslovne strukture so pogosto prikazani na kazalce // na to vrsto pred uporabo v različnih funkcijah in sistemskih klicih: struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address }; // IPv4 AF_INET sockets: struct sockaddr_in { short sin_family; // e.g. AF_INET, AF_INET6 unsigned short sin_port; // e.g. htons(3490) struct in_addr sin_addr; // glej strukturo in_addr, spodaj char sin_zero[8]; // Nič, če želite }; struct in_addr { unsigned long s_addr; // nalaganje z inet_pton () }; // IPv6 AF_INET6 sockets: struct sockaddr_in6 { u_int16_t sin6_family; // naslov družine, AF_INET6 u_int16_t sin6_port; // Številka vrat, vrstni red omrežnega bajta u_int32_t sin6_flowinfo; // IPv6 flow information struct in6_addr sin6_addr; // IPv6 address u_int32_t sin6_scope_id; // Scope ID }; struct in6_addr { unsigned char s6_addr[16]; // load with inet_pton() }; // Splošna struktura vtičnice, ki je dovolj velika, da jo drži // struct sockaddr_in ali struct sockaddr_in6 podatki: struct sockaddr_storage { sa_family_t ss_family; // address family // vse to je oblazinjenje, specifično izvajanje, ga prezri: char __ss_pad1[_SS_PAD1SIZE]; int64_t __ss_align; char __ss_pad2[_SS_PAD2SIZE]; };
Opis
To so osnovne strukture za vse syscalls in funkcije, ki se ukvarjajo z internetnimi naslovi. Pogosto boste uporabili getaddrinfo (), da napolnite te strukture in jih nato preberete, ko boste morali.
Struktura sockaddr_in in struct sockaddr_in6 v pomnilniku imata enako začetno strukturo kot struct sockaddr, zato lahko brez kaznivega dejanja svobodno postavite kazalec ene vrste v drugo, razen možnega konca vesolja.
Samo šališ se na to stvar o koncu vesolja… če se vesolje konča, ko strukturo sockaddr_in * postavite v strukturo sockaddr *, obljubim ti, da je to čisto naključje in ne bi smeli niti skrbeti.
Torej, upoštevajte to v mislih, ne pozabite, da vsakič, ko funkcija pravi, da potrebuje strukturo sockaddr *, lahko s to enostavno in brezplačno prenesete strukturo sockaddr_in *, struct sockaddr_in6 * ali struct sockadd_storage * v to vrsto.
Struct sockaddr_in je struktura, uporabljena pri naslovih IPv4 (npr. “192.0.2.10”). Ima družino naslovov (AF_INET), pristanišče v sin_port in naslov IPv4 v sin_addr.
Obstaja tudi to polje sin_zero v strukturi sockaddr_in, za katerega nekateri trdijo, da mora biti nastavljen na nič. Drugi ljudje ne zahtevajo ničesar o tem (dokumentacija o Linuxu sploh sploh ne omenja), in nastavitev na nič ni videti dejansko potrebna. Torej, če se vam zdi všeč, ga nastavite na nič z uporabo memset ().
Zdaj je ta struktura in_addr čudna zver v različnih sistemih. Včasih je nora zveza z vsemi vrstami #defines in drugih neumnosti. Toda, kar morate storiti, je v tej strukturi uporabljeno le polje s_addr, ker ga mnogi sistemi izvajajo samo.
Struct sockadd_in6 in struct in6_addr so zelo podobni, razen če se uporabljajo za IPv6.
Struct sockaddr_storage je struktura, ki jo lahko prenesete, da sprejmete () ali recvfrom (), ko poskušate napisati kodo različice agnostike in ne veste, ali bo novi naslov IPv4 ali IPv6. Struktura struct sockaddr_storage je dovolj velika, da drži oba tipa, za razliko od prvotne male strukture sockaddr.
Primer
// IPv4: struct sockaddr_in ip4addr; int s; ip4addr.sin_family = AF_INET; ip4addr.sin_port = htons(3490); inet_pton(AF_INET, "10.0.0.1", &ip4addr.sin_addr); s = socket(PF_INET, SOCK_STREAM, 0); bind(s, (struct sockaddr*)&ip4addr, sizeof ip4addr);
// IPv6: struct sockaddr_in6 ip6addr; int s; ip6addr.sin6_family = AF_INET6; ip6addr.sin6_port = htons(4950); inet_pton(AF_INET6, "2001:db8:8714:3a90::12", &ip6addr.sin6_addr); s = socket(PF_INET6, SOCK_STREAM, 0); bind(s, (struct sockaddr*)&ip6addr, sizeof ip6addr);
Poglej tudi
accept(), bind(), connect(), inet_aton(), inet_ntoa()
Original: http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html