Weicon socket-tutoriaali

Mureakuha

Loikkaa: valikkoon, hakuun

Weicon socket-tutoriaali

Osa 1 | Osa 2

Socket-ohjelmointi by Marko Parkkola Tämä dokumentti antaa lukijalle perustietämyksen socketeista ja socket-ohjelmoinnista. Dokumentin tarkoitus ei ole opettaa kaikkea, mitä socketit sisältävät, vaan näyttää mistä päästä alkuun. Lukijalla on täydet oikeudet kopioida kaikki tai osia tästä dokumentistä. Jotta lukija voisi täysin ymmärtää dokumenttia ja lähdekoodeja, tulisi hänen hankkia itselleen perus-C-ohjelmointitaidot.

Sisällysluettelo

Mikä on socket?

Socket on kuin tunnelin pää joka voidaan yhdistää kahden pisteen välille. Socket voidaan avata yhden prosessin sisällä, prosessien välille, yhdellä koneella tai useamman koneen välille. Socketteja on monenlaisia, mutta tämä dokumentti keskittyy niistä vain ehkä tärkeimpiin, TCP ja UDP socketteihin. Muita socketit ovat mm. UNIX domain, PFKEY, RAW jne. UNIX domain socketteja käytetään yhden koneen sisällä ja ne ovat nopeampia, kuin UDP tai TCP socketit. RAW socketilla voi lähettää itserakennettuja IP paketteja, kuten ICMP, verkkoon.

Socketit kehitti Berkeley yliopisto tiedonvälitykseen. Välitettävä tieto voi olla minkä tyyppistä tahansa. Socketin (tai sen lähetysfunktion) tarvitsee tietää vain osoite datan alkuun ja kuinka paljon sitä siirretään. TCP ja UDP socketit (protokollat) käsittelevät datan siirron eri tavoin ja on ohjelmoijan päätettävissä kumpaa käyttää. TCP socketilla data saadaan siirrettyä turvallisesti ilman pakettien häviämistä verkon yli. UDP socketit ovat nopeampia, mutta lähettäjä ei voi tietää tuliko data oikein perille. ( On tosin tilanteita, joissa TCP protokolla voi olla nopeampi, kuin UDP. ) TCP:n ja UDP:n välillä on toki myös muita eroja.

Socketteja (TCP) on periaatteessa kahta tyyppiä, serveri ja client socketteja. Serveri socketin tehtäviin kuuluu avata socket, sitoa se (bind), laittaa se kuuntelutilaan (listen) ja hyväksyä tulevat yhteydet. Se prosessoi asiakkaalta tulevia kyselyjä ja vastaa niihin tarvittaessa. Asiakas (client) socketin avaa socketin, yhdistää (connect) sen palvelin (server) sockettiin ja tekee tarvittavat kyselyt palvelimelle. On toki myös muita tapoja, kuin asiakas / palvelin - malli, mutta luulen, että tämä on yleisin.

Erot Windows:n ja Unixin välillä

Windowsissa prosessin tulee kutsua WSAStartup - funktiota ladatakseen DLL kirjaston. Windowsissa on myös laajennuksia socket kirjastoon, jota kutsutaan nimellä WinSock. Ne ovat asynkronisiä socketteja, jotka lähettävät window viestejä tilastaan. Minulla ei ole paljoa tietoa niistä, joten tämä dokumentti ei kajoa niihin. Kun socketteja ei enää tarvita, tulisi Windows ohjelman kutsua WSACleanup - funktiota vapauttaakseen varatut resurssit ja DLL kirjaston.

Socketit voivat käyttää ns. loopback IP osoitetta (127.0.0.1), joka toimii pelkästään yhden koneen sisällä. UNIX:ssa loopbackin hoitaa erillinen ajuri, Windowsissa se hoidetaan TCPIP protokollien sisällä.

Myös jotkin paluuarvot socket kutsuille ovat erilaisia Windowsissa kuin UNIX:ssa.

Tarvittavat kirjastot

Ainakin Linuxissa tarvitset seuraavat headerit ohjelmaasi, jotta socket-ohjelmointi onnistuisi:

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

UDP socket

Aloitetaan UDP socketeilla, koska ne ovat ehkä helpompi ymmärtää, kuin TCP socketit.

UDP socketit ovat ns. yhteyksettömiä socketteja. Kun asiakas avaa socketin, sitä ei yhdistetä palvelimella, kuten TCP sockettia. Tämä tarkoittaa käytännössä, että asiakas ei voi olla varma, että koko palvelinta on olemassa! Ja kun asiakas lähettää dataa palvelimelle, se ei voi tietää mitä datalle oikeasti tapahtuu. Voitkin ehkä ihmetelle, että "minkä takia käyttäisin UDP socketteja ollenkaan, koska ne ovat epäluotettavia?"

