Сокет - это файловый дескриптор, открытый как для чтения, так и для записи. Предназначен для взаимодействия:
- разных процессов, работающих на одном компьютере (хосте);
- разных процессов, работающих на разных хостах.
Создается сокет с помощью системного вызова socket
:
#include <sys/types.h>
#include <sys/socket.h>
int socket(
int domain, // тип пространства имён
int type, // тип взаимодействия через сокет
int protocol // номер протокола или 0 для авто-выбора
)
Механизм сокетов появился ещё в 80-е годы XX века, когда не было единого стандарта для сетевого взаимодействия, и сокеты являлись абстракцией поверх любого механизма сетевого взаимодействия, поддерживая огромное количество разных протоколов.
В современных системах используемыми можно считать несколько механизмов, определяющих пространство имен сокетов; все остальное - это legacy, которое мы дальше рассматривать не будем.
AF_UNIX
(man 7 unix
) - пространство имен локальных UNIX-сокетов, которые позволяют взаимодействовать разным процессам в пределах одного компьютера, используя в качестве адреса уникальное имя (длиной не более 107 байт) специального файла.AF_INET
(man 7 ip
) - пространство кортежей, состоящих из 32-битных IPv4 адресов и 16-битных номеров портов. IP-адрес определяет хост, на котором запущен процесс для взаимодействия, а номер порта связан с конкретным процессом на хосте.AF_INET6
(man 7 ipv6
) - аналогичноAF_INET
, но используется 128-разрядная адресация хостов IPv6; пока этот стандарт поддерживается не всеми хостерами и провайдерами сети Интернет.AF_PACKET
(man 7 packet
) - взаимодействие на низком уровне.
Через сокеты обычно происходит взаимодействие одним из двух способов (указывается в качестве второго параметра type
):
SOCK_STREAM
- взаимодействие с помощью системных вызововread
иwrite
как с обычным файловым дескриптором. В случае взаимодействия по сети, здесь подразумевается использование протоколаTCP
.SOCK_DGRAM
- взаимодейтсвие без предвариательной установки взаимодействия для отправки коротких сообщений. В случае взаимодействия по сети, здесь подразумевается использование протоколаUDP
.
Иногда сокеты удобно использовать в качестве механизма взаимодействия между разными потоками или родственными процессами: в отличии от каналов, они являются двусторонними, и кроме того, поддерживают обработку события "закрытие соединения". Пара сокетов создается с помощью системного вызова socketpair
:
int socketpair(
int domain, // В Linux поддерживатся только AF_UNIX
int type, // SOCK_STREAM или SOCK_DGRAM
int protocol, // Только значение 0 в Linux
int sv[2] // По аналогии с pipe, массив из двух int
)
В отличии от неименованных каналов, которые создаются системным вызовом pipe
, для пары сокетов не имеет значения, какой элемент массива sv
использовать для чтения, а какой - для записи, - они являются равноправными.
Сокеты могут участвовать во взаимодействии в одной из двух ролей. Процесс может быть сервером, то есть объявить некоторый адрес (имя файла, или кортеж из IP-адреса и номера порта) для приема входящих соединений, либо выступать в роли клиента, то есть подключиться к какому-то серверу.
Сразу после создания сокета, он ещё не готов к взамиодействию с помощью системных вызовов read
и write
. Установка взаимодействия с сервером осуществляется с помощью системного вызова connect
. После успешного выполнения этого системного вызова - взаимодействие становится возможным до выполнения системного вызова shutdown
.
int connect(
int sockfd, // файловый дескриптор сокета
const struct sockaddr *addr, // указатель на *абстрактную*
// структуру, описывающую
// адрес подключения
socklen_t addrlen // размер реальной структуры,
// которая передается в
// качестве второго параметра
)
Поскольку язык Си не является объектно-ориентированным, то нужно в качестве адреса передавать:
- Структуру, первое поле которой содержит целое число со значением, совпадающим с
domain
соответствующего сокета - Размер этой структуры.
Конкретными стурктурами, которые "наследуются" от абстрактной структуры sockaddr
могут быть:
- Для адресного пространства UNIX - стрктура
sockaddr_un
#include <sys/socket.h>
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // нужно записать AF_UNIX
char sun_path[108]; // путь к файлу сокета
};
- Для адресации в IPv4 - структура
sockaddr_in
:
#include <sys/socket.h>
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // нужно записать AF_INET
in_port_t sin_port; // uint16_t номер порта
struct in_addr sin_addr; // структура из одного поля:
// - in_addr_t s_addr;
// где in_addr_t - это uint32_t
};
- Для адресации в IPv6 - структура
sockaddr_in6
:
#include <sys/socket.h>
#include <netinet/in.h>
struct sockaddr_in6 {
sa_family_t sin6_family; // нужно записать AF_INET6
in_port_t sin6_port; // uint16_t номер порта
uint32_t sin6_flowinfo; // дополнительное поле IPv6
struct in6_addr sin6_addr; // структура из одного поля,
// объявленного как union {
// uint8_t [16];
// uint16_t [8];
// uint32_t [4];
// };
// т.е. размер in6_addr - 128 бит
uint32_t sin6_scope_id; // дополнительное поле IPv6
};
Адрес хоста в сети IPv4 - это 32-разрядное беззнаковое целое число в сетевом порядке байт, то есть Big-Endian. Для номеров портов - аналогично.
Конвертация порядка байт из сетевого в системный и наоборот осуществляется с помощью одной из функций, объявленных в <arpa/inet.h>
:
uint32_t htonl(uint32_t hostlong)
- 32-битное из системного в сетевой порядок байт;uint32_t ntohl(uint32_t netlong)
- 32-битное из сетевого в системный порядок байт;uint16_t htons(uint16_t hostshort)
- 16-битное из системного в сетевой порядок байт;uint16_t ntohs(uint16_t netshort)
- 16-битное из сетевого в системный порядок байт.
IPv4 адреса обычно записывают в десятичной записи, отделяя каждый байт точкой, например: 192.168.1.1
. Такая запись может быть конвертирована из текста в 32-битный адрес с помощью функций inet_aton
или inet_addr
.
Системный вызов close
предназначен для закрытия файлового дескриптора, и его нужно вызывать для того, чтобы освободить запись в таблице файловых дескрипторов. Это является необходимым, но не достаточным требованием при работе с TCP-сокетами.
Помимо закрытия файлового дескриптора, хорошим тоном считается уведомление противоположной стороны о том, что сетевое соединение закрывается
Это уведомление осуществляется с помощью системного вызова shutdown
.
Для использования сокета в роли сервера, необходимо выполнить следующие действия:
- Связать сокет с некоторым адресом. Для этого используется системный вызов
bind
, параметры которого точно такие же, как для системного вызоваconnect
. Если на компьютере более одного IP-адреса, то адрес0.0.0.0
означает "все адреса". Часто при отладке и возникает проблема, что порт с определенным номером уже был занят на предыдущем запуске программы (и, например, не был корректно закрыт). Это решается принудительным повторным использованием адреса:
// В релизной сборке такого обычно быть не должно!
#ifdef DEBUG
int val = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val));
#endif
-
Создать очередь, в которой будут находиться входящие, но ещё не принятые подключения. Это делается с помощью системного вызова
listen
, который принимает в качестве параметра максимальное количество ожидающих подключений. Для Linux это значение равно 128, определено в константеSOMAXCONN
. -
Принимать по одному соединению с помощью системного вызова
accept
. Второй и третий параметры этого системного вызова могуть бытьNULL
, если нас не интересует адрес того, кто к нам подключился. Системный вызовaccept
блокирует выполнение до тех пор, пока не появится входящее подключение. После чего - возвращает файловый дескриптор нового сокета, который связан с конкретным клиентом, который к нам подключился.