Калифорнийский университет в Беркли
- 1 year ago
- 0
- 0
Сокеты Беркли — интерфейс программирования приложений (API), представляющий собой библиотеку для разработки приложений на языке C с поддержкой межпроцессного взаимодействия (IPC), часто применяемый в компьютерных сетях .
Сокеты Беркли (также известные как API сокетов BSD ) впервые появились как API в операционной системе 4.1BSD Unix (выпущенной в 1982 году) . Тем не менее, только в 1989 году Калифорнийский университет в Беркли смог начать выпускать версии операционной системы и сетевой библиотеки без лицензионных ограничений AT&T , действующих в защищённой авторским правом Unix.
API сокетов Беркли сформировал фактически стандарт абстракции для сетевых сокетов. Большинство прочих языков программирования использует интерфейс, схожий с API языка Си.
API Интерфейса транспортного уровня (TLI), основанный на STREAMS, представляет собой альтернативу сокетному API. Тем не менее, API сокетов Беркли значительно преобладает в популярности и количестве реализаций.
Интерфейс сокета Беркли — API , позволяющий реализовывать взаимодействие между компьютерами или между процессами на одном компьютере. Данная технология может работать со множеством различных устройств ввода-вывода и драйверов , несмотря на то, что их поддержка зависит от реализации операционной системы . Подобная реализация интерфейса лежит в основе TCP/IP , благодаря чему считается одной из фундаментальных технологий, на которых основывается Интернет . Технология сокетов впервые была разработана в Калифорнийском университете Беркли для применения на UNIX -системах. Все современные операционные системы имеют ту или иную реализацию интерфейса сокетов Беркли, так как это стало стандартным интерфейсом для подключения к сети Интернет.
Программисты могут получать доступ к интерфейсу сокетов на трёх различных уровнях, наиболее мощным и фундаментальным из которых является уровень сырых сокетов . Довольно небольшое число приложений нуждается в ограничении контроля над исходящими соединениями, реализуемыми ими, поэтому поддержка сырых сокетов задумывалась быть доступной только на компьютерах, применяемых для разработки на основе технологий, связанных с Интернетом. Впоследствии в большинстве операционных систем была реализована их поддержка, включая Windows .
Программная библиотека сокетов Беркли включает в себя множество связанных заголовочных файлов.
<sys/socket.h>
<netinet/in.h>
<sys/un.h>
<arpa/inet.h>
<netdb.h>
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()
принимает три аргумента:
SOCK_STREAM
надёжная потокоориентированная служба (TCP) (сервис) или потоковый сокет
SOCK_DGRAM
служба датаграмм (UDP) или
датаграммный сокет
SOCK_SEQPACKET
надёжная служба последовательных пакетов
SOCK_RAW
Сырой сокет
— сырой протокол поверх сетевого уровня.
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()
возвращают указатель на объект типа
struct hostent
, описывающий интернет-узел по имени или по адресу, соответственно. Эта структура содержит или информацию, полученную от сервера имен или произвольные поля из строки в /etc/hosts. Если локальный сервер имен не запущен, то эти подпрограммы просматривают /etc/hosts. Функции принимают следующие аргументы:
Функции возвращают 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). Приложение должно проверять возвращаемое значение для отслеживания того, сколько байт было послано/получено и, соответственно, перепосылать необработанную на данный момент информацию . Это может привести к проблемам, если сокет продолжает «слушать»: программа может повиснуть из-за того, что сокет ждет данных, которые могут никогда не прибыть.
Сокет обычно указывается блокирующим или неблокирующим при помощи функций
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 минут.
TCP
реализует концепцию соединения. Процесс создаёт TCP-сокет вызовом функции
socket()
с параметрами
PF_INET
или
PF_INET6
, а также
SOCK_STREAM
(Потоковый сокет) и
IPPROTO_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-клиента происходит следующим образом:
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 значительно уступает протоколу 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;
}
Определение стандарта «де юре» интерфейса сокетов, содержащееся в стандарте POSIX , более известное как: