Сокеты Беркли

Сокеты Беркли — интерфейс программирования приложений (API), представляющий собой библиотеку для разработки приложений на языке C с поддержкой межпроцессного взаимодействия (IPC), часто применяемый в компьютерных сетях.

Сокеты Беркли (также известные как API сокетов BSD) впервые появились как API в операционной системе 4.1BSD Unix (выпущенной в 1982 году)[1]. Тем не менее, только в 1989 году Калифорнийский университет в Беркли смог начать выпускать версии операционной системы и сетевой библиотеки без лицензионных ограничений AT&T, действующих в защищённой авторским правом Unix.

API сокетов Беркли сформировал фактически стандарт абстракции для сетевых сокетов. Большинство прочих языков программирования использует интерфейс, схожий с API языка Си.

API Интерфейса транспортного уровня (TLI), основанный на STREAMS, представляет собой альтернативу сокетному API. Тем не менее, API сокетов Беркли значительно преобладает в популярности и количестве реализаций.

Интерфейс сокета Беркли

[править | править код]

Интерфейс сокета Беркли — API, позволяющий реализовывать взаимодействие между компьютерами или между процессами на одном компьютере. Данная технология может работать со множеством различных устройств ввода-вывода и драйверов, несмотря на то, что их поддержка зависит от реализации операционной системы. Подобная реализация интерфейса лежит в основе TCP/IP, благодаря чему считается одной из фундаментальных технологий, на которых основывается Интернет. Технология сокетов впервые была разработана в Калифорнийском университете Беркли для применения на UNIX-системах. Все современные операционные системы имеют ту или иную реализацию интерфейса сокетов Беркли, так как это стало стандартным интерфейсом для подключения к сети Интернет.

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

Заголовочные файлы

[править | править код]

Программная библиотека сокетов Беркли включает в себя множество связанных заголовочных файлов.

<sys/socket.h>
Базовые функции сокетов BSD и структуры данных.
<netinet/in.h>
Семейства адресов/протоколов PF_INET и PF_INET6. Широко используются в сети Интернет, включают в себя IP-адреса, а также номера портов TCP и UDP.
<sys/un.h>
Семейство адресов PF_UNIX/PF_LOCAL. Используется для локального взаимодействия между программами, запущенными на одном компьютере. В компьютерных сетях не применяется.
<arpa/inet.h>
Функции для работы с числовыми IP-адресами.
<netdb.h>
Функции для преобразования протокольных имен и имен хостов в числовые адреса. Используются локальные данные аналогично DNS.
  • sockaddr — обобщённая структура адреса, к которой, в зависимости от используемого семейства протоколов, приводится соответствующая структура, например:
struct sockaddr_in stSockAddr;
...
bind(SocketFD,(const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in));
  • sockaddr_in
  • sockaddr_in6
  • in_addr
  • in6_addr

socket() создаёт конечную точку соединения и возвращает дескриптор. socket() принимает три аргумента:

  • domain, указывающий семейство протоколов создаваемого сокета. Этот параметр задает правила использования именования и формат адреса. Например:
    • PF_INET для сетевого протокола IPv4 или
    • PF_INET6 для IPv6.
    • PF_UNIX для локальных сокетов (используя файл).
  • type (тип) один из:
    • SOCK_STREAM надёжная потокоориентированная служба (TCP) (сервис) или потоковый сокет
    • SOCK_DGRAM служба датаграмм (UDP) или датаграммный сокет
    • SOCK_SEQPACKET надёжная служба последовательных пакетов
    • SOCK_RAW Сырой сокет — сырой протокол поверх сетевого уровня.
  • protocol определяет используемый транспортный протокол. Самые распространённые — это IPPROTO_TCP, IPPROTO_SCTP, IPPROTO_UDP, IPPROTO_DCCP. Эти протоколы указаны в <netinet/in.h>. Значение «0» может быть использовано для выбора протокола по умолчанию из указанного семейства (domain) и типа (type).

Функция возвращает −1 в случае ошибки. Иначе, она возвращает целое число, представляющее присвоенный дескриптор.

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

gethostbyname() и gethostbyaddr()

[править | править код]

Функции gethostbyname() и gethostbyaddr() возвращают указатель на объект типа struct hostent, описывающий интернет-узел по имени или по адресу, соответственно. Эта структура содержит или информацию, полученную от сервера имен, или произвольные поля из строки в /etc/hosts. Если локальный сервер имен не запущен, то эти подпрограммы просматривают /etc/hosts. Функции принимают следующие аргументы:

  • name, определяющий имя хоста. Например: www.wikipedia.org
  • addr, определяющий указатель на struct in_addr, содержащую адрес хоста.
  • len, определяющий длину в байтах addr.
  • type, определяющий тип области адресов хоста. Например: PF_INET

