Представление сетевых адресов

Библиотека Sockets разрабатывалась с учётом возможности поддержки различных сетевых протоколов (и не только сетевых, она используется, например и для работы с локальными UNIX-сокетами). Поэтому её разработчики должны были предусмотреть возможность поддержки адресов различной структуры. Поскольку библиотека разрабатывалась для языка С без применения объектно-ориентированного програм-мирования, то для решения этой задачи авторы использовали подход, при котором сетевой адрес хранится в специальной структуре, передаваемой в функции библиотеки Sockets по указателю. При объявлении функций, работающих с адресом, используется тип struct sockaddr, который выступает в роли «базового класса» по терминологии ООП. При написании программ вместо структуры этого типа нужно использовать конкретные структуры, соответствующие используемому типу сетевых протоколов. Библиотека определяет тип структуры по её первому элементу и размеру (который передаётся отдельным параметром).

Для TCP/IP v4 структура для хранения адреса имеет тип struct sockaddr_in:

#include <netinet/in.h> struct sockaddr_in{ uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };
//
отсутствует во многих системах12
//
опционально
в POSIX13

Последний параметр применяется для выравнивания размера структуры и должен быть заполнен нулями. Остальные элементы несут смысловую нагрузку: sin_family определяет стек протоколов, и должен быть равен AF_INET для TCP/IP. Порт для соединения (на удаленном компьютере) указывается в параметре sin_port. IP адрес удаленного сервера задается структурой sin_addr, имеющей тип in_addr:

struct in_addr { union { struct {
uint8_t s_b1, s_b2, s_b3, s_b4; S_un_b;
// 8 бит // 8 бит // 8 бит // 8 бит
} struct {

uint16_t s_w1, // 16 бит s_w2; // 16 бит
uint32_t } S_un;
} S_un_w; S_addr;
// 32 бита
};

Эта структура позволяет получить доступ к IP-адресу как к одному 32-битному числу (S_addr), двум 16-битным (s_w1, s_w2) или четырем отдельным байтам (s_b1,…,s_b4).

В некоторых новых реализациях (начиная с BSD 4.2) отказались от использования объединения и определяют эту структуру как содержащую один элемент:

#include <netinet/in.h> struct in_addr { in_addr_t s_addr; // 32 бита };

Необходимо отметить, что все поля в структурах sockaddr_in и in_addr необходимо заполнять с использованием сетевого порядка байтов. Для преобразования данных из обычного порядка в сетевой предусмотрены специальные функции:

#include <arpa/inet.h> // или <netinet/in.h> uint32_t htonl( uint32_t hostlong); uint16_t htons( uint16_t hostshort);

Функция htonl() предназначена для преобразования 32-битных чисел, а htons()

– для 16-битных. Обратное преобразование осуществляют функциями ntohl() и

ntohs() соответственно.

Еще одна полезная функция - inet_addr, преобразует IP-адрес, записанный текстовой строкой, в 32-битное число в сетевом порядке:

in_addr_t inet_addr(const char *cp);

Заметим, что эта функция может преобразовывать только адреса, записанные в форме четырех чисел, разделенных точками (например “127.0.0.1”). Преобразование доменных имён она не выполняет.

Все функции библиотеки sockets, которые получают на вход сетевой адрес в виде структуры sockaddr, требуют также и передачи размера этой структуры отдельным параметром. При этом если адрес передаётся от пользовательского процесса ОС (как, например, при запросе соединения с удалённым хостом), то передача размера осуществляется по значению:

struct sockaddr_in serv; /* Заполнить структуру serv{} */ connect(sock,(struct sockaddr *) &serv, sizeof(serv)); // передача по значению

В случае если подразумевается передача адреса от ОС в пользовательский процесс (система сообщает адрес подключившегося к нам клиента), то передача размера происходит по указателю. При этом перед вызовом необходимо присвоить передаваемому значению размер доступной памяти, иначе функция выдаст ошибку: строки код INADDR_NONE (как правило, 0xFFFFFFFF) является корректным широковещательным ip адресом 255.255.255.255. Для избежания этой коллизии рекомендуется использовать более новую функцию inet_aton или функцию inet_pton, которая может работать как с IPv4 так и с IPv6 адресами. К сожалению обе эти функции не доступны под Windows.

Struct sockaddr_in cli; socklen_t len; len = sizeof(cli); // в len указываем размер доступной памяти getpeername(sock,(struct sockaddr *) &cli, &len); // передача по ссылке // значение len может быть изменено

