К содержанию

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 и зайти на порт своего сервера:

telnet localhost 3490

 

Код сервера:

/*
** 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, MAXDATASIZE1, 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, MAXBUFLEN1 , 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().

К содержанию