Функции возвращают NULL-указатель в случае ошибки. В этом случае может быть проверена дополнительная целая h_errno для выявления ошибки или неправильного или неизвестного хоста. В противном случае возвращается корректная struct hostent *.

struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, int len, int type);

connect() Устанавливает соединение с сервером. Возвращает целое число, представляющее код ошибки: 0 означает успешное выполнение, а −1 свидетельствует об ошибке.

Некоторые типы сокетов работают без установления соединения, это в основном касается UDP-сокетов. Для них соединение приобретает особое значение: цель по умолчанию для посылки и получения данных присваивается переданному адресу, позволяя использовать такие функции, как send() и recv() на сокетах без установления соединения.

Загруженный сервер может отвергнуть попытку соединения, поэтому в некоторых видах программ необходимо предусмотреть повторные попытки соединения.

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

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

  • sockfd — дескриптор, представляющий сокет при привязке
  • serv_addr — указатель на структуру sockaddr, представляющую адрес, к которому привязываем.
  • addrlen — поле socklen_t, представляющее длину структуры sockaddr.

Возвращает 0 при успехе и −1 при возникновении ошибки.

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

listen() подготавливает привязываемый сокет к принятию входящих соединений (так называемое «прослушивание»). Данная функция применима только к типам сокетов SOCK_STREAM и SOCK_SEQPACKET. Принимает два аргумента:

  • sockfd — корректный дескриптор сокета.
  • backlog — целое число, означающее число установленных соединений, которые могут быть обработаны в любой момент времени. Операционная система обычно ставит его равным максимальному значению.

После принятия соединения оно выводится из очереди. В случае успеха возвращается 0, в случае возникновения ошибки возвращается −1.

#include <sys/socket.h>
int listen(int sockfd, int backlog);

accept() используется для принятия запроса на установление соединения от удаленного хоста. Принимает следующие аргументы:

  • sockfd — дескриптор слушающего сокета на принятие соединения.
  • cliaddr — указатель на структуру sockaddr, для принятия информации об адресе клиента.
  • addrlen — указатель на socklen_t, определяющее размер структуры, содержащей клиентский адрес и переданной в accept(). Когда accept() возвращает некоторое значение, socklen_t указывает сколько байт структуры cliaddr использовано в данный момент.

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

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

Дополнительные параметры для сокетов

[править | править код]

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

  • TCP_NODELAY отключает алгоритм Нейгла;
  • SO_KEEPALIVE включает периодические проверки на наличие 'признаков жизни', если это поддерживается ОС.

Блокирующие и неблокирующие сокеты

[править | править код]

Сокеты Беркли могут работать в одном из двух режимов: блокирующем или неблокирующем. Блокирующий сокет не возвращает контроль, пока не отошлёт (или пока не получит) все данные, указанные для операции. Это верно лишь для Linux-систем. В других системах, например во FreeBSD, вполне естественно для блокирующего сокета посылать не все данные (но можно поставить в send() или recv() флаг MSG_WAITALL). Приложение должно проверять возвращаемое значение для отслеживания того, сколько байт было послано/получено и, соответственно, перепосылать необработанную на данный момент информацию[2]. Это может привести к проблемам, если сокет продолжает «слушать»: программа может повиснуть из-за того, что сокет ждет данных, которые могут никогда не прибыть.

Сокет обычно указывается блокирующим или неблокирующим при помощи функций fcntl() или ioctl().

Передача данных

[править | править код]

Для передачи данных можно пользоваться стандартными функциями чтения/записи файлов read и write, но есть специальные функции для передачи данных через сокеты:

Нужно обратить внимание, что при использовании протокола TCP (сокеты типа SOCK_STREAM) есть вероятность получить меньше данных, чем было передано, так как ещё не все данные были приняты, поэтому нужно либо дождаться, когда функция recv возвратит 0 байт, либо выставить флаг MSG_WAITALL для функции recv, что заставит её дождаться окончания передачи. Для остальных типов сокетов флаг MSG_WAITALL ничего не меняет (например, в UDP весь пакет = целое сообщение). См. также главу «Блокирующие и неблокирующие сокеты».

Высвобождение ресурсов

[править | править код]