Передача адреса всегда осуществляется по ссылке.

Открытие активного соединения

После того, как мы создали сокет, можно с его помощью соединиться с удаленным компьютером. Для этого используем функцию connect:

#include <sys/types.h> #include <sys/socket.h>   int connect(int s, const struct sockaddr *name, socklen_t namelen);

Первым параметром является сокет, который мы хотим соединить. Второй – адрес сервера, заданный структурой sockaddr_in для TCP/IP. Последним параметром передается размер структуры с адресом.

Функция connect возвращает 0 при успешной установке соединения. В случае ошибки возвращается отрицательное значение. Необходимо понимать, что функции connect необходимо дождаться завершения процесса сетевого соединения, поэтому ее выполнение может занять значительное время, на которое ваша программа будет заблокирована. Также необходимо всегда проверять возвращаемое значение, поскольку процесс установки сетевого соединения часто заканчивается ошибкой.

Собрав все наши знания вместе, мы получим примерно такой фрагмент кода для установки соединения с телнет-сервером на локальной машине:

#define CONNECTION_PORT 23
// Telnet
int sock; sockaddr_in serv_addr; sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(CONNECTION_PORT); serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(sock,(struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) printf("Connection failed.\n");

Обмен данными

После того, как установлено соединение, можно обмениваться данными с удаленной системой. Для этого имеются две функции: send для отправки данных и recv для получения.

#include <sys/types.h> #include <sys/socket.h>
ssize_t send(int s, const void *msg, size_t len, int flags); ssize_t recv(int s, void *buf, size_t len, int flags);

Первым параметром этих функций является сокет, через который будет производиться обмен данными. Второй параметр, buf, является указателем на область с данными, которые необходимо передать или принять. Параметр len определяет число передаваемых или принимаемых данных. Параметр flag позволяет регулировать некоторые особенности обработки данных, так, например, значение MSG_OOB позволяет передать или получить «срочные» (out of band) данные, которые могут быть приняты удаленной стороной быстрее, чем еще имеющиеся в сетевых буферах обычные данные. Функция recv поддерживает флаг MSG_PEEK, позволяющий получить данные, не удаляя их из системного буфера, так что они будут доступны и для следующего вызова recv.

В случае успешного завершения операции обе функции возвращают число

переданных или отправленных байтов. В случае ошибки возвращается значение SOCKET_ERROR. Если соединение было закрыто удаленной стороной и в буфере приема больше не осталось входящих данных, то функция recv вернет 0.

Успешный вызов функции send на самом деле не гарантирует, что все данные были доставлены удаленной системе. Она возвращает управление после того, как все переданные ей данные будут размещены в системном буфере стека протоколов TCP/IP. При этом часть данных может быть уже принята удаленным компьютером, часть может находиться в пути, а часть оставаться на вашем компьютере. Помешать передать данные до конца может множество событий: от сбоев в сети или на одном из компьютеров, до некорректного закрытия сокета вашей программой.

В обычном режиме функции send и recv блокируют работу компьютера, до тех пор, пока не будет выполнена запрошенная операция, например, пока не будет полностью получен по сети запрошенный объем данных. Такое поведение может быть неудобно при написании программ. Первый возможный вариант решения – использовать многопоточное программирование. Например, сервер может создавать по отдельному потоку для каждого клиентского соединения. У протокола TCP/IP прием и передача данных абсолютно независимы, поэтому мы можем без каких-либо последствий читать данные из сокета в одном потоке и записывать в другом. Второй вариант – использовать неблокирующий (non-blocking) режим работы, о котором будет рассказано позднее. Кроме того, имеется функция select, которая позволяет определить, имеются ли новые данные для приема, или свободное место в буферах для отправки:

#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

Эта функция работает с тремя наборами сокетов: набор readfds проверяется на возможность выполнения операции чтения, writefds – записи, а exceptfds – на наличие ошибок или срочных (OOB) данных.

Параметр nfds задаёт максимальное значение номера сокета, которое может содержаться в наборах (проверяются значения с 0 по nfds-1). Функция select будет ожидать выполнения одного из условий в течение времени, заданного в структуре timeval:

struct timeval {

long long };
tv_sec; tv_usec;
// seconds // and microseconds

Для реализации «бесконечного» ожидания необходимо передать вместо указателя на эту структуру значение NULL.

Однако в первую очередь необходимо передать этой функции сокеты, которые мы хотим проверять. Для этого используются специальные переменные, наборы fd_set, которые позволяют передать одновременно несколько сокетов для каждой операции. Для работы с наборами, необходимо определить переменную типа fd_set и подготовить её с помощью специальных макросов:

#include <sys/select.h> FD_ZERO(*set) FD_SET(s, *set) FD_CLR(s, *set) FD_ISSET(s, *set)

Здесь *set – указатель на переменную типа fd_set, а s – SOCKET. Макрос FD_ZERO очищает набор (рекомендуется использовать перед началом работы), FD_SET добавляет сокет в набор, FD_CLR удаляет сокет из набора, а FD_ISSET проверяет, есть ли сокет в наборе (возвращает логическое значение true если s входит в набор set).

Функция select возвращает 0 в случае истечения времени ожидания, SOCKET_ERROR в случае ошибки и число готовых к работе сокетов в случае успешного завершения работы. При этом select модифицирует полученные на вход наборы таким образом, что остаются установленными только те сокеты, которые готовы к выполнению соответствующей операции.

Завершение соединения

Закончив обмен данными, необходимо корректно закрыть соединение. Если соединение будет закрыто некорректно, то могут быть потеряны передаваемые данные и удаленная система своевременно не узнает о закрытии соединения. Корректное завершение работы подразумевает, во-первых, обмен специальными сообщениями о завершении связи и, во-вторых, освобождение системных ресурсов на локальной машине.

Первая стадия этой операции выполняется функцией shutdown:

#include <sys/types.h> #include <sys/socket.h> int shutdown(int s, int how);

Параметр s идентифицирует сокет, соединение по которому должно быть закрыто. Параметр how определяет, как надо закрыть соединение, он может принимать значения SHUT_WR, SHUT_RD и SHUT_RDWR. Тут вновь проявляется полная независимость приемной и передающей частей протокола TCP/IP: можно закрыть сокет для передачи данных, но оставить его открытым для приема использовав значение SHUT_WR. Или наоборот, вызов функции со значением SHUT_RD закроет сокет для приема данных, оставив его открытым для записи. Значение SHUT_RDWR предписывает закрыть соединение в обе стороны. Например, запрашивая страницу с web-сервера можно отправить запрос, закрыть сокет на передачу, принять данные от сервера и после этого закрыть сокет на прием.

После того, как сетевое соединение будет закрыто, можно освободить системные ресурсы, закрыв сокет. Делается это функцией close:

#include <unistd.h> int close(int s);

Функция принимает в качестве параметра дескриптор закрываемого сокета и возвращает 0 в случае успешного выполнения, SOCKET_ERROR в случае ошибки.

Рассмотрим пример программы, устанавливающей соединение с web-сервером, запрашивающей корневую директорию и печатающую ответ сервера.

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

  int main(int argc,char * argv[]) { int s; sockaddr_in addr; int i; char str[101];   if(argc<2) { printf(“enter IP address of server as parameter”); return 100; }    

  // создаём сокет s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(s==INVALID_SOCKET) { printf("socket() error\n"); return 1; }   // заполняем структуру с адресом сервера memset(&addr,0,sizeof(addr)); addr.sin_family=AF_INET; addr.sin_port=htons(80); // для web-ceрверов обычно // используется порт 80 addr.sin_addr.S_un.S_addr=inet_addr(argv[1]); // IP адрес сервера задаётся // в командной строке   // устанавливаем соединение

Прием входящих соединений

Для приема входящих соединений нам также нужно в первую очередь создать сокет с помощью функции socket. В случае исходящего соединения нам было не принципиально, какой локальный порт будет использован системой для открытия соединения. В случае же приема входящих соединений нужно указать локальный адрес, используя функцию bind:

#include <sys/types.h> #include <sys/socket.h> int bind(int s, const struct sockaddr *name, socklen_t namelen);

Также как и при вызове функции connect, локальный адрес передается с помощью указателя name, на структуру типа sockaddr_in. Эта функция позволяет задать не только порт, но и адрес локального интерфейса (их может быть несколько), который будет связан с сокетом. Если это не требуется, то можно вместо локального IP-адреса задать значение INADDR_ANY, которое укажет системе на необходимость использовать все доступные интерфейсы.

Функция bind может быть использована и для задания конкретного значения локального адреса для исходящих соединений, в этом случае она должна быть вызвана до вызова connect.

После того, как мы связали сокет с локальным адресом, мы можем перевести сокет в

режим приема соединений (режим пассивного соединения протокола TCP). Это делается с помощью функции listen:

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

int listen(int s, int backlog);

Параметр s является дескриптором сокета, который переводится в состояние приема,

backlog определяет размер системной очереди входящих соединений.

После успешного вызова listen сокет готов к приему входящих соединений. Как только какая-нибудь удаленная система запросит соединение с нашим адресом, мы сможем установить с ней соединение. Для приема входящего соединения используется функция accept:

#include <sys/types.h> #include <sys/socket.h> int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

Этой функции необходимо передать дескриптор сокета s, который был переведен в состояние приема соединений. Узнать адрес удаленной системы, с которой происходит соединение, можно передав в параметре addr указатель на структуру sockaddr_in, а в параметре addrlen – указатель на переменную, в которой указан размер этой структуры. Если оба этих параметра заданы, то функция accept запишет адрес удаленной системы по указанному адресу. Если адрес удаленной системы не требуется, то можно указать значение NULL.

При вызове функции accept создается новый сокет, который и должен использоваться для обмена данными с удаленной системой. Его дескриптор является возвращаемым значением этой функции. Исходный сокет (который был переведен в режим приема соединений) для обмена данными не используется, он готов для приема следующего соединения с помощью еще одного вызова accept. Сокет, который возвращает функция accept, полностью готов к передаче данных с помощью send/recv и никаких дополнительных подготовительных операций для работы с ним не требуется. В случае ошибки функция accept возвращает значение INVALID_SOCKET.

По умолчанию, функция accept блокирует работу вызвавшего ее потока до тех пор, пока не будет принято входящее соединение. Если это неприемлемо, то можно проверить, имеются ли входящие соединения, передав дескриптор принимающего сокета функции select в списке сокетов, готовых к чтению (readfds).

Пример приёма соединения.

Ниже приведён пример программы, которая принимает входящие соединения на порт 1234, передаёт клиенту текст «Hello!» и закрывает соединение. В момент подключения клиента его адрес и порт печатаются на консоли.

#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h>   void main(int argc,char *argv[]) { int s,client; sockaddr_in addr; int val,len;   // создаём сокет для приёма соединений s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

if(s==INVALID_SOCKET)

{

printf("socket() error\n"); return 1;

}

// разрешаем повторное использование локального порта val=1; setsockopt(s,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val));

// заполняем структуру локальным адресом memset(&addr,0,sizeof(addr)); addr.sin_family=AF_INET;

addr.sin_port=htons(1234); // порт для приёма соединений addr.sin_addr.S_un.S_addr=INADDR_ANY;

// используются все локальные интерфейсы

// связываем сокет с локальным портом if(bind(s,(sockaddr*)&addr,sizeof(addr))!=0)

{

printf("bind() failed\n"); close(s);

return 2;

}

// переводим сокет в режим пассивного соединения if(listen(s,10)!=0)

{

printf("listen() failed\n"); close(s);

return 3;

}

for(;;) // соединения принимаются в бесконечном цикле

{

// необходимо указать размер структуры адреса len=sizeof(addr);

// принимаем входящее соединение client=accept(s,(sockaddr*)&addr,&len); printf("incoming connection from %d.%d.%d.%d:%d\n",

addr.sin_addr.S_un.S_un_b.s_b1, addr.sin_addr.S_un.S_un_b.s_b2, addr.sin_addr.S_un.S_un_b.s_b3, addr.sin_addr.S_un.S_un_b.s_b4, ntohs(addr.sin_port));

// посылаем привет клиенту send(client,"hello!",6,0);

// закрываем соединение и клиентский сокет shutdown(client,SD_BOTH);

close(client);

printf("incoming connection closed\n");

}

return 0;

}

В этой программе использовалась функция setsockopt(), с помощью которой можно устанавливать различные режимы работы сокета. В данном случае она была использована, чтобы включить разрешение на многократное использование локальных портов. Если этого не сделать, то только один сокет может быть связан с одним портом. При попытке использовать порт повторно, функция bind() вернёт ошибку.

Во время отладки программ и их аварийного завершения операционная система не

всегда сразу освобождает порты, использованные разрабатываемой программой. В этом случае использование режима повторного использования позволяет сразу запустить отлаживаемую программу, не ожидая освобождения портов. Для финальной версии программы такой режим обычно не требуется.

Наши рекомендации