Ako programovat sietove veci v C (pod unixamy)

03.12.2002 19:09

Prednedavnom vznikla debata o tom, ako sa v c programuje sietovo. Prinasame jeden clanok, ktory tuto problematiku objasnuje. Vsetko co sa tyka sietovej komunikacii v Unixe sa toci okolo socketu

Sockety su postavene na zakladnom Unixovom modele: \"Vsetko je subor\" Ale kvoli dost zlozitemu sposobu komunikacie v internete, nemozeme pouzit jednoducho systemove volanie open, resp. open() funkciu v Ccku. Ale namiesto toho musime spravit niekolko krokov, aby sme `otvorili socket`.Hned ako to spravime, mozme zo socketom robit to iste co s hociakym file deskriptorom: zapisovat do nho, citat z neho, atd...

Socketove funkcie
Na otvorenie socketu nam stacia 4 (server), niekedy len 2 (client) funkcie.

socket
Tuto funkciu pouzivaju ako serveri, tak aj clienti. Deklaracia:

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

Domain argument povie systemu, aku 'protocol family' chceme pouzit. Existuje ich velmi vela, niektore su specificke pre niektorych vyrobcov, niektore su velmi zname. My pouzijeme PF_INET pre TCP,UDP a ine ipv4 protokoly. (vsetky ich najdete v sys/socket.h) Existuje 5 hodnot pre argument type (takisto ich najdete v sys/socket.h). Vsetky zacinaju s 'SOCK_'. Jeden z najznamejsich je SOCK_STREAM, ktorym povieme systemu, ze chceme spojovanu sluzbu (co je TCP, ked pouzijeme PF_INET) Pokial poziadame o SOCK_DGRAM, budeme pozadovat nespojovanu sluzbu (UDP). Nakoniec, argument protocol. Zavisi od 2 predchadzajucich argumentov a nie je vzdy ziaduce ho pouzit. My pouzijeme 0.

nespojeny socket
Teraz mame nespojeny socket. Mohli by sme to prirovnat k telefonnej linke: Zapojili sme modem do linky, ale nespravili sme este ziadne volanie a ani sme ho nenakonfigurovali, aby odpovedal na nejake prichodize volania.

Struktura sockaddr
Predtym nez sa dostaneme na ostatne socketove funkcie, napisem nieco o strukture

sockaddr.

Rozne funkcie socketu sa odkazuju na strukturu sockaddr (sys/socket.h)
Tato struktura je deklarovana takto:

/*
      * Structure used by kernel to store most
      * addresses.
      */
    struct sockaddr {
        u_char      sa_len;    /* total length */
        sa_family_t sa_family;  /* address family */
        char        sa_data[14];    /* actually longer; address value */
    };
    #define SOCK_MAXADDRLEN 255    /* longest possible addresses */

poznamka: sa_data moze byt vacsia ako 14 bajtov.

Sockety su velmy silny interface.Vela ludi si mysli, ze sockety sa pouzivaju iba na IP a vela aplikacii ich iba tak pouziva. Sockety sa ale daju pouzit na vela roznych typov komunikacii medzi procesmy, s ktorych IP je iba 1. V sys/socket.h najdete vela roznych typov socketovych protokolov, ktore sa nazyvaju 'address families':

/*
* Address families.
*/
#define AF_UNSPEC  0      /* unspecified */
#define AF_LOCAL    1      /* local to host (pipes, portals) */
#define AF_UNIX    AF_LOCAL    /* backward compatibility */
#define AF_INET    2      /* internetwork: UDP, TCP, etc. */
#define AF_IMPLINK  3      /* arpanet imp addresses */
#define AF_PUP      4      /* pup protocols: e.g. BSP */
#define AF_CHAOS    5      /* mit CHAOS protocols */

My pouzijeme AF_INET pre IP. sa_family v strukture sockaddr udava, ako sa pouziju data v sa_data. Ked ale pouzijeme 'address family' AF_INET, bude lepsie pouzit namiesto struktury sockaddr, strukturu sockaddr_in, ktora je definovana v netinet/in.h

