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

4. Системные вызовы

В этом разделе мы добрались до вызовов функций, которые позволят тебе использовать сетевую функциональность Unix. Когда ты вызываешь одну из этих функций, ядро ОС выполняет всю работу автомагически.

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

Возьми это руководство, добавь пару примеров, легкую закуску (боюсь, о ней тебе придется позаботиться самому), железную волю и пламенное сердце, и скоро ты будешь использовать сокеты, как будто всю жизнь только этим и занимался!


4.1. socket() - Дай мне дескриптор файла!

Не могу больше оттягивать этот момент - пора рассказать о функции socket(). Вот расклад:

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol); 

Но что значат эти аргументы? domain должен быть равен "PF_INET". type сообщает ОС тип сокета: SOCK_STREAM или SOCK_DGRAM. Наконец, оставь protocol равным "0" и socket() сам выберет нужный протокол, основываясь на type. (Замечание: есть гораздо больше domain'ов чем я указал. Есть гораздо больше type'ов чем я указал. Подсмотри socket() в справке. Есть и более "правильный" срособ указать protocol, но 0 работает в 99.9% ситуаций. Подсмотри getprotobyname(), если тебе интересно.)

socket() возвращает дескриптор сокета для последующих вызовов, или -1 при ошибке. Глобальная переменная errno тогда содержит номер ошибки. (Описания ошибок на странице perror() справки.)

(PF_INET близко по значению к AF_INET, которую ты использовал при инициализации поля sin_family в struct sockaddr_in. По сути, они даже имеют одинаковое значение, и многие программисты передают в socket() аргументом AF_INET вместо PF_INET. А теперь пора слушать сказку. Давным-давно люди считали, что, возможно, семья адресов (Address Family - "AF" в "AF_INET") будет поддерживать много протоколов, которые объединятся в семью протоколов (Protocol Family - "PF" в "PF_INET"). Но этого не случилось. И жили они долго и счастливо. Конец. Так что правильнее использовать AF_INET в struct sockaddr_in и PF_INET в socket().)

Ну ладно, а на что способен этот сокет? Пока абсолютно ни на что, и тебе нужно читать дальше, как его использовать.


4.2. bind() - На каком порту я сижу?

Раз у тебя есть сокет, его нужно связать с портом на локальной машине (Это делается если ты собираешься ожидать соединения на определенном порту с помощью listen(). MUDы так делают, когда говорят "соединись telnet'ом на x.y.z:6969"). Номер порта используется ОС чтобы передать входящий пакет соответствующему сокету. Если ты будешь только соединяться с помощью connect(), это не нужно. Все равно, прочитай ради интереса.

Вот синтаксис функции bind():

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen); 

sockfd - это дескриптор сокета, возвращенный функцией socket(). my_addr - указатель на struct sockaddr, которая содержит информацию об адресе, т.е. о порте и IP-адресе. addrlen равно sizeof(struct sockaddr).

Давай рассмотрим пример:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MYPORT 3490

main()
{
    int sockfd;
    struct sockaddr_in my_addr;

    sockfd = socket(PF_INET, SOCK_STREAM, 0); // проверяем на ошибки!

    my_addr.sin_family = AF_INET;         // системный порядок байт
    my_addr.sin_port = htons(MYPORT);     // сетевой порядок байт
    my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
    memset(&(my_addr.sin_zero), '\0', 8); // обнуляем

    // не забудь проверить bind() на ошибки
    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
    .
    .
    . 

Здесь есть пара важных мест: my_addr.sin_port в Сетевом Порядке. my_addr.sin_addr.s_addr тоже. Еще одна проблема - файлы заголовков меняются от системы к системе. Смотри справку.

Наконец, процесс получения адреса и порта может быть автоматизирован:

my_addr.sin_port = 0; // выбрать произвольный порт
my_addr.sin_addr.s_addr = INADDR_ANY;  // использовать мой IP-адрес

Приравнивая my_addr.sin_port к нулю, ты просишь bind() выбрать любой порт. Приравнивая my_addr.sin_addr.s_addr к INADDR_ANY, ты просишь ее автоматически подставить IP-адрес локальной машины.

Если ты обращаешь внимание на мелочи, то, наверное, заметил, что я не преобразовывал INADDR_ANY в Сетевой Порядок! Такой я нехороший. Тем не менее, у меня есть секретная информация: INADDR_ANY на самом деле равен нулю! Ноль всегда ноль, как бы не переставляли его байты. Однако, пуристы скажут, что может существовать параллельное измерение, в котором INADDR_ANY равен, например, 12 и там мой код не сработает. Не вопрос!

my_addr.sin_port = htons(0); // выбрать произвольный порт
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // использовать мой IP-адрес

Теперь этот код такой портабельный, что ты даже не поверишь. Тем не менее, чаще всего ты не будешь прогонять INADDR_ANY через htonl().

bind() также возвращает -1 при ошибке и сохраняет в errno ее ошибки.

Еще одна проблема с bind(): следи за своими номерами портов. Все порты до 1024 ЗАРЕЗЕРВИРОВАНЫ (если ты не суперпользователь)! Ты можешь выбрать любой порт от 1024 до 65535, если он еще не используется другой программой.

Иногда при перезапуске сервера bind() выдает ошибку "Адрес уже используется". Что это значит? Кусочек сокета все еще не закрылся и занимает порт. Ты можешь либо подождать, пока он закроется (около минуты), или использовать код, позволяющий повторно использовать порт:

int yes=1;
//char yes='1'; // использовать это на Solaris

// избавиться от ошибки "Адрес уже используется"
if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
    perror("setsockopt");
    exit(1);
} 