Система не освобождает ресурсы, выделенные при вызове socket(), пока не произойдет вызова close(). Это особенно важно в случае, если вызов connect() прошёл неудачно и может быть повторен. Каждый вызов socket() должен иметь соответствующий вызов close() во всех возможных путях исполнения. Необходимо добавлять заголовочный файл <unistd.h> для поддержки функции закрытия.

Результатом выполнения системного вызова close() является только обращение к интерфейсу для закрытия сокета, а не закрытие самого сокета. Это является командой для ядра закрыть сокет. Иногда на серверной стороне сокет может перейти в режим ожидания TIME_WAIT длительностью до 4 минут.[1]

Пример клиента и сервера, использующих TCP

[править | править код]

TCP реализует концепцию соединения. Процесс создаёт TCP-сокет вызовом функции socket() с параметрами PF_INET или PF_INET6, а также SOCK_STREAM (Потоковый сокет) и IPPROTO_TCP.

Создание простейшего TCP-сервера состоит из следующих шагов:

  • Создание TCP-сокетов вызовом функции socket().
  • Привязывание сокета к прослушиваемому порту вызовом функции bind(). Перед вызовом bind() программист должен объявить структуру sockaddr_in, очистить её (при помощи memset()), затем sin_family (PF_INET или PF_INET6) и заполнить поля sin_port (прослушиваемый порт, указать в виде последовательности байтов). Преобразование short int в порядок байтов может быть выполнено при помощи вызова функции htons() (сокращение от «от хоста в сеть»).
  • Подготовка сокета к прослушиванию на предмет соединений (создание прослушиваемого сокета) при помощи вызова listen().
  • Принятие входящих соединений через вызов accept(). Это блокирует сокет до получения входящего соединения, после чего возвращает дескриптор сокета для принятого соединения. Первоначальный дескриптор остаётся прослушиваемым дескриптором, а accept() может быть вызван вновь для этого сокета в любое время (пока он открыт).
  • Соединение с удаленным хостом, которое может быть создано при помощи send() и recv() или write() и read().
  • Итоговое закрытие каждого открытого сокета, который больше не нужен, происходит при помощи close(). Если были любые вызовы fork(), то каждый процесс должен закрыть известные ему сокеты (ядро отслеживает количество процессов, имеющих открытый дескриптор), а кроме того, два процесса не должны использовать один и тот же сокет в одно время.
/* Код сервера на языке Си */

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

#define port	1100

int main(void) {
	struct sockaddr_in stSockAddr;
	int i32SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (i32SocketFD == -1) {
		perror("ошибка при создании сокета");
		exit(EXIT_FAILURE);
	}

	memset(&stSockAddr, 0, sizeof (stSockAddr));

	stSockAddr.sin_family = PF_INET;
	stSockAddr.sin_port = htons(port);
	stSockAddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if (bind(i32SocketFD, (struct sockaddr*) &stSockAddr, sizeof (stSockAddr)) == -1) {
		perror("Ошибка: связывания");

		close(i32SocketFD);
		exit(EXIT_FAILURE);
	}

	if (listen(i32SocketFD, 10) == -1) {
		perror("Ошибка: прослушивания");

		close(i32SocketFD);
		exit(EXIT_FAILURE);
	}

	for (;;) {
		int i32ConnectFD = accept(i32SocketFD, 0, 0);

		if (i32ConnectFD < 0) {
			perror("Ошибка: принятия");
			close(i32SocketFD);
			exit(EXIT_FAILURE);
		}

		/* выполнение операций чтения и записи ... */

		shutdown(i32ConnectFD, SHUT_RDWR);

		close(i32ConnectFD);
	}

	return 0;
}

Создание TCP-клиента происходит следующим образом:

  • Создание TCP-сокета вызовом socket().
  • Соединение с сервером при помощи connect(), передача структуры sockaddr_in с sin_family с указанными PF_INET или PF_INET6, sin_port для указания порта прослушивания (в байтовом порядке) и sin_addr для указания IPv4 или IPv6 адреса прослушиваемого сервера (также в байтовом порядке).
  • Взаимодействие с сервером при помощи send() и recv() или write() и read().
  • Завершение соединения и сброс информации при вызове close(). Аналогично, если были какие-либо вызовы fork(), каждый процесс должен закрыть (close()) сокет.
/* Код клиента на языке Си */

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