Kun TCP lähettää dataa, täytyy toisen pään vastata jokaiseen pakettiin, onko se tullut perille oikein. Jos paketti hukkuu, täytyy se lähtettää uudelleen. Ajattele tilannetta, jossa katsot elokuvaa palvelimelta. Jos jokin frame elokuvasta hukkuu, täytyisi asiakkaan kysyä pakettia uudelleen. Tämä tarkoittaisi, että elokuvaan tulisi katkos, kunnes paketti on saatu perille. Elokuvassa ei yhden tai kahden framen häviämistä välttämättä huomaa, joten TCP protokolla on periaatteessa aivan turha käyttää. UDP ei kysele framea uudestaan vaan jatkaa elokuvan näyttämistä tyytyväisenä.

Kun ohjelmoija haluaa avata UDP socketin, täytyy hänen täyttää tietty struktuuri, jotta socket tietää, minne tavara pitää lähettää. Struktuuri pitää sisällään kaksi tärkeätä arvoa. Ensimmäinen on palvelinkoneen IP osoite ja toinen on porttinumero, jota halutaan käyttää. Porttinumero tulee olla väliltä 0 - 65535, portit alle 1024 ovat varattuja protokollille kuten HTTP ja telnet. Kun socketti avataan, täytyy molempien päiden käyttää samaa porttinumeroa.

Yksinkertainen koodi, jolla avataan UDP socketti osoitteeseen 192.168.1.1, porttiin 8888:

int OpenSocket(char *Data, int DataLen) {
	WSADATA wsadata; /* For Windows only */
	int fd; /* Socket */
	struct sockaddr_in sa; /* Socket structure that spesifies address and port */
 
	if  ( WSAStartup(MAKEWORD(2, 2), &wsadata) == SOCKET_ERROR )
		return -1; /* For Windows only */
 
	/*
	 * PF_INET is macro that means Protocol Family Inet. Don't know much about protocol families.
	 * SOCK_DGRAM means that data is transmitted in datagrams.
	 * IPPROTO_UDP means that socket is bound to UDP protocol.
	 */
	if (( fd = socket ( PF_INET, SOCK_DGRAM, IPPROTO_UDP )) < 0 )
		return -1; /* Socket can't be opened. */
 
	memset ( &sa, 0, sizeof ( sa )); /* Set structure full of zeros. */
	sa.sin_family = AF_INET; /* Address Family Inet = Protocol Family Inet. */
	sa.sin_port = htons ( 8888 ); /* Port number. */
	sa.sin_addr.s_addr = inet_addr ( "192.168.1.1" ); /* Change dottet IP address to network byte order. */
 
	WSACleanup(); /* For Windows only. */
 
	return 0;
}

Nyt meillä on aukinainen socket. Huomaa, että data lähetetään paketteina. Tämä tarkoittaa, että toinen pää vastaanottaa ne kokonaisina, eikä virtana, kuten TCP:ssä. Muista, että paketti saattaa silti hävitä siirron aikana.

Lisätäänpä koodia, joka lähettää dataa. Lisää tämä ennen WSACleanup kutsua:

sendto ( fd, Data, DataLen, 0, (struct sockaddr *)&sa, sizeof ( sa ));

Ensimmäinen argumentti funktiossa on socket kahva, jonka saimme socket kutsussa. Toinen argumentti on osoitin datan alkuun ja kolmas kuinka paljon dataa pitää lähettää. Neljäs on tiettyjä flagejä varten, Microsoft ei kannusta näiden käyttöön. Viides on struktuuri, joka pitää sisällään vastaanottajan osoitteen ja portin ja kuudes on tuon struktuurin koko.

Palvelin puolen socket muistuttaa paljon asiakas sockettia ja palvelin tietysti vastaanottaa dataa. Pieni esimerkki UDP palvelimesta:

int StartServer( void ) {
	WSADATA wsadata; /* For Windows only */
	int fd; /* Socket */
	struct sockaddr_in sa; /* Socket structure that spesifies address and port */
	char buffer[100]; /* Buffer where we copy the data. */
	int buffer_size; /* Maximum amount data we can receive. */
 
	if  ( WSAStartup(MAKEWORD(2, 2), &wsadata) == SOCKET_ERROR )
		return -1; /* For Windows only */
 
	/*
	 * PF_INET is macro that means Protocol Family Inet. Don't know much about protocol families.
	 * SOCK_DGRAM means that data is transmitted in datagrams.
	 * IPPROTO_UDP means that socket is bound to UDP protocol.
	 */
	if (( fd = socket ( PF_INET, SOCK_DGRAM, IPPROTO_UDP )) < 0 )
		return -1; /* Socket can't be opened. */
 
	memset ( &sa, 0, sizeof ( sa )); /* Set structure full of zeros. */
	sa.sin_family = AF_INET; /* Address Family Inet = Protocol Family Inet. */
	sa.sin_port = htons ( 8888 ); /* Port number. */
	sa.sin_addr.s_addr = htonl ( INADDR_ANY ); /* Client socket can come from any address. */
 
	if ( bind ( fd, (struct sockaddr *)&sa, sizeof ( sa )) < 0 )
		return -1; /* Can't bind the socket. */
 
	recv ( fd, buffer, buffer_size, 0 ); /* We receive data from socket. */
 
	WSACleanup(); /* For Windows only. */
 
	return 0;
}