Последнее замечание по поводу bind(): есть случаи, когда ее совсем не надо вызывать. Если ты соединяешься с другой машиной и не беспокоишься о локальном порте (как telnet, например), то тебе нужно просто вызвать connect(), и она сама привяжет сокет к неиспользованному локальному порту.


4.3. connect() - Эй, ты!

Давай представим на минутку, что ты программа telnet. Твой пользователь приказывает тебе (как в фильме ТРОН) открыть дескриптор сокета. Ты вызываешь socket(). Потом пользователь требует соединиться с "10.12.110.57" на порт "23" (стандартный порт telnet) Ого! Что теперь делать?

К счастью для тебя, программа, ты читаешь раздел connect() - как соединиться с удаленным компьютером. Читай скорее! Пользователь ждет!

Вызов connect():

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 

sockfd - наш дескриптор сокета, который был возвращен функцией socket(), serv_addr это struct sockaddr, содержащая адрес и порт назначения,а addrlen равен sizeof(struct sockaddr).

Начинаешь что-то понимать? Давай посмотрим пример:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define DEST_IP   "10.12.110.57"
#define DEST_PORT 23

main()
{
    int sockfd;
    struct sockaddr_in dest_addr;   // адрес назначения

    sockfd = socket(PF_INET, SOCK_STREAM, 0); // проверяй на ошибки!

    dest_addr.sin_family = AF_INET;          // системный порядок байтов
    dest_addr.sin_port = htons(DEST_PORT);   // сетевой порядок байтов
    dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
    memset(&(dest_addr.sin_zero), '\0', 8);  // обнуляем

    // не забудь проверить на ошибки!
    connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
    .
    .
    . 

Не забудь проверить результат connect() - она вернет -1 при ошибке и сохранит в errno ее номер.

Заметь, мы не вызывали bind(). Нам все равно, какой локальный порт нам выделят; важен только порт, на который мы соединяемся (удаленный порт). ОС автоматически выберет нам порт, и удаленный компьютер автоматически получит эту информацию. Никаких проблем.


4.4. listen() - Позвоните мне, кто-нибудь...

Пора сменить цель. А что если ты не хочешь никуда соединяться? Допустим, ты хочешь сам подождать соединения и как-то его обработать. Этот процесс состоит их двух шагов: listen() и accept().

Вызов listen довольно прост:

int listen(int sockfd, int backlog); 

sockfd - дескриптор сокета. backlog - объем очереди входящих соединений. Что это значит? Входящие соединения будут ждать в очереди, пока ты их не подтвердишь. Большинство систем ограничивают этот объем где-то до 20; скорее всего, его можно установить в 5 или 10.

Как всегда, listen() возвращает -1 при ошибке и сохраняет в errno ее номер.

Перед вызовом listen() нужно вызвать bind(), или ОС посадит нас слушать произвольный порт. Так что код для этого выглядит так:

socket();
bind();
listen();
/* здесь разместить accept() */ 

Я не буду писать более полный пример, так как эта часть довольно понятна. (Код в разделе accept() подробнее.) Самая хитроумная часть всего дела - это вызов accept().


4.5. accept() - "Спасибо, что позвонили на порт 3490."

Приготовься - вызов accept() довольно странный! Вот что должно произойти: кто-то издалека попытается вызвать connect() к твоей машине, на порт, на котором ты запустил listen(). Соединение будет добавлено в очередь, ожидая вызова accept(). Ты вызываешь accept() и она принимает ожидающее соединение. Она вернет тебе еще один дескриптор сокета для этого конкретного соединения! Да-да, теперь у тебя два дескриптора! Первый все еще слушает порт, а второй наконец-то готов к передаче данных. Ура!

Вызов функции:

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

sockfd - дескриптор, который был передан listen(). В addr возвратится адрес удаленной машины - помни, что сюда можно передать и указатель на struct sockaddr_in! В addrlen нужно указать sizeof(struct sockaddr_in) до вызова accept(). Accept не вернет больше байт в addr, чем указано. Если она вернет меньше, то изменит значение addrlen.

Опять, accept() вернет -1 при ошибке и сохранит в errno ее номер.Неожиданно, да?

Вот обещанный пример:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490    // порт, который будем слушать

#define BACKLOG 10     // сколько соединений хранить в очереди

main()
{
    int sockfd, new_fd;  // слушаем на sock_fd, новый сокет в new_fd
    struct sockaddr_in my_addr;    // локальный адрес 
    struct sockaddr_in their_addr; // удаленный адрес
    int sin_size;

    sockfd = socket(PF_INET, SOCK_STREAM, 0); // проверяй на ошмбки!

    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); // обнуляем

    // проверяй на ошибки!
    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

    listen(sockfd, BACKLOG);

    sin_size = sizeof(struct sockaddr_in);
    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
    .
    .
    . 

Учти, что мы будем использовать дескриптор new_fd во всех вызовах send() и recv() . Если тебе нужно только одно соединение, можешь закрыть слушающий сокет с помощью close().


4.6. send() и recv() - Поговори со мной, крошка!

Эти две функции используются для передачи через потоковые и соединенные датаграммные сокеты. Если ты используешь несоединенные датаграммные сокеты, тебе нужно использовать sendto() и recvfrom().

Вызов send():

int send(int sockfd, const void *msg, int len, int flags); 

sockfd - дескриптор, через который ты будешь слать данные. msg - указатель на данные, которые нужно послать. len - длина данных в байтах. Оставь flags равным 0. (см. send() в справке для получения детальной информации о флагах.)

Пример:

char *msg = "Здесь был Beej!";
int len, bytes_sent;
.
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
. 

send() возвращает количество реально отправленных байт - может быть и меньше, чем ожидалось! Иногда ты шлешь большой пакет данных и система не может обработать его целиком. Она отправляет столько, сколько может за раз, и ты должен переслать остаток позже. Помни, что если результат send() не совпадает со значением len, ты сам должен отослать остаток строки. Впрочем, если пакет достаточно мал (меньше 1К или около того), скорее всего он отошлется за один раз. Опять-таки при ошибке возвращается -1, а номер ошибки сохраняется в errno.

Вызов recv() очень похож:

int recv(int sockfd, void *buf, int len, unsigned int flags); 

sockfd - дескриптор, из которого будем читать, buf - буфер, в который будем читать,len - максимальная длина буфера, а flags опять можно оставить в 0. (см. recv() в справке.)

recv() возвращает количество прочитанных байт, или -1 при ошибке (и сохраняет ее в errno).

recv() может вернуть 0. Это значит, что удаленная сторона закрыла соединение.


4.7. sendto() и recvfrom() - Поговори со мной датаграммами!

"Это все хорошо", скажешь ты, "но что мне делать с несоединенными датаграммными сокетами?".

Поскольку датаграммные сокеты не соединяются с удаленным компьютером, угадай, что нам нужно передать кроме пакета? Правильно! Адрес назначения!

int sendto(int sockfd, const void *msg, int len, unsigned int flags,
           const struct sockaddr *to, socklen_t tolen); 

Как видишь, синтаксис практически такой же, как и у send(). to указывает на struct sockaddr, которая содержит адрес получателя, а в tolen можно передать sizeof(struct sockaddr).

Так же как и send(), sendto() возвращает количество переданных байт (которое опять может быть меньше ожидаемого), или -1 при ошибке.

Точно так же соотносятся recv() и recvfrom(). Синтаксис recvfrom():

int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
             struct sockaddr *from, int *fromlen); 