int main(void) {
	struct sockaddr_in stSockAddr;
	int i32Res;
	int i32SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (i32SocketFD == -1) {
		perror("Ошибка: невозможно создать сокет");
		return EXIT_FAILURE;
	}

	memset(&stSockAddr, 0, sizeof (stSockAddr));

	stSockAddr.sin_family = PF_INET;
	stSockAddr.sin_port = htons(1100);
	i32Res = inet_pton(PF_INET, "192.168.1.3", &stSockAddr.sin_addr);

	if (i32Res < 0) {
		perror("Ошибка: первый параметр не относится к категории корректных адресов");
		close(i32SocketFD);
		return EXIT_FAILURE;
	} else if (!i32Res) {
		perror("Ошибка: второй параметр не содержит корректного IP-адреса");
		close(i32SocketFD);
		return EXIT_FAILURE;
	}

	if (connect(i32SocketFD, (struct sockaddr*) &stSockAddr, sizeof (stSockAddr)) == -1) {
		perror("Ошибка: соединения");
		close(i32SocketFD);
		return EXIT_FAILURE;
	}

	/* выполнение операций чтения и записи ... */

	shutdown(i32SocketFD, SHUT_RDWR);

	close(i32SocketFD);
	return 0;
}

Пример клиента и сервера, использующих UDP

[править | править код]

UDP основывается на протоколе без установления соединений, то есть протокол, не гарантирующий доставку информации. UDP-пакеты могут приходить не в указанном порядке, дублироваться и приходить более одного раза, или даже не доходить до адресата вовсе. Из-за этих минимальных гарантий UDP значительно уступает протоколу TCP. Отсутствие установки соединений означает отсутствие потоков или соединений между двумя хостами, так как вместо этого данные прибывают в датаграммах (Датаграммный сокет).

Адресное пространство UDP, область номеров UDP-портов (в терминологии ISO — TSAP) полностью отделены от TCP-портов.

Код может создавать UDP-сервер на порту 7654 следующим образом:

int sock = socket( PF_INET, SOCK_DGRAM, IPPROTO_UDP );

struct sockaddr_in sa;
int bound;
ssize_t recsize;
socklen_t *address_len=NULL;

sa.sin_addr.s_addr = htonl(INADDR_ANY);
sa.sin_port = htons( 7654 );

bound = bind( sock, ( struct sockaddr* )&sa, sizeof( struct sockaddr ) );

if ( bound < 0 )
  fprintf( stderr, "bind(): ошибка %s\n", strerror( errno ) );

bind() связывает сокет с парой адрес/порт.

while( 1 )
  {
    printf( "recv test....\n" );
    recsize = recvfrom( sock, ( void* )Hz, 100, 0, ( struct sockaddr* )&sa, address_len );

    if ( recsize < 0 )
      fprintf( stderr, "Ошибка %s\n", strerror( errno ) );

    printf( "recsize: %d\n ", recsize );
    sleep( 1 );
    printf( "datagram: %s\n", Hz );
  }

Такой бесконечный цикл получает все UDP-датаграммы, приходящие на порт 7654, при помощи recvfrom(). Функция использует параметры:

  • сокет,
  • указатель на буфер данных,
  • размер буфера,
  • флаги (аналогично при чтении или других сокетных функциях получения),
  • адресная структура отправителя,
  • длина адресной структуры отправителя.

Простейшая демонстрация отправки UDP-пакета, содержащего «Привет!» на адрес 127.0.0.1, порт 7654, выглядит примерно так:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h> /* для вызова close() для сокета */

int main( void )
  {
    int sock;
    struct sockaddr_in sa;
    int bytes_sent;
    const char* buffer = "Привет!";
    int buffer_length;

    buffer_length = strlen( buffer ) + 1;

    sock = socket( PF_INET, SOCK_DGRAM, IPPROTO_UDP );

    if ( sock == -1 )
      {
         printf("Ошибка создания сокета");
         return 0;
      }

    sa.sin_family = PF_INET;
    sa.sin_addr.s_addr = htonl( 0x7F000001 );
    sa.sin_port = htons( 7654 );

    bytes_sent =
      sendto(
        sock,
        buffer,
        strlen( buffer ) + 1,
        0,
        ( struct sockaddr* )&sa,
        sizeof( struct sockaddr_in )
      );

    if ( bytes_sent < 0 )
      printf( "Ошибка отправки пакета: %s\n", strerror( errno ) );

    close( sock );
    return 0;
  }

Примечания

[править | править код]
  1. Uresh Vahalia. UNIX internals: the new frontiers. — Upper Saddle River, New Jersey 07458: Prentice Hall PTR, 2003. — 844 с. — ISBN 0-13-101908-2.
  2. Beej’s Guide to Network Programming. Дата обращения: 12 декабря 2008. Архивировано 10 апреля 2011 года.

Определение стандарта «де юре» интерфейса сокетов, содержащееся в стандарте POSIX, более известное как: