Диаграмма состояний для установления и разрыва соединения показана на илл. 6.4. Каждый переход вызывается каким-то событием — операцией, выполненной локальным пользователем транспортной службы, или входящим пакетом. Для простоты мы будем считать, что каждый сегмент подтверждается отдельно. Мы также предполагаем, что используется модель симметричного разъединения, в которой клиент делает первый ход. Обратите внимание на простоту этого примера. Позднее, когда мы будем говорить о TCP, мы рассмотрим более реалистичные модели.
6.1.3. Сокеты Беркли
Теперь рассмотрим другой набор примитивов транспортного уровня — примитивы сокетов, используемые для протокола TCP. Впервые сокеты стали применяться в 1983 году в операционной системе Berkeley UNIX 4.2BSD. Очень скоро они приобрели популярность и сейчас широко используются для интернет-программирования в большинстве операционных систем, особенно UNIX; кроме того, существует специальный API, предназначенный для программирования сокетов в системе Windows — «winsock».
Илл. 6.4. Диаграмма состояний для простой схемы управления соединениями. Переходы, обозначенные курсивом, вызываются поступлением пакетов. Сплошными линиями показана последовательность состояний клиента. Пунктирными линиями показана последовательность состояний сервера
Примитивы сокетов перечислены на илл. 6.5. Модель сокетов во многом похожа на представленную выше, но обладает большей гибкостью и предоставляет больше возможностей. Сегменты, соответствующие этой модели, будут рассматриваться далее в этой главе.
Первые четыре примитива из списка выполняются серверами в указанной последовательности. Примитив SOCKET создает новый сокет и выделяет для него место в таблице транспортной подсистемы. Параметры вызова указывают используемый формат адресов, тип требуемой службы (например, надежный поток байтов) и протокол. В случае успеха SOCKET возвращает обычный файловый дескриптор, используемый при вызове следующих операций, подобно тому, как процедура OPEN работает для файла.
Примитив
Значение
SOCKET (СОКЕТ)
Создать новый сокет (гнездо связи)
BIND (СВЯЗАТЬ)
Связать локальный адрес с сокетом
LISTEN (ОЖИДАТЬ)
Объявить о желании принять соединение; указать размер очереди
ACCEPT (ПРИНЯТЬ)
Пассивно установить входящее соединение
CONNECT (СОЕДИНИТЬ)
Активно пытаться установить соединение
SEND (ОТПРАВИТЬ)
Отправить данные по соединению
RECEIVE (ПОЛУЧИТЬ)
Получить данные у соединения
CLOSE (ЗАКРЫТЬ)
Разорвать соединение
Илл. 6.5. Примитивы сокетов для TCP
У только что созданного сокета нет сетевых адресов. Они назначаются с помощью примитива BIND. После того как сервер привязывает адрес к сокету, с ним могут связаться удаленные клиенты. Вызов SOCKET не создает адрес напрямую, так как некоторые процессы придают своим адресам большое значение (например, они использовали один и тот же адрес годами, и он известен всем).
Далее идет вызов примитива LISTEN, который выделяет место для очереди входящих соединений на случай, если несколько клиентов попытаются соединиться одновременно. В отличие от аналогичного примитива в нашем первом примере, в модели сокетов LISTEN не является блокирующим вызовом.
Чтобы заблокировать ожидание входящих соединений, сервер выполняет примитив ACCEPT. Получив сегмент с запросом соединения, транспортная подсистема создает новый сокет с теми же свойствами, что и у исходного сокета, и возвращает для него файловый дескриптор. При этом сервер может разветвить процесс или поток, чтобы обработать соединение для нового сокета и вернуться к ожиданию следующего соединения для первоначального сокета. ACCEPT возвращает файловый дескриптор, который можно использовать для чтения и записи стандартным способом, как это делается в случае файлов.
Теперь посмотрим на этот процесс со стороны клиента. И здесь прежде всего должен быть создан сокет с помощью примитива SOCKET, но BIND в этом случае не требуется, так как используемый адрес не имеет значения для сервера. CONNECT блокирует вызывающего и инициирует активный процесс соединения. Когда этот процесс завершается (то есть когда соответствующий сегмент, отправленный сервером, получен), процесс клиента разблокируется, и соединение считается установленным. После этого обе стороны могут использовать SEND и RECIEVE для передачи и получения данных по полнодуплексному соединению. Могут также применяться стандартные UNIX-вызовы READ и WRITE, если нет нужды в использовании специальных свойств SEND и RECIEVE.
В модели сокетов используется симметричный разрыв соединения. Это происходит, когда обе стороны выполняют примитив CLOSE.
Получив широкое распространение, сокеты де-факто стали стандартом абстрагирования транспортных служб для приложений. Часто сокет-API используется вместе с протоколом TCP для предоставления службы, ориентированной на установление соединения, — надежного потока байтов (reliable byte stream); на деле это надежный битовый канал, о котором мы говорили выше. Этот API может сочетаться и с другими протоколами, но в любом случае результат должен быть одинаковым для пользователя.
Преимущество сокет-API состоит в том, что приложение может использовать его и для других транспортных служб. К примеру, с помощью сокетов можно реализовать службу без установления соединения. В этом случае CONNECT задает адрес удаленного узла, а SEND и RECEIVE отправляют и получают дейтаграммы. (Иногда используется расширенный набор вызовов, например примитивы SENDTO и RECEIVEFROM, позволяющие приложению не ограничиваться одним транспортным узлом.) Иногда сокеты используются с транспортными протоколами, в которых вместо байтового потока применяется поток сообщений и которые могут включать (или не включать) контроль перегрузок. К примеру, дейтаграммный протокол с контролем перегрузок (Datagram Congestion Control Protocol, DCCP) является вариантом UDP с управлением перегрузкой (Колер и др.; Kohler et al., 2006). Необходимую службу выбирают сами пользователи.
Тем не менее последнее слово в вопросе транспортных интерфейсов, скорее всего, останется не за сокетами. Довольно часто приложениям приходится работать с группой связанных потоков, например браузер может одновременно запрашивать у сервера несколько объектов. В таком случае применение сокетов обычно означает, что для каждого объекта будет использоваться один поток. В результате управление перегрузкой будет выполняться отдельно для каждого потока (а не для всей группы). Безусловно, это далеко не оптимальный вариант, поскольку управление набором потоков становится задачей приложения. Чтобы более эффективно обрабатывать группы связанных потоков и уменьшить роль приложения в этом процессе, был создан ряд дополнительных протоколов и интерфейсов. В частности, протокол передачи с управлением потоками (Stream Control Transmission Protocol, SCTP), описанный в RFC 4960 (Форд; Ford, 2007), и протокол QUIC (он будет рассмотрен ниже). Эти протоколы слегка изменяют сокет-API для удобства работы с группами потоков, обеспечивая новые возможности, например работу со смешанным трафиком (с установлением соединения и без) и даже поддержку множественных сетевых путей.
6.1.4. Пример программирования сокета: файл-сервер для интернета
Чтобы узнать, как выполняются вызовы для сокета на практике, рассмотрим клиентский и серверный код на илл. 6.6. Имеется примитивный файл-сервер, работающий в интернете, и использующий его клиент. У программы много ограничений (о которых еще будет сказано), но теоретически данный код, описывающий сервер, может быть скомпилирован и запущен на любой UNIX-системе, подключенной к интернету. Код, описывающий клиента, может быть запущен с определенными параметрами. Это позволит ему получить любой файл, к которому у сервера есть доступ. Файл отображается на стандартном устройстве вывода, но, разумеется, может быть перенаправлен на диск или какому-либо процессу.
/* На этой странице содержится клиентская программа, запрашивающая файл у серверной программы, расположенной на следующей странице. Сервер в ответ на запрос высылает файл. */
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8080 /* По договоренности между клиентом и сервером */
#define BUF_SIZE 4096 /* Размер передаваемых блоков */
int main(int argc, char **argv)
{
int c, s, bytes;
char buf[BUF_SIZE]; /* буфер для входящего файла */
struct hostent *h; /* информация о сервере */
struct sockaddr_in channel; /* содержит IP-адрес */
if (argc != 3) {printf("Для запуска введите: client имя_сервера имя_файла"); exit(-1);}
h = gethostbyname(argv[1]); /* поиск IP-адреса хоста */
if (!h) {printf("gethostbyname не удалось найти %s", argv[1]); exit(-1;}
s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s <0) {printf("сбой вызова сокета"); exit(-1);}
memset(&channel, 0, sizeof(channel));
channel.sin_family= AF_INET;
memcpy(&channel.sin_addr.s_addr, h->h_addr, h->h_length);
channel.sin_port= htons(SERVER_PORT);
c = connect(s, (struct sockaddr *) &channel, sizeof(channel));
if (c < 0) {printf("сбой соединения"); exit(-1);}
/* Соединение установлено. Отправляется имя файла с нулевым байтом на конце */
write(s, argv[2], strlen(argv[2])+1);
/* Получить файл, записать на стандартное устройство вывода */
while (1) {
bytes = read(s, buf, BUF_SIZE); /* Читать из сокета */
if (bytes <= 0) exit(0); /* Проверка конца файла */
write(1, buf, bytes);