В from возвращается адрес и порт посылавшей машины. fromlen должен указывать на переменную, которую нужно установить в sizeof(struct sockaddr) до вызова. Функция возвращает в fromlen длину адреса в from.

recvfrom() возвращает количество полученных байт, или -1 при ошибке.

Помни, что если ты вызовешь connect() для датаграммного сокета, то можешь использовать send() и recv() для всех операций. Сокет остается датаграммным и использует UDP, но интерфейс автоматически подставляет информацию об отправителе и получателе.


4.8. close() и shutdown() - Исчезни!

...Ты слал и принимал данные целый день, и уже порядком устал. Теперь можешь закрыть дескриптор сокета. Это можно сделать обычной функцией закрытия дескриптора Unix: close()

close(sockfd); 

Это остановит весь прием и передачу в сокете. Если кто-то попытается писать в него или читать из него на удаленном компьютере, он получит ошибку.

Если ты хочешь получить чуть больше контроля над закрытием сокета, можешь использовать функцию shutdown(). Она позволяет остановить передачу в определенном направлении (как и close()) Синтаксис:

int shutdown(int sockfd, int how); 

sockfd - это закрываемый дескриптор how - одно из следующих:

  • 0 - запрещен прием
  • 1 - запрещена передача
  • 2 - запрещены прием и передача (как close())