/*
* Socket address, internet style.
*/
struct sockaddr_in {
u_char  sin_len;
u_char  sin_family;
u_short sin_port;
struct  in_addr sin_addr;
char    sin_zero[8];
};

Vyzera to nejako takto:

0        1        2      3
  +--------+--------+-----------------+
0 |    0  | Family |      Port      |
  +--------+--------+-----------------+
4 |            IP Address            |
  +-----------------------------------+
8 |                0                |
  +-----------------------------------+
12 |                0                |
  +-----------------------------------+

Tri najdolezitejsie polia su sin_family, ktora ma 1 bajt, sin_port, ktora ma 16 bitov = 2 bajty a sin_addr, co je 32 bitovy integer reprezentujuci IP adresu.
Mimochodom sin_addr je definovany takto:

/*
      * Internet address (a structure for historical reasons)
      */
    struct in_addr {
        in_addr_t s_addr;
    };

in_addr_t je 32 bitovy integer

Problem s network byte order a host byte order:
povedzme ze napiseme nieco taketo:

sa.sin_family      = AF_INET;
        sa.sin_port        = 13;
        sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;

na intelackych systemoch (little endian) bude sockaddr_in vyzerat nejako takto:

0      1        2      3
  +--------+--------+--------+--------+
0 |    0  |  2    |  13  |  0    |
  +--------+--------+--------+--------+
4 |  18  |  244  |  43  |  192  |
  +-----------------------------------+
8 |                0                |
  +-----------------------------------+
12 |                0                |
  +-----------------------------------+

na inom systeme (big endian) to bude vyzerat takto:
        0      1        2        3
  +--------+--------+--------+--------+
0 |    0  |  2    |    0  |  13  |
  +--------+--------+--------+--------+
4 |  192  |  43  |  244  |  18  |
  +-----------------------------------+
8 |                0                |
  +-----------------------------------+
12 |                0                |
  +-----------------------------------+

a na systeme PDP to zase bude vyzerat uplne inak.

A v com je problem ? Problem je v tom, ze to neni portabilne. Spravne je samozrejme to 2 (network byte order = MSB - most significant byte masiny to maju defaultne). Aby nam to ficalo ale aj na intelackych masinach, musime to upravit takto:
sa.sin_family = AF_INET;
sa.sin_port = 13 << 8;
sa.sin_addr.s_addr = (((((18 << 8) | 244) << 8) | 43) << 8) | 192;
Toto obrati vsetky hodnoty ako v tej 2. tabulke na network byte order.Ale zase tu je problem: keby ste to chceli spustit na MSB (most significant byte) masine, ktora ma defaultne network byte order, opacne to hodi do host byte order a nebude to ficat.

Riesenie:
Funkcie htons,htonl a ntohs,ntohl v Ccku. Na MSB-first masinach tieto funkcie nic nespravie na LSB-firts (least significant byte first) masinach obrati hodnoty na network byte order. htons obrati 2 bajtovu hodnotu a htonl obrati 4 bajtovu hodnotu.
poznamka: ked si pozriete zdrojaky tak htons=ntohs a htonl=ntohl

