Это архив сайта coldflame.by.ru, он не обновлялся с 2007 года. Мой современный сайт тут: http://leonid.shevtsov.me.
Домой! Обо мне Специально для РИ-06-1 Разнообразное... барахло, короче :) Программы и прочее Статьи и переводы Блог SmartDaemon
Предыдущая ОглавлениеСледующая

5. Клиент-сервер: основы

Это клиент-серверный мир, детка. Практически все в Сети состоит из клиентских процессов, которые общаются с серверными процессами, и наоборот. Взять к примеру telnet. Когда ты соединяешься с удаленным компьютером на порт 23 с помощью клиента telnet, программа на этом компьютере (telnetd, сервер) пробуждается к жизни и начинает диалог с клиентом.


None

Клиент-серверное взаимодействие


Обмен информацией между клиентом и сервером обобщен на Рисунке 2.

Заметь, что пара клиент-сервер может использовать SOCK_STREAM, SOCK_DGRAM, или вообще что угодно, пока они используют один и тот же протокол. Примеры клиент-серверных пар: telnet/telnetd, ftp/ftpd, или bootp/bootpd. Когда ты используешь ftp, ее обслуживает удаленная программа ftpd.

Часто на машине расположен только один сервер, и он обрабатывает множественных клиентов с помощью fork(). Базовая процедура такова: сервер ждет соединения, принимает его через accept(), создает процесс с помощью fork() и обрабатывает его. Всем этим занимается наш пример сервера в следующем разделе.


5.1. Простой потоковый сервер

Этот сервер просто отсылает строку "Hello, World!\n" по потоковому соединению. Чтобы его протестировать, запусти сервер в одном окне и telnet в другом:

$ telnet remotehostname 3490

где remotehostname - имя твоего компьютера.

Код сервера

/*
** server.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 <sys/wait.h>
#include <signal.h>

#define MYPORT 3490    // порт

#define BACKLOG 10     // количество входящих соединений

void sigchld_handler(int s)
{
    while(waitpid(-1, NULL, WNOHANG) > 0);
}

int main(void)
{
    int sockfd, new_fd;  // слушаем на sock_fd, соединение в new_fd
    struct sockaddr_in my_addr;    // локальный адрес
    struct sockaddr_in their_addr; // удаленный адрес
    socklen_t sin_size;
    struct sigaction sa;
    int yes=1;

    if ((sockfd = socket(PF_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; // автоматически указать локальный IP
    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) {  // основной цикл accept()
        sin_size = sizeof(struct sockaddr_in);
        if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
                                                       &sin_size)) == -1) {
            perror("accept");
            continue;
        }
        printf("сервер: соединение с %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;
} 

Я разместил код в одной большой функции main() для ясности. Его можно разбить на меньшие функции, если тебе так проще.

(Вызов sigaction() может быть новым для тебя - это нормально. Этот код удаляет завершившиеся процессы. Если их долго не удалять, администратор точно разозлится.)

Для взаимодействия с этим сервером можно использовать клиента из следующего раздела.


5.2. Простой потоковый клиент

Клиент даже проще, чем сервер. Все, что он делает, это соединяется с сервером, указанном в командной строке, на порт 3490. Он получает строку, посланную сервером.

Код клиента:

/*
** client.c -- пример потокового клиента
*/

#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) {  // получить адрес сервера
        herror("gethostbyname");
        exit(1);
    }

    if ((sockfd = socket(PF_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("Принял: %s",buf);

    close(sockfd);

    return 0;
} 

Заметь, что если не запустить сервер до клиента, connect() вернет "В соединении отказано". Очень полезно.


5.3. Датаграммные сокеты

Тут практически не о чем говорить, так что я просто предоставлю пару примеров: talker.c и listener.c.

listener ждет входящего пакета на порт 4950. talker посылает пакет на этот порт, на указанную машину.

Исходник listener.c:

/*
** 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; // удаленный адрес
    socklen_t addr_len;
    int numbytes;
    char buf[MAXBUFLEN];

    if ((sockfd = socket(PF_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; // автоматически ввести локальный IP
    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("получил пакет от %s\n",inet_ntoa(their_addr.sin_addr));
    printf("длина пакета %d байт\n",numbytes);
    buf[numbytes] = '\0';
    printf("пакет содержит: \"%s\"\n",buf);

    close(sockfd);

    return 0;
} 

Обрати внимание, что в вызове socket() мы наконец-то используем SOCK_DGRAM. Кроме этого, не нужно вызывать listen() или accept(). Это одно из преимуществ использования датаграммных сокетов!

Код talker.c:

/*
** 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 SERVERPORT 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(SERVERPORT); // сетевой адрес
    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("отослал %d байт на %s\n", numbytes, inet_ntoa(their_addr.sin_addr));

    close(sockfd);

    return 0;
}

Вот и все! Запусти listener на одной машине, talker на другой и смотри как они общаются! Бесконечные часы развлечений для всей семьи!

Еще одна небоьшая деталь: соединенный датаграммные сокеты. Допустим, talker вызывает connect() и указывает адрес listener'а. Теперь talker может обмениваться данными только с адресом, указанным в connect(). Поэтому вместо sendto() и recvfrom(); нужно использовать send() и recv().


Предыдущая ОглавлениеСледующая