6. Взаимодействие Клиент-Сервер
Этот мир — клиент-серверный, малыш. Практически всё в сети происходит по клиент-серверной логике. Возьмём хоть Telnet. При подключении к удалённому узлу на 23 порт телнетом, программа на этом хосте (так называемый telnetd, сервер telnet) как бы просыпается, возвращается к жизни. Она обрабатывает входящее telnet-соединение, обрабатывает введённые вами логин и пароль, и т.д.
В этой диаграмме показан обмен данными между клиентом и сервером.
Обратим внимание, что клиент-серверная пара может «разговаривать» через SOCK_STREAM, SOCK_DGRAM, да и как угодно иначе — до тех пор, пока они говорят «на одном языке», то есть на одинаковом протоколе.
Некоторые хорошие примеры пар клиент-сервер: telnet/telnetd, FTP/FTPd, Firefox/Apache. Каждый раз, используя фтп, на другой стороне провода вы общаетесь с FTPD-сервером.
Обычно на машине запускается только один экземпляр сервера, который обрабатывает несколько клиентов, используя fork(). Основная процедура: сервер ждёт соединения, accetp() его и fork() — рождает дочерний процесс для обработки каждого соединения. Именно так и будет работать наш простой сервер из следующего раздела.
Простой TCP-сервер
Всё, что делает этот сервер — шлёт строку «Hello, World!n» через потоковое соединение. Всё, что вам нужно сделать для проверки этого сервера — запустить его в одном окне, а в другом запустить telnet и зайти на порт своего сервера:
** server.c — a stream socket server demo
*/#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» // порт, на который будут приходить соединения#define BACKLOG 10 // как много может быть ожидающих соединенийvoid sigchld_handler(int s)
{
while(waitpid(—1, NULL, WNOHANG) > 0);
}
// получаем адрес сокета, ipv4 или 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; // слушаем на sock_fd, новые соединения — на new_fd
struct addrinfo hints, *servinfo, *p;
struct sockaddr_storage their_addr; // информация об адресе клиента
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: %sn», gai_strerror(rv));
return 1;
}
// цикл через все результаты, чтобы забиндиться на первом возможном
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;
}
if (p == NULL) {
fprintf(stderr, «server: failed to bindn»);
return 2;
}
freeaddrinfo(servinfo); // всё, что можно, с этой структурой мы сделали
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);
}
printf(«server: waiting for connections…n»);
while(1) { // главный цикл accept()
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 %sn», s);
if (!fork()) { // тут начинается дочерний процесс
close(sockfd); // дочернему процессу не нужен слушающий сокет
if (send(new_fd, «Hello, world!», 13, 0) == —1)
perror(«send»);
close(new_fd);
exit(0);
}
close(new_fd); // а этот сокет больше не нужен родителю
}
return 0;
}
Весь код содержится в одной большой функции main() для большей синтаксической ясности. Если вам это кажется неудобным, разбейте код на функции поменьше.
(Новая функция — sigaction() — отвечает за подчисткой зомби-процессов, которые возникают после того, как дочерний (fork()) процесс завершает работу. Если вы сделаете много зомби и не подчистите их, это не лучшим образом скажется на работе ОС).
Вы можете получить данные с сервера, используя написанный клиент, в следующем разделе.
Простой TCP-клиент
Эта штука ещё проще, чем сервер. Всё, что делает клиент — конектится к хосту, который вы укажете в командной строке, и к порту 3490. И примет строку, которую отошлёт сервер.
/*
** client.c — a stream socket client demo
*/
#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» // Порт, к которому подключается клиент
#define MAXDATASIZE 100 // максимальное число байт, принимаемых за один раз
// получение структуры sockaddr, IPv4 или 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 hostnamen»);
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: %sn», gai_strerror(rv));
return 1;
}
// Проходим через все результаты и соединяемся к первому возможному
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 connectn»);
return 2;
}
inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
s, sizeof s);
printf(«client: connecting to %sn», s);
freeaddrinfo(servinfo); // эта структура больше не нужна
if ((numbytes = recv(sockfd, buf, MAXDATASIZE—1, 0)) == —1) {
perror(«recv»);
exit(1);
}
buf[numbytes] = ‘;
printf(«client: received ‘%s’n»,buf);
close(sockfd);
return 0;
}
Обратите внимание, что если вы запустите клиент раньше сервера, connect() вернёт «Connection refused».
UDP-сокеты
Основы UDP-сокетов мы уже рассмотрели, когда обсуждали sendto и recvfrom, так что я просто приведу пару примеров — talker.c и listener.c
listener запущен на машине и ждёт входящие пакеты на порту 4950. talker посылает пакеты на это порт на указанной машине.
Код listener.c:
** listener.c — a datagram sockets «server» demo
*/#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» // порт, к которому будет соединяться клиент
#define MAXBUFLEN 100
// получаем sockaddr, IPv4 или 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; // если нужен только IPv4, замените на AF_INET
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE; // использовать мой IP
if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0) {
fprintf(stderr, «getaddrinfo: %sn», gai_strerror(rv));
return 1;
}
// цикл через все результаты, бинд на первый возможный
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 socketn»);
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 %sn»,
inet_ntop(their_addr.ss_family,
get_in_addr((struct sockaddr *)&their_addr),
s, sizeof s));
printf(«listener: packet is %d bytes longn», numbytes);
buf[numbytes] = ‘;
printf(«listener: packet contains «%s«n», buf);
close(sockfd);
return 0;
}
Обратите внимание, что вызывая getaddrinfo() мы наконец-то используем SOCK_DGRAM. Также обратите внимание, что нет необходимости вызывать listen() или accept(). Это один из плюсов использования UDP!
Следующее — код talker.c:
** talker.c — a datagram «client» demo
*/#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» // порт для соединения
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 messagen»);
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: %sn», gai_strerror(rv));
return 1;
}
// пробегаемся по результатам и создаём сокет
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 bind socketn»);
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 %sn», numbytes, argv[1]);
close(sockfd);
return 0;
}
Вот и всё, что необходимо сделать! Запустить «слушателя» на одной машине и «говорилку» на другой! И смотреть, как они общаются.
В принципе, вы не обязаны даже запускать сервер! Вы можете запустить только клиент, и он будет счастливо пулять пакеты в сеть, где они и сгинут, если никто на другом конце вызвать recvfrom(). Помните: UDP-сокет не даёт гарантии, что данные будут доставлены!
За исключением ещё одного случая, которую я уже не раз упоминал — UDP-сокета, устанавливающего соединение. Чтобы превратить наше приложение в устанавливающее соединение, talker должен вызывать connect() и указать адрес listener’а. с этого момента talker сможет отсылать и принимать данные только с адреса, указанного при connect(). И вместо sendto() и recvfrom() вы сможете использовать send() и recv().