To świat klientów i serwerów, chłopcze. Praktycznie wszystkie operacje w sieci to procesy klientów rozmawiające z procesami serwera i vice-versa. Weźmy za przykład telnet. Kiedy łączysz się ze zdalnym hostem na porcie 23 używając telneta (klient), program na tamtym hoście (nazywany telnetd, serwer) budzi się do życia. Zajmuje się przychodzącymi połączeniami telnetowymi, wyświetla ci prośbę o zalogowanie, itd.
Rysunek 2. Interakcja Klient-Serwer.
TODO: wstawić obrazek jeżeli uda się go skądś pobrać. |
Wymiana informacji pomiędzy klientem i serwerem jest przedstawiona w Figure 2.
Zauważ, że para klient-serwer może rozmawiać używając SOCK_STREAM, SOCK_DGRAM, lub czegokolwiek innego (dopóki rozmawiają tym samym językiem). Dobrymi przykładami par klient-serwer są telnet/telnetd, ftp/ftpd, lub bootp/bootpd. Za każdym razem, gdy używasz programu ftp, po drugiej stronie jest zdalny program, ftpd, który ci służy.
Często będzie tylko jeden serwer na maszynie, i ten serwer bedzie obsługiwał wiele klientów używając fork(). Najprostszym sposobem jest: serwer czeka na połączenia, akceptuje je (accept()) i tworzy proces potomny (fork()), który obsłuży to połączenie. To jest właśnie to, co nasz przykładowy serwer robi w następnej sekcji.
Prosty strumieniowy serwer
Wszystko co robi ten serwer to wysłanie tekstu "Hello, World\n" przez połączenie strumieniowe. Wszystko co musisz zrobić, by przetestować ten serwer, to uruchomić go w jednym oknie, i połączyć się za pomocą telneta z drugiego okna:
$ telnet remotehostname 3490
gdzie remotehostname jest nazwą maszyny, na której właśnie pracujesz.
Kod serwera: (Zauważ: znak '\' kończący linię oznacza, że linia jest kontynuowana w następnym wierszu.)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define MYPORT 3490
#define BACKLOG 10
void sigchld_handler( int s )
{
while( wait( NULL ) > 0 );
}
int main( void )
{
int sockfd, new_fd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size;
struct sigaction sa;
int yes = 1;
if(( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) == - 1 ) {
perror( "socket" );
exit( 1 );
}
if( setsockopt( sockfd, SOL_SOCKET, SO_REUSEADDR, & yes, sizeof( int ) ) == - 1 ) {
perror( "setsockopt" );
exit( 1 );
}
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons( MYPORT );
my_addr.sin_addr.s_addr = INADDR_ANY;
memset( &( my_addr.sin_zero ), '\0', 8 );
if( bind( sockfd,( struct sockaddr * ) & my_addr, sizeof( struct sockaddr ) )
== - 1 ) {
perror( "bind" );
exit( 1 );
}
if( listen( sockfd, BACKLOG ) == - 1 ) {
perror( "listen" );
exit( 1 );
}
sa.sa_handler = sigchld_handler;
sigemptyset( & sa.sa_mask );
sa.sa_flags = SA_RESTART;
if( sigaction( SIGCHLD, & sa, NULL ) == - 1 ) {
perror( "sigaction" );
exit( 1 );
}
while( 1 ) {
sin_size = sizeof( struct sockaddr_in );
if(( new_fd = accept( sockfd,( struct sockaddr * ) & their_addr,
& sin_size ) ) == - 1 ) {
perror( "accept" );
continue;
}
printf( "server: got connection from %s\n",
inet_ntoa( their_addr.sin_addr ) );
if( !fork() ) {
close( sockfd );
if( send( new_fd, "Hello, world!\n", 14, 0 ) == - 1 )
perror( "send" );
close( new_fd );
exit( 0 );
}
close( new_fd );
}
return 0;
}
Jeśli jesteś tego ciekaw, to umieściłem ten kod w jednej dużej funkcji main() dla przejrzystości. Możesz spokojnie rozbić go na mniejsze funkcje jeśli to ci poprawi humor.
(Również ten cały sigaction() może być nowy dla ciebie -- nic nie szkodzi. Ten kod jest odpowiedzialny za zbieranie martwych procesów, które się pojawią, gdy procesy-dzieci zakończą działanie. Jeśli zrobisz dużo zombie i nie zbierzesz ich, twój administrator systemu będzie trochę wzburzony.)
Dane z serwera możesz pobrać korzystając z klienta umieszczonego w następnej sekcji.
Prosty strumieniowy klient
Ten gościu jest nawet prostszy niż serwer. Wszystko co robi ten klient, to łączenie się z podanych w linii komend hostem, na port 3490. Pobiera tekst, który serwer wysyła.
Kod klienta:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 3490
#define MAXDATASIZE 100
int main( int argc, char * argv[] )
{
int sockfd, numbytes;
char buf[ MAXDATASIZE ];
struct hostent * he;
struct sockaddr_in their_addr;
if( argc != 2 ) {
fprintf( stderr, "usage: client hostname\n" );
exit( 1 );
}
if(( he = gethostbyname( argv[ 1 ] ) ) == NULL ) {
perror( "gethostbyname" );
exit( 1 );
}
if(( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) == - 1 ) {
perror( "socket" );
exit( 1 );
}
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons( PORT );
their_addr.sin_addr = *(( struct in_addr * ) he->h_addr );
memset( &( their_addr.sin_zero ), '\0', 8 );
if( connect( sockfd,( struct sockaddr * ) & their_addr,
sizeof( struct sockaddr ) ) == - 1 ) {
perror( "connect" );
exit( 1 );
}
if(( numbytes = recv( sockfd, buf, MAXDATASIZE - 1, 0 ) ) == - 1 ) {
perror( "recv" );
exit( 1 );
}
buf[ numbytes ] = '\0';
printf( "Received: %s", buf );
close( sockfd );
return 0;
}
Zauważ, że jeśli nie uruchomisz serwera przed klientem, connect() zwróci "Connection refused" ("Połączenie odrzucone"). Bardzo użytecznie.
Gniazda datagramowe
Naprawdę nie trzeba tutaj tak dużo mówić, więc po prostu przedstawię parę przykładowych programów: talker.c i listener.c.
listener siedzi na maszynie i czeka na pakiety przychodzące na port 4950. talker wysyła pakiet na ten port, na do podanej maszyny, który zawiera cokolwiek użytkownik wprowadzi w lini poleceń.
Tu jest źródło dla listener.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MYPORT 4950
#define MAXBUFLEN 100
int main( void )
{
int sockfd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int addr_len, numbytes;
char buf[ MAXBUFLEN ];
if(( sockfd = socket( AF_INET, SOCK_DGRAM, 0 ) ) == - 1 ) {
perror( "socket" );
exit( 1 );
}
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons( MYPORT );
my_addr.sin_addr.s_addr = INADDR_ANY;
memset( &( my_addr.sin_zero ), '\0', 8 );
if( bind( sockfd,( struct sockaddr * ) & my_addr,
sizeof( struct sockaddr ) ) == - 1 ) {
perror( "bind" );
exit( 1 );
}
addr_len = sizeof( struct sockaddr );
if(( numbytes = recvfrom( sockfd, buf, MAXBUFLEN - 1, 0,
( struct sockaddr * ) & their_addr, & addr_len ) ) == - 1 ) {
perror( "recvfrom" );
exit( 1 );
}
printf( "got packet from %s\n", inet_ntoa( their_addr.sin_addr ) );
printf( "packet is %d bytes long\n", numbytes );
buf[ numbytes ] = '\0';
printf( "packet contains \"%s\"\n", buf );
close( sockfd );
return 0;
}
Zauważ, że w naszym wywołaniu socket() w końcu używamy SOCK_DGRAM. Zauważ również, że nie ma potrzeby korzystania z funkcji listen() oraz accept(). Jest to jedna z zalet korzystania z niepołączonych gniazd strumieniowych!
Przyszła kolej na kod źródłowy talker.c:
#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
int main( int argc, char * argv[] )
{
int sockfd;
struct sockaddr_in their_addr;
struct hostent * he;
int numbytes;
if( argc != 3 ) {
fprintf( stderr, "usage: talker hostname message\n" );
exit( 1 );
}
if(( he = gethostbyname( argv[ 1 ] ) ) == NULL ) {
perror( "gethostbyname" );
exit( 1 );
}
if(( sockfd = socket( AF_INET, SOCK_DGRAM, 0 ) ) == - 1 ) {
perror( "socket" );
exit( 1 );
}
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons( MYPORT );
their_addr.sin_addr = *(( struct in_addr * ) he->h_addr );
memset( &( their_addr.sin_zero ), '\0', 8 );
if(( numbytes = sendto( sockfd, argv[ 2 ], strlen( argv[ 2 ] ), 0,
( struct sockaddr * ) & their_addr, sizeof( struct sockaddr ) ) ) == - 1 ) {
perror( "sendto" );
exit( 1 );
}
printf( "sent %d bytes to %s\n", numbytes,
inet_ntoa( their_addr.sin_addr ) );
close( sockfd );
return 0;
}
I to wszystko co się tego tyczy! Uruchom listener na jednej maszynie, następnie uruchom talker na innej. Patrz jak gadają ze sobą! Fun G-rated excitement for the entire nuclear family!
Poza jednym szczegółem, o którym wspomniałem wiele razy w przeszłości: połączone gniazda datagramowe. Muszę o tym powiedzieć tutaj, ponieważ jesteśmy w sekcji o gniazdach datagramowych. Powiedzmy, że talker wywołuje connect() i podaje adres programu listener. Od tego momentu, talker może wysyłać i odbierać dane tylko z podanego funkcji connect() adresu. Z tego powodu nie musisz używać sendto() ani recvfrom(): możesz po prostu używać send() i recv().