Takze na koniec to bude vyzerat takto:
sa.sin_family = AF_INET;
sa.sin_port = htons(13);
sa.sin_addr.s_addr = inet_addr(\"192.43.244.18\");

ta inet_addr je funkcia, ktora zmeni retazec znakov reprezentujucy IP adresu do 4 bajtov v network byte order

Funkcie klienta

connect
Ked klient vytvoril socket, potrebuje sa spojit na specificky port na vzdialenej masine.Pouzije connect.

int connect(int s, const struct sockaddr *name, socklen_t namelen);

Argument s je socket, resp. hodnota vratena funkciou socket.
Argument name je pointer na strukturu sockaddr (sockaddr_in).
A namelen je velkost tej struktury.

Ked bol connect uspesny, vrati 0, pokial nie vrati -1 a vyplni errno chybovim kodom.Treba vzdy testovat, co vrati, lebo server nemusi odpovedat, resp. IP nemusi existovat, atd...

Funkcie serveru
Server sa typicky nikde nepripaja, naopak caka, ked sa nejaky klient na neho pripoji a pozaduje nejake sluzby.

Na to mame 3 zakladne funkcie.

bind
Pomocou bindu definujeme na ktorom porte bude cakat server. V TCP a UDP existuje 65535 portov.

Deklaracia:

int bind(int s, const struct sockaddr *addr, socklen_t addrlen);

Okrem nastavenia portu, moze nastavit aj IP adresu, na ktorej bude cakat na spojenia. Povatsinou to nepotrebujeme a chceme aby cakal na vsetkych IP adresach (normalne mame 1 sietovku a loopback interface, takze aby cakal aj na 127.0.0.1 aj na IP adrese sietovky). Takze ked chceme aby cakal na vsetkych interfejsoch, ktore maju IP adresu,resp INADDR_ANY, ktorej definicia vyzera takto:

#define INADDR_ANY (u_int32_t)0x00000000

listen
Definicia:

int listen(int s, int backlog);

Pomocou listen nastavime, kolko prichadzajuchich poziadaviek prijmeme, pokial sme zatial zamestnany poslednou prichodzou poziadavkou. Respektive inymi slovamy backlog nastavi maximalnu velkost queue(jak to prelozit ? buffra) cakajucich spojeni.

accept
Ked prirovname k telefonnej linke, tak accept zdvihne telefon, ked zazvoni.Teraz mame vytvorene spojenie z klientom. Toto spojenie pretrva pokial client alebo my (server) neukonci spojenie.

Definicia:

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

addrlen je pointer na strukturu sockaddr (sockaddr_in), ktoru accept pretentokrat vyplni IP adresou klienta a portom, odkial sa pripojil navratova hodnota je integer - novy socket, ktory pouzijeme na komunikaciu. A co sa stalo zo starym socketom ? Zostal cakat na dalsie spojenia (pametate na backlog v listen ?) pokial ho nezavrieme. Ten novy socket vrateni accept je urceni len na komunikaciu, nemozeme pouzit znovu listen ani znovu accept.

Pomocne funkcie
gethostbyname
Kedze neexistuje ziadny sposob v socketoch ako pouzit domenu nejakeho hostu.Musime pouzit pomocnu funkciu gethostbyname (gethostbyname2). Obidve funkcie vratia pointer na strukturu hostent.

Definicia z netdb.h:
struct hostent * gethostbyname(const char *name);
struct hostent * gethostbyname2(const char *name, int af);

struct  hostent {
        char    *h_name;        /* official name of host */
        char    **h_aliases;    /* alias list */
        int    h_addrtype;    /* host address type */
        int    h_length;      /* length of address */
        char    **h_addr_list;  /* list of addresses from name server */
#define h_addr  h_addr_list[0]  /* address, for backward compatibility */
};

po zavolani gethostbyname bude h_addr_list[0] obsahovat IP adresu uz v network byte order.

Ine funkcie

poll a select
Pouzivaju sa na tzv. \"synchronous I/O multiplexing\" Velmi dobry priklad je inetd daemon. Tento demon caka na viacerych portoch na spojenia. Lenze accept moze cakat iba na 1 porte. Ostatne nema ako obsluzit. Tieto funkcie mu to ale umoznia: dokazu cakat na viacerych file deskriptoroch (socketoch) a ked sa na nejakom nieco stane, vratia nejaku hodnotu a mi mozme acceptnut dany socket, na ktorom pozaduje spojenie klient. Maju vela sposobov vyuzitia, mozme pouzit tieto funkcie nie len predaccept, ale aj ked mame uz viacero otvorenych spojeni a obsluhujeme ich naraz. (Na konci uvediem priklady, kde pouzijem poll aj select)

send a recv
Existuju funkcie send,sendto,sendmsg a recv,recvfrom,recvmsg, pouzivaju sa na posielani datagramov (paketov, niekedy aj ramcov). Pozrite si ich manualovu stranku, ak vas zaujimaju viac.

NEJAKE PRIKLADY
klient, co sa bude niekde pripajat pomocou tcp (pouzijeme poll)
conn.c

bindshell - caka na nejkom porte a hodi shell
bindshell.c

redir - tcp redirektor (pouzijeme select)
redir.c

Pouzite zdroje:
developers-handbook v FreeBSD
manualove stranky v FreeBSD
stanojr