Bindaus ei ole tarpeellista, mutta ohjelman tulisi käyttää sitten recvfrom kutsua määrittääkseen, mistä dataa halutaan.

Näin siis käytetään UDP sockettia. Kuten mainitsin, socket voi lähettää minkälaista dataa tahansa. Ainut mitä sen tulee tietää on osoitin datan alkuun ja sen pituus.

Muista, että kaikki nämä socket kutsut ovat ns. blokkaavia, joten ohjelma ei voi jatkaa, ennen kuin kutsu on suoritettu. Käytännössä tämä tarkoittaa sitä, että jos ohjelma kutsuu recv - funktiota, ja mitään dataa ei olekaan tulossa, ohjelma on jumissa.

TCP socket

TCP socket on ehkä hieman hankalampi oppia, kuin UDP. Ne ovat periaatteessa samoja muutamin poikkeuksin.

TCP on yhteyksellinen protokolla. Kun asiakas avaa socketin, täytyy se yhdistää palvelin socketiin. Tämä tarkoittaa, että sinulla on suora tunneli asiakaskoneen ja palvelinkoneen välillä. Kun asiakas yhdistää socketin palvelimeen, täytyy palvelin socketin olla kuuntelutilassa ja sen täytyy hyväksyä tuleva yhteys. Tämä on hankala kohta. Kun palvelin hyväksyy yhteyden, se itseasiassa avaa uuden socketin ja palauttaa sen käyttäjälle. Tämä on erittäin kätevää varsinkin useampi threadisessä ohjelmassa, joka voi käyttää useampia yhteyksiä yhtäaikaa. Toinen poikkeus on tapa, millä dataa liikutetaan. UDP lähettää datan kokonaisena pakettina, mutta TCP lähettää sen datavirtana. Tämä tarkoittaa, että palvelimen täytyy tietää, kuinka paljon asiakas on lähettämässä dataa. Tämän voi toteuttaa lähettämällä esim. long tyyppinen arvo, joka määrää itse datan pituuden. Tämän jälkeen palvelimen tulee kutsua vastaanottofunktiota (recv), niin monta kertaa, että kaikki data on vastaanotettu. Muista, että recv ei välttämättä palauta kaikkea dataa kerrallaan.

Pieni esimerkki TCP asiakasohjelmasta:

int OpenSocket ( char *Data, int DataLen ) {
	WSADATA wsadata; /* For Windows only */
	int fd; /* Socket */
	struct sockaddr_in sa; /* Socket structure that spesifies address and port */
 
	if  ( WSAStartup(MAKEWORD(2, 2), &wsadata) == SOCKET_ERROR )
		return -1; /* For Windows only */
 
	/*
	 * PF_INET is macro that means Protocol Family Inet. Don't know much about protocol families.
	 * SOCK_STREAM means that data is transmitted as stream.
	 * IPPROTO_TCP means that socket is bound to TCP protocol.
	 */
	if (( fd = socket ( PF_INET, SOCK_STREAM, IPPROTO_TCP )) < 0 )
		return -1; /* Socket can't be opened. */
 
	memset ( &sa, 0, sizeof ( sa )); /* Set structure full of zeros. */
	sa.sin_family = AF_INET; /* Address Family Inet = Protocol Family Inet. */
	sa.sin_port = htons ( 8888 ); /* Port number. */
	sa.sin_addr.s_addr = inet_addr ( "192.168.1.1" ); /* Change dottet IP address to network byte order. */
 
	/*
	 * Here client send special SYN packet / packets to server and waits server to answer with ACK packet.
	 * After ACK is received client send FIN packet and connection is established.
	 */
	if ( connect ( fd, (struct sockaddr *)&sa, sizeof ( sa )) < 0 )
		return -1; /* Can't connect to server. */
 
	/*
	 * Since socket is connected we don't need to use sendto() function.
	 */
	send ( fd, Data, DataLen, 0 );
 
	WSACleanup(); /* For Windows only. */
 
	return 0;
}

Huomaa, että asiakkaan ei tarvitse välittää socketin vaihdosta, kun palvelin hyväksyy yhteyden. Taaskin voit lähettää minkälaistaa dataa tahansa. Tämä esimerkki ei näytä sitä, kuinka kerrotaan, kuinka paljon dataa lähetetään. Jätän sen lukijan mietittäväksi.

