- Сокеты Беркли
-
Сокеты Беркли — интерфейс программирования приложений (API), представляющий собой библиотеку для разработки приложений на языке Си с поддержкой межпроцессного взаимодействия (IPC), часто применяемый в компьютерных сетях.
Сокеты Беркли (также известные как API сокетов BSD), впервые появились как API в операционной системе 4.2BSD Unix (выпущенной в 1983 году). Тем не менее, только в 1989 году Калифорнийский университет в Беркли смог начать выпускать версии операционной системы и сетевой библиотеки без лицензионных ограничений AT&T, действующих в защищённой авторским правом Unix.
API сокетов Беркли сформировал de facto стандарт абстракции для сетевых сокетов. Большинство прочих языков программирования используют интерфейс, схожий с API языка Си.
API Интерфейса транспортного уровня (TLI), основанный на STREAMS, представляет собой альтернативу сокетному API. Тем не менее, API сокетов Беркли значительно преобладает в популярности и количестве реализаций.
Интерфейс сокета Беркли
Интерфейс сокета Беркли — API, позволяющий реализацию взаимодействия между компьютерами или между процессами на одном компьютере. Данная технология может работать со множеством различных устройств ввода/вывода и драйверов, несмотря на то, что их поддержка зависит от реализации операционной системы. Подобная реализация интерфейса лежит в основе TCP/IP, благодаря чему считается одной из фундаментальных технологий, на которых основывается Интернет. Технология сокетов впервые была разработана в Калифорнийском университете Беркли для применения на Юникс-системах. Все современные операционные системы имеют ту или иную реализацию интерфейса сокетов Беркли, так как это стало стандартным интерфейсом для подключения к сети Интернет.
Программисты могут получать доступ к интерфейсу сокетов на трёх различных уровнях, наиболее мощным и фундаментальным из которых является уровень сырых сокетов. Довольно небольшое число приложений нуждается в ограничении контроля над исходящими соединениями, реализуемыми ими, поэтому поддержка сырых сокетов задумывалась быть доступной только на компьютерах, применяемых для разработки на основе технологий, связанных с Интернет. Впоследствии, в большинстве операционных систем была реализована их поддержка, включая Windows XP.
Заголовочные файлы
Программная библиотека сокетов Беркли включает в себя множество связанных заголовочных файлов. В их числе:
-
<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()
создаёт конечную точку соединения и возвращает дескриптор.socket()
принимает три аргумента:- domain, указывающий семейство протоколов создаваемого сокета. Например:
- type (тип) один из:
SOCK_STREAM
(надёжная потокоориентированная служба (сервис) или потоковый сокет)SOCK_DGRAM
(служба датаграмм или датаграммный сокет)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()
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()
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()
listen()
подготавливает привязываемый сокет к принятию входящих соединений. Данная функция применима только к типам сокетовSOCK_STREAM
иSOCK_SEQPACKET
. Принимает два аргумента:sockfd
— корректный дескриптор сокета.backlog
— целое число, означающее число установленных соединений, которые могут быть обработаны в любой момент времени. Операционная система обычно ставит его равным максимальному значению.
После принятия соединения оно выводится из очереди. В случае успеха возвращается 0, в случае возникновения ошибки возвращается −1.
Прототип
#include <sys/socket.h> int listen(int sockfd, int backlog);
accept()
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
отключает алгоритм Нэгла (англ. Nagle's_algorithm)SO_KEEPALIVE
включает периодические проверки на наличие 'признаков жизни', если это поддерживается ОС.
Блокирующие и неблокирующие сокеты
Сокеты Беркли могут работать в одном из двух режимов: блокирующем или неблокирующем. Блокирующий сокет не возвращает контроль пока не отошлет (или пока не получит) все данные, указанные для операции. Это верно лишь для Linux-систем. В других системах, например во FreeBSD, вполне естественно для блокирующего сокета посылать не все данные. Приложение должно проверять возвращаемое значение для отслеживания того, сколько байт было послано/получено и, соответственно, перепосылать необработанную на данный момент информацию [1]. Это может привести к проблемам, если сокет продолжает «слушать»: программа может повиснуть из-за того, что сокет ждет данных, которые могут никогда не прибыть.
Сокет обычно указывается блокирующим или неблокирующим при помощи функций
fcntl()
илиioctl()
.Передача данных
Для передачи данных можно пользоваться стандартными функциями чтения/записи файлов
read
иwrite
, но есть специальные функции для передачи данных через сокеты:- send
- recv
- sendto
- recvfrom
- sendmsg
- recvmsg
Нужно обратить внимание, что при использовании протокола TCP (сокеты типа
SOCK_STREAM
) есть вероятность получить меньше данных, чем было передано, так как ещё не все данные были переданы, поэтому нужно либо дождаться, когда функцияrecv
возвратит 0 байт, либо выставить флагMSG_WAITALL
для функцииrecv
, что заставит её дождаться окончания передачи. Для остальных типов сокетов флагMSG_WAITALL
ничего не меняет (например, в UDP весь пакет = целое сообщение). Смотри также главу «Блокирующие и неблокирующие сокеты».Высвобождение ресурсов
Система не освобождает ресурсы, выделенные при вызове
socket()
, пока не произойдет вызоваclose()
. Это особенно важно в случае, если вызовconnect()
прошёл неудачно и может быть повторен. Каждый вызовsocket()
должен иметь соответствующий вызовclose()
во всех возможных путях исполнения. Необходимо добавлять заголовочный файл <unistd.h> для поддержки функции закрытия.Результатом выполнения системного вызова
close()
является только обращение к интерфейсу для закрытия сокета, а не закрытие самого сокета. Это является командой для ядра закрыть сокет. Иногда, на серверной стороне сокет может перейти в режим ожиданияTIME_WAIT
до 4 минут.[2]Пример клиента и сервера, использующих 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> 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( 1100 ); stSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); if ( bind( i32SocketFD, ( sockaddr* )&stSockAddr, sizeof( stSockAddr ) ) == -1 ) { perror( "ошибка связывания" ); CloseSocketAndExitWithFailure: close( i32SocketFD ); exit( EXIT_FAILURE ); } if ( listen( i32SocketFD, 10 ) == -1 ) { perror( "ошибка прослушивания" ); goto CloseSocketAndExitWithFailure; } for(;;) { int i32ConnectFD = accept( i32SocketFD, 0, 0 ); if ( i32ConnectFD < 0 ) { perror( "ошибка принятия" ); close( i32ConnectFD ); /* нужно ли закрывать сокет после ошибки? */ goto CloseSocketAndExitWithFailure; } /* выполнение операций чтения и записи ... */ 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( "Ошибка: невозможно создать сокет" ); exit( 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( "Ошибка: первый параметр не относится к категории корректных адресов" ); CloseSocketAndExitWithFailure: close( i32SocketFD ); exit( EXIT_FAILURE ); } else if ( !i32Res ) { perror( "Ошибка: второй параметр не содержит корректного IP-адреса" ); goto CloseSocketAndExitWithFailure; } if ( connect( i32SocketFD, ( const void* )&stSockAddr, /* зачем приведение типа? */ sizeof( stSockAddr ) ) == -1 ) { perror("Ошибка соединения"); goto CloseSocketAndExitWithFailure; } /* выполнение операций чтения и записи ... */ 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; }
См. также
- Компьютерная сеть
- Интернет-сокет
- Unix domain socket
- Winsock — API, основанный на сокетах Беркли, но предназначенный для создания распределённых приложений на платформе Microsoft Windows
Ссылки
Определение стандарта «де юре» интерфейса сокетов, содержащееся в стандарте POSIX, более известное как:
- IEEE Std. 1003.1-2001 Standard for Information Technology — Portable Operating System Interface (POSIX).
- Open Group Technical Standard: Base Specifications, Issue 6, December 2001.
- ISO/IEC 9945:2002
- Сайте Остина — информация об этих стандартах, а также о текущей работе над ними.
- RFC3493 и RFC3542 — описание IPv6-расширения базового API сокетов.
- Unix Manual Pages
- «Изучите алгоритмы работы системных вызовов TCP» (рус.) на сайте IBM.
- Касперски К. "Самоучитель игры на WINSOCK"
- Beej’s Guide to Network Programming — 2007
- UnixSocket FAQ
- Get system IP list — C++ Example
- quick TCP-IP NetIntro with C examples
- Porting Berkeley Socket programs to Winsock — Microsoft’s documentation.
- Programming UNIX Sockets in C — Frequently Asked Questions — 1996
- Linux network programming — Linux Journal, 1998
Категории:- API
- Межпроцессное взаимодействие
-
Wikimedia Foundation. 2010.