26 февраля 2011 г.

Сокеты. Пример создания простого сетевого приложения (клиента и сервера) с использованием TCP.

Почти все приложения которые взаимодействуют с сетью используют библиотеку, поддерживаемую операционной системой, - сокеты (sockets). Ее реализации бывают разными, но в целом принцип работы везде одинаков.
Далее в качестве примера рассмотрим сервер получения времени и клиент к нему. Скорее всего, вам уже известно, что такое клиент и сервер, но вкратце объясню. Грубо говоря,  клиент - сетевое приложение которое обращается к серверу с каким то запросом. А сервер - сетевое приложение, которое обрабатывает запросы от клиентов. Зачастую, сервер - приложение которое запущено как демон, то есть после загрузки в память операционной системы, не завершается, а ждет какого-то события и потом его обрабатывает. Так же следует знать, что обычно один клиент обращается всего лишь к одному серверу, но сервер, может одновременно обрабатывать запросы нескольких клиентов, хотя в этой схеме бывают и исключения (например, adc-клиент).

Сервер времени и даты использует протокол TCP. Вот код такого простого сервера:
#include <sys/socket.h> // собственно, сокеты
#include <string.h> // отсюда мы берем memset()
#include <resolv.h> // тут объявлены структуры sockaddr
#include <stdio.h> // snprintf()
         
#define BUFSIZE 128 // максимальный размер буфера под выводимую строку
#define LISTENQ 256 // максимальный размер очереди клиентов
         
#define bzero(x,y) memset((x),0,(y)) // макрос позволяющий пользоваться bzero вместо memset
         
int main()
{        
    int listenfd, connfd; // дескрипторы сокетов
    struct sockaddr_in servaddr; // структура, которой задается адрес сокета
    char buff[BUFSIZE]; // массив чаров. Туда будет помещаться строка, которую сервер отдаст клиенту
    time_t ticks; // сюда будет помещено время в секундах
         
    listenfd = socket(AF_INET, SOCK_STREAM, 0); // просим операционную систему создать для нас сокет протокола IPv4 (AF_INET) / TCP (SOCK_STREAM). Эта функция вернет дескриптор сокета.
    bzero(&servaddr, sizeof(servaddr)); // зануляем память структуры, это необходимо. Использовать bzero вместо memset сложилось исторически, еще с появления сокетов на BSD. Хотя разницы нет.
    servaddr.sin_family = AF_INET; // структура для IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // задаем адрес
    servaddr.sin_port = htons(1313); // задаем порт. Стандартно для сервера времени - 13, но его от обычного пользователя мы не сможем использовать.
         
    bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); // "привязываем" сокет к адресу и порту
         
    listen(listenfd, LISTENQ); // говорим операционной системе ожидать подключений к нашему сокету
         
    for(;;) { 
        connfd = accept(listenfd, (struct sockaddr*) NULL, NULL); // когда клиент подключается к серверу, то операционная система создаст сокет. Собственно этой функцией и задаем подключение клиентов, она возвращает дескриптор на новый сокет клиента
        ticks = time(NULL); // получаем текущее время в секундах
        snprintf(buff, sizeof(buff), "%.24s\er\en\n", ctime(&ticks)); // переводим секунды в удобный для восприятия формат
        send(connfd, buff, strlen(buff), 0); // отправляем данные (на самом деле ничего не отправляется тут)
        close(connfd); // закрываем соединение
    }    
    return 0;
}
Предупреждаю, что возвращаемые значения функций там не обрабатываются, а они возвращают 0 или номер ошибки. Поэтому, если у вас эта программа не будет работать, то скорее всего в работе какой-то функции возникает ошибка. Хорошим тоном, будет создать для всех функций обвертки и проверять в них возвращаемые значения, если функция вернет не ноль, то выводить ошибку и завершать программу.
Еще хочу сказать про функцию send, казалось бы, по названию можно подумать что она что-то там куда-то отправляет, но это не так. На самом деле она всего лишь помещает данные в буфер TCP (этот буфер уникальный для каждого порта) операционной системы, а уже она будет что-то там пытаться отправить. То есть, ничто не может гарантировать успешную отправку данных.

Ну что, компилируем (обратите внимание, никакие динамические библиотеки подключать не надо), запускаем, программа "висит". Хорошо. Пробуем подключиться с помощью telnet:
$ telnet 127.0.0.1 1313
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Sat Feb 26 15:46:12 2011
Connection closed by foreign host.
Все работает!


Далее напишем клиент:
#include <sys/socket.h>
#include <string.h>
#include <resolv.h>
#include <stdio.h>
     
#define BUFSIZE 128
     
#define bzero(x,y) memset((x),0,(y))
     
int main()
{    
    int connfd;
    struct sockaddr_in servaddr;
    char buff[BUFSIZE];
    char servIP[] = "127.0.0.1"; // задаем IP адрес сервера
     
    connfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, servIP, &servaddr.sin_addr); // заполняем поле адреса (32 бита) с помощью функции inet_pton
    servaddr.sin_port = htons(1313);
     
    connect(connfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); // подключаемся к серверу
    recv(connfd, buff, BUFSIZE, 0); // получаем данные от сервера (вернее получаем данные из буфера TCP)
     
    printf("%s", buff);
     
    close(connfd);
    return 0;
}
Здесь, так же, обработки ошибок нет. Я не стал их делать, чтобы не загромождать код, ведь, это всего лишь примеры.

Кстати, IP адрес и порт которые помещаются в struct sockaddr_in должны иметь сетевой порядок следования байтов. Для этого пользуются специальными функциями преобразования:
htons (host TO net short) - для преобразования порта.
htonl (host TO net long) - для преобразования адресов.
Функция inet_pton преобразует массив символов (строку) в адрес (32 бита для IPv4) с сетевым порядком.
В функциях connect, bind и accept делается преобразование типа к struct sockaddr для их совместимости с различными протоколами IP. Подробно об этом не буду говорить, скажу лишь, то, что это преобразование нужно делать.
Возможно, вы заметили что в коде клиента нет функции bind. Но ведь клиенту тоже нужен порт, с которого он будет отправлять данные? Дело в том, что bind можно и не делать, если так, то операционная система назначит порт для клиента автоматически.
Кстати, полное описание, прототипы, возвращаемые и значения всех функций можно найти в man.



Это были всего лишь основы. Если вам интересно сетевое программирование, то очень советую  почитать книгу UNIX Network Programming (рус. UNIX. Разработка сетевых приложений), автор W. R. Stevens. В ней все очень подробно расписывается, как про сокеты так и про устройство основных протоколов интернета.

5 комментариев:

  1. Большое спасибо за статью! И книжку обязательно почитаю, а то спросят меня про устройство протоколов в приличном обществе а я не кроме http, ftp и tcp/ip ни одного не знаю. Вот стыд то будет =)

    ОтветитьУдалить
  2. Вспомнил информатику в школе. Мы рисовали снеговиков на "Корветах".

    ОтветитьУдалить
  3. в виндах работа с сокетами идёт по похожему принципу

    ОтветитьУдалить