shutdown() возвращает 0 при успехе, и -1 при ошибке.

Если ты попытаешься использовать shutdown() на несоединенном датаграммном сокете, это просто сделает невозможным дальнейшее использование send() и recv() (ты можешь использовать их после вызова connect() для этого сокета).

Важно, что shutdown() не закрывает сокет - она просто изменяет его доступность. Чтобы освободить связанные с ним ресурсы, вызови close().

Вот и все.


4.9. getpeername() - Ты кто?

Эта функция простая.

Такая простая, что я почти не выделил ей отдельного раздела. Но все же, вот он.

Функция getpeername() сообщит адрес удаленного компьютера:

#include <sys/socket.h>

int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); 

sockfd - дескриптор сокета, addr указатель на struct sockaddr (то есть struct sockaddr_in), которая будет получать результат, а addrlen- указатель на int, в котором нужно указать sizeof(struct sockaddr).

Функция возвращает -1 при ошибке и сохраняет ее в errno.

Когда у тебя есть этот адрес, можно вызвать inet_ntoa() или gethostbyaddr() для получения более подробной информации. Нет, ты не можешь узнать их логин. (Да, да. Если на той машине работает демон ident, это возможно. Но выходит за рамки этого руководства. Почитай RFC-1413.)


4.10. gethostname() - Кто я?

gethostname() даже проще, чем getpeername(). Она возвращает имя локального компьютера, которое дальше может быть передано gethostbyname() чтобы узнать локальный IP-адрес.

Что может быть интереснее? Я, конечно, знаю пару вещей, но они не относятся к сетевому программированию:

#include <unistd.h>

int gethostname(char *hostname, size_t size); 

Аргументы просты: hostname - указатель на буфер для получения результата, а size - максимальный размер hostname.

Функция возвращает 0 при успехе и -1 при ошибке, устанавливая как обычно errno.


4.11. DNS - ты говоришь "whitehouse.gov", я отвечаю "63.161.169.137"

Если ты не знаешь, что значит DNS, это расшифровывается как "Domain Name Service" (Сервис Имен Доменов). По-простому, ты говоришь ему читабельный адрес сайта, а он сообщает его IP-адрес (чтобы потом использовать его в bind(), connect(), sendto(), или вообще где угодно.) Таким образом, когда пользователь вводит:

$ telnet whitehouse.gov

telnet знает, что он должен соединиться с "63.161.169.137".

Но как работать с DNS? Через функцию gethostbyname():

#include <netdb.h>

struct hostent *gethostbyname(const char *name); 

Как видишь, она возвращает указатель на struct hostent, которая содержит:

struct hostent {
    char    *h_name;
    char    **h_aliases;
    int     h_addrtype;
    int     h_length;
    char    **h_addr_list;
};
#define h_addr h_addr_list[0] 

И вот описания её членов:

  • h_name - Официальное имя хоста
  • h_aliases - Список альтернативных имен хоста, список заканчивается нулем
  • h_addrtype - Тип возвращаемого адреса, обычно AF_INET.
  • h_length - Длина адреса в байтах
  • h_addr_list - Список сетевых адресов хоста в Сетевом Порядке Байтов, заканчивается нулем
  • h_addr - Первый адрес в h_addr_list.

gethostbyname() возвращает указатель на заполненную struct hostent, или ноль при ошибке. НО errno не устанавливается - вместо нее ошибка сохраняется в h_errno. (см. herror())

Но как она используется? Гораздо проще, чем описывается.

Вот пример:

/*
** getip.c - программа получения адреса хоста
*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    struct hostent *h;

    if (argc != 2) {  // проверить ошибки командной строки
        fprintf(stderr,"usage: getip address\n");
        exit(1);
    }

    if ((h=gethostbyname(argv[1])) == NULL) {  // получить информацию о хосте
        herror("gethostbyname");
        exit(1);
    }

    printf("Имя хоста : %s\n", h->h_name);
    printf("IP-адрес  : %s\n", inet_ntoa(*((struct in_addr *)h->h_addr)));
   
   return 0;
} 

Нельзя напечатать ошибку gethostbyname() с помощью perror() (так как errno не используется). Вместо нее используй herror().

Все довольно просто. Ты передаешь строку с именем компьютера ("whitehouse.gov") в gethostbyname(), и получаешь информацию из возвращенной struct hostent.

Единственная странность в примере - вывод IP-адреса. h->h_addr это char*, но inet_ntoa() требует параметром struct in_addr Так что я кастовал h->h_addr в struct in_addr*, а потом разыменовал ее, чтобы получить данные.


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