Esimerkki TCP palvelimesta ( oletamme sisääntulevan data pituudeksi 100 tavua ):

int StartServer( void ) {
	WSADATA wsadata; /* For Windows only */
	int fd; /* Socket */
	struct sockaddr_in sa; /* Socket structure that spesifies address and port */
	char buffer[100];
	int buffer_len = 100;
	int len, data;
 
	if  ( WSAStartup(MAKEWORD(2, 2), &wsadata) == SOCKET_ERROR )
		return -1; /* For Windows only */
 
	/*
	 * PF_INET is macro that means Protocol Family Inet. Don't know much about protocol families.
	 * SOCK_STREAM means that data is transmitted as stream.
	 * IPPROTO_TCP means that socket is bound to TCP protocol.
	 */
	if (( fd = socket ( PF_INET, SOCK_STREAM, IPPROTO_TCP )) < 0 )
		return -1; /* Socket can't be opened. */
 
	memset ( &sa, 0, sizeof ( sa )); /* Set structure full of zeros. */
	sa.sin_family = AF_INET; /* Address Family Inet = Protocol Family Inet. */
	sa.sin_port = htons ( 8888 ); /* Port number. */
	sa.sin_addr.s_addr = inet_addr ( "192.168.1.1" ); /* Change dottet IP address to network byte order. */
 
	if ( bind ( fd, (struct sockaddr *)&sa, sizeof ( sa )) < 0 )
		return -1; /* Can't bind socket. */
 
	/*
	 * Now we must set socket to listen state.
	 * Second parameter means how many pending connections can be in queue.
	 */
	if ( listen ( fd, SOMAXCONN ) < 0 )
		return -1; /* Can't set to listen state. */
 
	len = sizeof  ( sa );
	if (( fd = accept ( fd, (struct sockaddr *)&sa, &len )) < 0 )
		return -1; /* Can't accept connection. */
 
	while ( buffer_len > 0 ) {
		if (( data = recv ( fd, buffer, buffer_len, 0 )) == 0 )
			break; /* No more data to receive. */
		buffer_len -= data;
	}
 
	WSACleanup(); /* For Windows only. */
 
	return 0;
}

Tämä palvelin hyväksyy vain yhden asiakkaan kerrallaan. Kun yhteys on hyväksytty, talletaan uusi socket muuttujaan ja sitä käytetään vastaanottamaan tuleva data. Oikea palvelin ehkä loisi uuden threadin jokaista asiakasta kohti ja näin ollen voisi hyväksyä useamman asiakkaan kerrallaan. Mutta tämäkin jätetään lukijan mietittäväksi.

Nämäkin funktiot ovat blokkaavia. Yksi tapa olisi kiertää tämä käyttämällä select kutsua, joka ilmoittaa muutoksen socketin tilassa (esim. dataa tulossa). select kutsulle voi määrätä ajan, jonka jälkeen se palaa, vaikka dataa ei olisikaan tulossa, jottei se blokkaisi ohjelmaa täysin. UNIX maailmassa select funktiota voidaan käyttää muidenlaisten kahvojen kanssa, kuin sockettien, mutta Windowissa vain socketit ovat sallittuja. Toinen tapa olisi käyttää ei blokkaavaa sockettia. Tämä tapahtuu laittamalla socket ei blokaavaan tilaan setsockopt funktiolla. Kolmas tapa olisi luoda uusi threadi jokaiselle blokaavalle funktiolle ja terminoida se, jos mitään ei ole tapahtunut määrätyssä ajassa. Tämä tosin voi johtaa synkronoitiongelmiin.

Lyhyt katsaus select kutsuun. Tämä näyttää, kuinka tarkkailla tulevaa dataa:

select( fd, readfds, NULL, NULL, timeout );

fd tarkoittaa 'suurinta' kahvaa fd setissä +1. Tätä ei huomioida Windows koneissa. readfds on fd set struktuuri, joka pitää sisällään socketit, joita kuunnellaan. timeout on timeval struktuuri, joka määrittää, kuinka kauan select saa odottaa tulevaa data. Jos tämä parametri on NULL, select blokkaa, kunnes verkosta tulee dataa.

Miten tästä eteenpäin

Jos kaikki tämä kuulostaa helpolta, suosittelen lukemaan Douglas E. Gomerin kirjan Internetworking with TCPIP, erityisesti kohdat, joissa puhutaan TCP ja UDP protokollista. Linux ohjelmoijat löytävät tietoa man sivuilta ja Windows ohjelmoijat voivat käyttää MSDN:ää (Microsoft Developer Network).

Aiheesta muualla

Beej's Guide to Network Programming, Internet socket tutoriaali (englanniksi)

Henkilökohtaiset työkalut