9. Pistokeohjelmointi
Sisältö:
9 Pistokeohjelmointi
Pistoke-abstraktio on suunniteltu ohjelmointirajapinnaksi TCP/IP-protokollien käyttöä varten. Todennäköisesti ylivoimaisesti eniten käytetty ohjelmointirajapinta TCP/IP-protokollien käyttöön. Pistokerajapinta on toteutettu myös monille muille käyttöjärjestelmille esim. Windows Sockets. Tarpeet/tavoitteet:
- Läpinäkyvyys (ei eroa onko sama vai eri kone)
- Yhteensopivuus (putket, stdin/stdout, read, write)
- Sopivuus erilaisiin ympäristöihin
- Tehokkuus
- Kommunikointipalvelun laadun valinta
Pistokerajapinta mahdollistaa kommunikoinnin verkon yli, eri protokollien käytön ja erilaiset osoitemuodot. Pistoke on tiedostokuvaajan kaltainen abstraktio eli riittää, että tietää keskustelukumppanin osoitteen. Mahdollisia kommunikointisemantiikkoja:
- Järjestyksen säilytys
- Kahdentumattomuus
- Luotettava kuljetus
- Sanomarajojen säilytys
- Pikasanomat
- Yhteydellinen kommunikointi
Useita eri pistoketyyppejä: tietosähkepistoke, tavuvirtapistoke ja pakettipistoke. Pistoke luodaan tiettyyn viestintäalueeseen, joka määrää käytettävät protokollat ja osoitteen muodon. Osoitetyyppi (nimi) määräytyy viestintäalueen (protokollaperheen) käyttämän osoiteperheen mukaan (esim. AF_INET). Yleinen geneerinen muoto:
struct sockaddr { [uint8_t sa_len;} /* Rakenteen koko POSIX.1g*/ short sa_family; /* Osoiteperhe sa_family_t */ char sa_data[14]; /* Osoite (nimi) */ };
9.1 Tavuvirtapistoke
Tavuvirtapistoke on semantiikaltaltaan yhteyspohjainen luotettava, joka säilyttää järjestyksen ja ei monista dataa. Ei säilytä sanomarajoja. Kommunikointi koostuu yhteyden muodostuksesta, tiedon siirrosta ja yhteyden purusta.
Pistoke luodaan funktiolla int socket(int domain, int type, int protocol), joka palauttaa pistokekuvaajan (eräänlainen tiedostokuvaaja). Pistokkeella ei vielä osoitetta ja kytkemätön. Parametreissä domain on viestintäalue (esim PF_UNIX, PF_INET, AF_UNIX, AF_INET), type == SOCK_STREAM pistokkeen tyyppi eli kommunikointisemantiikka ja protocol = PF_UNSPEC (järjestelmä valitsee sopivan protokollan).
Pistoke sidotaan paikalliseen osoitteeseen funktiolla int bind(int sd, const struct sockaddr *name, int namelen), missä sd on pistokekuvaaja, name on osoiterakenne, joka määrittelee paikallisen osoitteen pistokkeelle, ja namelen on osoiterakenteen pituus.
Pistokkeen kytkeminen toiseen pistokkeeseen tapahtuu sekä palvelijassa että asiakkaassa. Palvelija odottaa yhteyspyyntöjä kuuntelupistokkeeseen (listen-pistoke) ja hakee uuden yhteyden (accept-kutsu). Asiakas pyytää yhteyden avausta (connect).
Asiakas aloittaa yhteyden funktiolla int connect(int sd, const struct sockaddr *name, int namelen). Prosessi odottaa connect()-kutsussa, kunnes yhteys on luotu.
Palvelija odottaa passiivisti yhteyden aloitusta funktiolla int listen(int sd, int backlog);, joka alustaa saapuvien pyyntöjen kuuntelun. backlog on hyväksymistä (accept) odottavien yhteyspyyntöjen jonon maksimipituus. Palvelija hyväksyy yhteyden funktiolla int accept(int sd,struct sockaddr *addr,int *addrlen). Oletusarvoisesti prosessi odottaa accept()-kutsussa kunnes uusi yhteys muodostunut. Kutsu palauttaa uuden pistokekuvaajan, yhteyttä ottaneen asiakkaan osoitteen ja osoiterakenteen koon.
Luku ja kirjoitus tapahtuu sovelluksesta riippuen sovellusprotokollan mukaisesti. read() ja write() kelpaavat. Myös muita tapoja (recv(), send(), readv(), writev() ). Yhteys puretaan close-kutsulla.
9.2 Tietosähkepistoke
Tietosähkepistoke on semantiikaltaan yhteydetön, epäluotettava, ei takaa järjestystä ja sanomat saattavat monistua. Sanomarajat säilyvät. Tietosähkepistoke luodaan kiuten tavuvirtapistoke socket()-funktiolla, mutta pistokkeen tyypiksi (type) asetetaan SOCK_DGRAM. Sitominen paikalliseen osoitteeseen tapahtuu bind()-funktiolla.
Sanomia lähetetään funktiolla int sendto(int sd,void *msg,int len,int flags, const struct sockaddr *to, int tolen), missä sd on pistokekuvaaja, msg on sanomapuskuri, len on sanoman pituus, flags on erikoisohjeet, to on vastaanottajan osoite ja tolen on osoiterakenteen koko.
Sanomia vastaanotetaan funktiolla int recvfrom(int sd,void *msg,int len, int flags, struct sockaddr *from,int *fromlen), missä sd on pistokekuvaaja, msg on sanomapuskuri, len on sanoman pituus, flags on erikoisohjeet (esim. MSG_PEEK), from on lähettäjän osoite ja fromlen on osoiterakenteen koko.
Internet-alueen osoiterakenne (AF_INET):
struct sockaddr_in { [uint8_t sa_len;] /* POSIX.1g */ sa_family_t sin_family; /* AF_INET */ in_port_t sin_port; /* portti */ struct in_addr sin_addr; /* IP-osoite */ char sin_zero[8]; }; struct in_addr { u_long s_addr; };
9.3 Muunnosrutiineita
Piste-esitystavan mukaisen merkkijonon muuntaminen binääriesitysmuotoon (verkon tavujärjestys) tapahtuu funktiolla int inet_aton(const char *cp, struct in_addr *ip). Binäärimuotoinen osoite muunnetaan piste-esitykseksi funktiolla char *inet_ntoa(struct in_addr in).Verkossa siirtoa varten tavujärjestykseksi sovittu "big endian" eli eniten merkitsevä tavu ensimmäisenä. Tätä varten on muunnosfunktiot paikallisen ja verkon esitystavan välisiin muunnoksiin:
#include < netinet/in.h > unsigned long int htonl(unsigned long int hostlong); unsigned short int htons(unsigned short int hohstshort); muunna paikallinen esitystapa verkon esitystavaksi long (32-bit) ja short (16-bit) unsigned long int ntohl(unsigned long int netlong); unsigned short int ntohs(unsigned short int netshort); muunna verkon esitystapa paikalliseksi esitystavaksi long (32-bit) ja short (16-bit)
9.4 Esimerkki tavuvirtapistokkeesta
Käännettävät koodit Asiakas ja Palvelin.
/* CLIENT */ #include < stdio.h > #include < sys/types.h > #include < sys/socket.h > #include < netinet/in.h > #include < arpa/inet.h > #define SERV_UDP_PORT 11012 #define SERV_TCP_PORT 11012 #define SERV_HOST_ADDR "128.214.48.122" /* melkki.cs.helsinki.fi */ void my_err(char *error) { perror(error); exit(1); } int main(int argc,char **argv) { int sockfd; struct sockaddr_in serv_addr; /* Open socket */ if(( sockfd = socket(PF_INET,SOCK_STREAM,PF_UNSPEC)) < 0) my_err("socket()"); memset((void *)&serv_addr,sizeof(serv_addr),0); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR); serv_addr.sin_port = htons(SERV_TCP_PORT); if ( connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0 ) my_err("connect()"); str_cli(stdin,sockfd); close(sockfd); exit(0); } void str_cli(FILE *fp,int sockfd) { int n; char sendline[512],recvline[512]; while(fgets(sendline,512,fp) != NULL ) { n = strlen(sendline); if ( write(sockfd,sendline,n,1) != n) my_err("write()"); if (( n = read(sockfd,recvline,512,1)) < 0) my_err("read()"); recvline[n] = '\0'; fputs(recvline,stdout); } if ( ferror(fp) ) my_err("fgets()"); }
/* SERVER */ #include < stdio.h > #include < sys/types.h > #include < sys/socket.h > #include < netinet/in.h > #include < arpa/inet.h > #define SERV_UDP_PORT 11012 #define SERV_TCP_PORT 11012 #define SERV_HOST_ADDR "128.214.48.122" /* melkki.cs.helsinki.fi */ void my_err(char *error) { perror(error); exit(1); } int main(int argc,char **argv) { int listenfd, newsockfd, clilen, childpid; struct sockaddr_in cli_addr, serv_addr; if (( listenfd = socket(PF_INET,SOCK_STREAM,PF_UNSPEC)) < 0 ) my_err("socket()"); memset((void *)&serv_addr,sizeof(serv_addr),0); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_TCP_PORT); if (( bind(listenfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)) my_err("bind()"); if (( listen(listenfd,5 ) < 0)) my_err("listen()"); for(;;) { clilen = sizeof(cli_addr); if (( newsockfd = accept(listenfd,(struct sockaddr *)&cli_addr,&clilen)) < 0) my_err("accept()"); if (( childpid = fork()) < 0) my_err("fork()"); else if ( childpid == 0 ) { close(listenfd); str_echo(newsockfd); exit(0); } close(newsockfd); } } void str_echo(int sockfd) { int n,len; char mesg[512]; for(;;) { if ((n = read(sockfd,mesg,512,1)) < 0 ) my_err("read()"); if (write(sockfd,mesg,n,1) != n) my_err("write()"); } }
9.5 Esimerkki tietosähkepistokkeesta
Käännettävä koodi Asiakas ja Palvelin.
/* CLIENT */ #include < stdio.h > #include < sys/types.h > #include < sys/socket.h > #include < netinet/in.h > #include < arpa/inet.h > #define SERV_UDP_PORT 11012 #define SERV_TCP_PORT 11012 #define SERV_HOST_ADDR "128.214.48.122" /* melkki.cs.helsinki.fi */ void my_err(char *error) { perror(error); exit(1); } int main(int argc,char **argv) { int sockfd; struct sockaddr_in serv_addr; /* Open socket */ if(( sockfd = socket(PF_INET,SOCK_DGRAM,PF_UNSPEC)) < 0) my_err("socket()"); memset((void *)&serv_addr,sizeof(serv_addr),0); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR); serv_addr.sin_port = htons(SERV_UDP_PORT); dg_cli(stdin,sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); close(sockfd); exit(0); } void dg_cli(FILE *fp,int sockfd,const struct sockaddr *pservaddr,int servlen) { int n; char sendline[512],recvline[512]; while(fgets(sendline,512,fp) != NULL ) { n = strlen(sendline); if ( sendto(sockfd,sendline,n,0,pservaddr,servlen) != n) my_err("sendto()"); if (( n = recvfrom(sockfd,recvline,512,0,NULL,NULL)) < 0) my_err("recvfrom()"); recvline[n] = '\0'; fputs(recvline,stdout); } if ( ferror(fp) ) my_err("fgets()"); }
/* SERVER */ #include < stdio.h > #include < sys/types.h > #include < sys/socket.h > #include < netinet/in.h > #include < arpa/inet.h > #define SERV_UDP_PORT 11012 #define SERV_TCP_PORT 11012 #define SERV_HOST_ADDR "128.214.48.122" /* melkki.cs.helsinki.fi */ void my_err(char *error) { perror(error); exit(1); } int main(int argc,char **argv) { int sockfd; struct sockaddr_in cli_addr, serv_addr; if (( sockfd = socket(PF_INET,SOCK_DGRAM,PF_UNSPEC)) < 0 ) my_err("socket()"); memset((void *)&serv_addr,sizeof(serv_addr),0); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_UDP_PORT); if (( bind(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0) my_err("bind()"); dg_echo(sockfd,(struct sockaddr *)&cli_addr,sizeof(cli_addr)); } void dg_echo(int sockfd,struct sockaddr *pcliaddr,int clilen) { int n,len; char mesg[512]; for(;;) { len = clilen; if ((n = recvfrom(sockfd,mesg,512,0,pcliaddr,&len)) < 0 ) my_err("recvfrom()"); if (sendto(sockfd,mesg,n,0,pcliaddr,len) != n) my_err("sendto()"); } }
9.6 Nimi-/osoitetietojen haku
#include < sys/types.h > #include < sys/socket.h > #include < netdb.h > struct hostent { char *h_name; /* 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 */ }; struct servent { char *s_name; /* Name */ char **s_aliases; /* Alias list */ int s_port; /* Port number */ char *s_proto; /* Protocol to use */ };
Koneen osoitetiedot voi hakea koneen nimen perusteella funktiolla struct hostent *gethostbyname(char *name), missä name on koneen nimi merkkijonomuodossa. Funktio palauttaa osoittimen hostent-rakenteeseen tai NULL-osoittimen.
Koneen osoitetiedot voi hakea Internet-osoitteen perusteella funktiolla struct hostent *gethostbyaddr(const char *addr,int len, int type), missä addr on osoitteen tyyppi eli AF_INET tapauksessa struct in_addr, len on osoitteen koko, type on osoitetyyppi (esim AFV_INET). Funktio palauttaa osoitteen hostent-rakenteeseen tai NULL-osoittimen.
Palvelun tiedon palvelun nimen perustella, kun halutaan tietää portti, josta palvelua tarjotaan, kysytään funktiolla struct servent *getservbyname(const char *name,const char *proto). Funktio palauttaa osoittimen servent-rakenteeseen tai NULL-osoittimen.
Palvelun tiedot voi hakea portin perusteella funktiolla struct servent *getservbyport(int port,const char *proto), joka palauttaa osoittimen servent-rakenteeseen tai NULL-osoittimen.
Jan Lindström (Jan.Lindstrom@cs.Helsinki.FI)