Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 7 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]
Приняв все вышеизложенные термины и определения, посмотрим на рис. 2.15, где показано, что происходит, когда приложение записывает данные в сокет TCP.
Рис. 2.15. Этапы записи данных в сокет TCP и буферы, используемые при этой записи
У каждого сокета TCP есть буфер отправки, и мы можем изменять размер этого буфера с помощью параметра сокета SO_SNDBUF
(см. раздел 7.5). Когда приложение вызывает функцию write
, ядро копирует данные из буфера приложения в буфер отправки сокета. Если для всех данных приложения недостаточно места в буфере сокета (либо буфер приложения больше буфера отправки сокета, либо в буфере отправки сокета уже имеются данные), процесс приостанавливается (переходит в состояние ожидания). Подразумевается, что мы используем обычный блокируемый сокет (о неблокируемых сокетах мы поговорим в главе 15). Ядро возвращает управление из функции write
только после того, как последний байт в буфере приложения будет скопирован в буфер отправки сокета. Следовательно, успешное возвращение управления из функции write
в сокет TCP говорит нам лишь о том, что мы можем снова использовать наш буфер приложения. Оно не говорит о том, получил ли собеседник отправленные данные или получило ли их приложение-адресат (более подробно мы рассмотрим это при описании параметра сокета SO_LINGER
в разделе 7.5).
TCP помещает данные в буфер отправки сокета и отправляет их собеседнику TCP, основываясь на всех правилах передачи данных TCP (главы 19 и 20 [111]). Собеседник TCP должен подтвердить данные, и только когда от него придет сегмент ACK, подтверждающий прием данных, наш TCP сможет удалить подтвержденные данные из буфера отправки сокета. TCP должен хранить копию данных, пока их прием не будет подтвержден адресатом.
TCP отправляет данные IP порциями размером MSS или меньше, добавляя свой заголовок TCP к каждому сегменту. Здесь MSS – это значение, анонсированное собеседником, или 536, если собеседник не указал значения для MSS. IP добавляет свой заголовок, ищет в таблице маршрутизации IP-адрес назначения (соответствующая запись в таблице маршрутизации задает исходящий интерфейс, то есть интерфейс для исходящих пакетов) и передает дейтаграмму на соответствующий канальный уровень. IP может выполнить фрагментацию перед передачей дейтаграммы, но, как мы отмечали выше, одна из целей параметра MSS – не допустить фрагментации; а более новые реализации также используют обнаружение транспортной MTU. У каждого канального соединения имеется очередь вывода, и если она заполнена, пакет игнорируется, и вверх по стеку протоколов возвращается ошибка: от канального уровня к IP и затем от IP к TCP. TCP учтет эту ошибку и попытается отправить сегмент позже. Приложение не информируется об этом временном состоянии.
На рис. 2.16 показано, что происходит, когда приложение записывает данные в сокет UDP.
Рис. 2.16. Отправка данных через сокет UDP
На этот раз буфер отправки сокета изображен пунктирными линиями, поскольку он (буфер) на самом деле не существует. У сокета UDP есть размер буфера отправки (который мы можем изменить с помощью параметра сокета SO_SNDBUF
, см. раздел 7.5), но это просто верхнее ограничение на размер дейтаграммы UDP, которая может быть записана в сокет. Если приложение записывает дейтаграмму размером больше буфера отправки сокета, возвращается ошибка EMSGSIZE
. Поскольку протокол UDP не является надежным, ему не нужно хранить копию данных приложения. Ему также не нужно иметь настоящий буфер отправки (данные приложения обычно копируются в буфер ядра по мере их движения вниз по стеку протоколов, но эта копия сбрасывается канальным уровнем после передачи данных).
UDP просто добавляет свой 8-байтовый заголовок и передает дейтаграмму протоколу IP. IPv4 или IPv6 добавляет свой заголовок, определяет исходящий интерфейс, выполняя функцию маршрутизации, и затем либо добавляет дейтаграмму в очередь вывода канального уровня (если размер дейтаграммы не превосходит MTU), либо фрагментирует дейтаграмму и добавляет каждый фрагмент в очередь вывода канального уровня.
Если приложение UDP отправляет большие дейтаграммы (например, 2000-байтовые), существует гораздо большая вероятность фрагментации, чем в случае TCP, поскольку TCP разбивает данные приложения на порции, равные по размеру MSS, а этому параметру нет аналога в UDP.
Успешное возвращение из функции записи в сокет UDP говорит о том, что либо дейтаграмма, либо фрагменты дейтаграммы были добавлены к очереди вывода канального уровня. Если для дейтаграммы или одного из ее фрагментов недостаточно места, приложению в большинстве случаев возвращается сообщение ENOBUFS
.
ПРИМЕЧАНИЕ
К сожалению, некоторые реализации не возвращают этой ошибки, не предоставляя приложению никаких указаний на то, что дейтаграмма была проигнорирована еще до начала передачи.
На рис. 2.17 показан процесс записи данных в сокет SCRIPT.
Рис. 2.17. Отправка данных через сокет SCRIPT
Для обеспечения надежности в SCRIPT предусмотрен буфер отправки. Приложение может менять размер этого буфера при помощи параметра сокета SO_SNDBUF
(см. раздел 7.5), как и при работе с TCP. Когда приложение вызывает функцию write
, ядро копирует все данные из буфера приложения в буфер отправки сокета. Если в буфере сокета недостаточно места для размещения всего объема данных приложения (то есть буфер приложения больше буфера сокета или в последнем уже имелись данные), пользовательский процесс приостанавливается. Приостановка производится для блокируемых сокетов. По умолчанию сокеты SCRIPT являются блокируемыми (о неблокируемых сокетах речь пойдет в главе 16). Ядро не возвращает управление процессу до тех пор, пока все байты буфера приложения не будут скопированы в буфер отправки сокета. Успешное возвращение из вызова write
для сокета SCRIPT означает лишь, что приложение снова может воспользоваться своим буфером. Оно вовсе не означает, что SCRIPT адресата или приложение-адресат получили отправленные данные.
SCRIPT обрабатывает данные, которые находятся в буфере отправки на основании правил передачи SCRIPT (подробнее см. главу 5 [117]). Передающий SCRIPT должен дождаться получения порции SACK, в которой передается кумулятивное уведомление о приеме, чтобы удалить данные из буфера отправки сокета.
2.12. Стандартные службы ИнтернетаВ табл. 2.1 перечислены некоторые стандартные службы, предоставляемые большинством реализаций TCP/IP. Заметьте, что все они поддерживают и TCP, и UDP, и номер порта для обоих протоколов один и тот же.
Таблица 2.1. Стандартные службы TCP/IP, предоставляемые в большинстве реализаций
echo | 7 | 7 | 862 | Сервер возвращает то, что посылает клиент |
discard | 9 | 9 | 863 | Сервер игнорирует все данные, присланные клиентом |
daytime | 13 | 13 | 867 | Сервер возвращает время и дату в формате, удобном для восприятия человеком |
chargen | 19 | 19 | 864 | TCP-сервер посылает непрерывный поток символов, пока соединение не будет разорвано клиентом. UDP-сервер посылает дейтаграмму со случайным количеством символов каждый раз, когда клиент посылает дейтаграмму |
time | 37 | 37 | 868 | Сервер возвращает текущее время в виде двоичного 32-разрядного числа. Это число представляет собой количество секунд, прошедших с полуночи 1 января 1900 года (UTC) |
Часто эти службы предоставляются демоном inetd на узлах Unix (см. раздел 13.5). Стандартные службы делают возможным простейшее тестирование при помощи стандартного клиента Telnet.
Вот, например, тесты для сервера, определяющего время и дату, и для эхо-сервера.
aix % telnet freebsd daytime
Trying 12.106.32.254... вывод клиента Telnet
Connected to freebsd.unpbook.com вывод клиента Telnet
Escape character is '^]'. вывод клиента Telnet
Mon Jul 28 11:56:22 2003 вывод сервера времени и даты
Connection closed by foreign host. вывод клиента Telnet (сервер закрыл
соединение)
aix % telnet freebsd echo
Trying 12.106.32.254... вывод клиента Telnet
Connected to freebsd.unpbook.com вывод клиента Telnet
Escape character is '^]'. вывод клиента Telnet
hello, world ввод с клавиатуры
hello, world эхо-ответ сервера
^] ввод с клавиатуры для обращения к клиенту Telnet
telnet> quit команда клиенту на завершение соединения
Connection closed. на этот раз соединение завершает клиент
В этих двух примерах мы вводим имя узла и название службы ( daytime
и echo
). Соответствие названий служб и номеров портов (см. табл. 2.1) устанавливается в файле /etc/services
(см. раздел 11.5).
Заметьте, что когда мы соединяемся с сервером daytime
, сервер выполняет активное закрытие. В случае эхо-сервера активное закрытие выполняет клиент. Вспомним рис. 2.4, где показано, что узел, выполняющий активное закрытие, – это узел, проходящий состояние TIME_WAIT.
В современных системах стандартные службы чаще всего отключены по умолчанию, потому что через них могут быть проведены атаки типа «отказ в обслуживании» и другие, связанные с чрезмерным потреблением ресурсов.
2.13. Использование протоколов типичными приложениями ИнтернетаТаблица 2.2 иллюстрирует использование протоколов типичными приложениями Интернета.
Таблица 2.2. Использование протоколов типичными приложениями Интернета
ping | • | ||||
traceroute | • | • | |||
OSPF (протокол маршрутизации) | • | ||||
RIP (протокол маршрутизации) | • | ||||
BGP (протокол маршрутизации) | • | ||||
BOOTP (протокол bootstrap – протокол дистанционной загрузки и запуска устройств в сети) | • | ||||
DHCP (протокол bootstrap) | • | ||||
NTP (синхронизирующий сетевой протокол) | • | ||||
TFTP (упрощенный протокол передачи файлов) | • | ||||
SNMP (управление сетью) | • | ||||
SMTP (электронная почта) | • | ||||
Telnet (удаленный вход в систему) | • | ||||
FTP (передача файлов) | • | ||||
HTTP (протокол передачи HTML-файлов по сети WWW) | • | ||||
NNTP (сетевой протокол передачи новостей) | • | ||||
DNS (система доменных имен) | • | • | |||
NFS (сетевая файловая система) | • | • | |||
Sun RPC (удаленный вызов процедур) | • | • | |||
DCE RPC (удаленный вызов процедур) | • | • | |||
IUA (ISDN поверх IP) | • | ||||
M2UA, M3UA (телефонная связь SS7) | • | ||||
H.248 (управление шлюзом) | • | • | • | ||
H.323 (IP-телефония) | • | • | • | ||
SIP (IP-телефония) | • | • | • |
Первые два приложения, ping
и traceroute
, являются диагностическими и используют протокол ICMP, traceroute
создает свои собственные пакеты UDP и считывает ответы ICMP.
Три популярных протокола маршрутизации демонстрируют многообразие транспортных протоколов, которые используются протоколами маршрутизации. Алгоритм OSPF (Open Shortest Path First – первоочередное открытие кратчайших маршрутов) использует IP непосредственно через символьный сокет, в то время как RIP (Routing Information Protocol – протокол информации о маршрутизации) использует UDP, a BGP (Border Gateway Protocol – протокол граничных шлюзов) использует TCP.
Далее идут пять приложений, основанные на UDP, за ними следуют семь приложений TCP и четыре приложения UDP/TCP. Последние пять приложений относятся к IP-телефонии. Они могут использовать либо только SCRIPT, либо UDP, TCP и SCRIPT по выбору.
2.14. РезюмеUDP является простым, ненадежным протоколом, не ориентированным на установление соединения, в то время как TCP – это сложный, надежный, ориентированный на установление соединения протокол. SCRIPT сочетает особенности обоих протоколов, расширяя возможности TCP. Хотя большинство приложений в Интернете используют протокол TCP (веб-сервисы, Telnet, FTP, электронная почта), существует потребность во всех трех транспортных протоколах. В разделе 22.4 мы рассматриваем причины, по которым иногда вместо TCP выбирается UDP. В разделе 23.12 будут проанализированы ситуации, в которых SCRIPT предпочтительнее TCP.
TCP устанавливает соединения, используя трехэтапное рукопожатие, и разрывает соединение, используя обмен четырьмя пакетами. Когда соединение TCP установлено, оно переходит из состояния CLOSED в состояние ESTABLISHED. При разрыве соединения оно переходит обратно в состояние CLOSED. Всего существует 11 состояний, в которых может находиться соединение TCP, и диаграмма переходов состояний определяет правила перемещения между этими состояниями. Понимание этой диаграммы существенно для диагностики проблем при использовании программы netstat
и для понимания того, что происходит, когда мы вызываем такие функции, как connect
, accept
и close
.
Состояние TCP TIME_WAIT – неиссякаемый источник путаницы, возникающей у сетевых программистов. Это состояние существует для того, чтобы реализовать разрыв двустороннего соединения TCP (то есть для решения проблем, возникающих в случае потери последнего сегмента ACK), а также чтобы дождаться, когда истечет время жизни в сети старых дублированных сегментов.
SCRIPT устанавливает ассоциацию, выполняя четырехэтапное рукопожатие, и завершает соединение обменом тремя пакетами. При установлении ассоциации SCRIPT происходит переход из состояния CLOSED в состояние ESTABLISHED, а при завершении ассоциации – возврат к состоянию CLOSED. Ассоциация SCRIPT может находиться в восьми состояниях, правила перехода между которыми описываются диаграммой состояний. Благодаря использованию контрольных меток SCRIPT не нуждается в состоянии TIME_WAIT.
Упражнения1. Мы говорили об IPv4 и IPv6. А что произошло с версией 5 и каковы были версии 0, 1, 2 и 3? ( Подсказка: найдите журнал IANA «Internet Protocol». Можете сразу переходить к решению, если вы не можете подключиться к http://www.iana.org/
.)
2. Где вы будете искать дополнительную информацию о протоколе, которому присвоено название «IP версия 5»?
3. Описывая рис. 2.15, мы отметили, что TCP считает MSS равным 536, если не получает величину параметра MSS от собеседника. Почему используется это значение?
4. Нарисуйте рисунок, аналогичный рис. 2.5, для клиент-серверного приложения времени и даты из главы 1, предполагая, что сервер возвращает 26 байт данных в отдельном сегменте TCP.
5. Допустим, что установлено соединение между узлом в Ethernet, чей TCP объявляет MSS, равный 1460, и узлом в Token-ring, чей TCP объявляет MSS, равный 4096. Ни один из узлов не пытается обнаружить, чему равна транспортная MTU. При просмотре пакетов мы никогда не видим более 1460 байт данных в любом направлении. Почему?
6. Описывая табл. 2.2, мы отметили, что OSPF использует IP непосредственно. Каково значение поля протокола в заголовке IPv4 (см. рис. А.1) для дейтаграмм OSPF?
7. Обсуждая отправку данных по SCRIPT, мы отметили, что отправителю приходится ждать получения кумулятивного уведомления, чтобы удалить данные из буфера сокета. Если еще до получения кумулятивного уведомления принято выборочное уведомление, указывающее, что данные уже доставлены, почему буфер все равно не может быть освобожден?
Часть 2
Элементарные сокеты
Глава 3
Введение в сокеты
3.1. ВведениеЭта глава начинается с описания программного интерфейса приложения (API) сокетов. Мы начнем со структур адресов сокетов, которые будут встречаться почти в каждом примере на протяжении всей книги. Эти структуры можно передавать в двух направлениях: от процесса к ядру и от ядра к процессу. Последний случай – пример аргумента, через который передается возвращаемое значение, и далее в книге мы встретимся с другими примерами таких аргументов.
Перевод текстового представления адреса в двоичное значение, входящее в структуру адреса сокета, осуществляется функциями преобразования адресов. В большей части существующего кода IPv4 используются функции inet_addr
и inet_ntoa
, но две новых функции inet_pton
и inet_ntop
работают и с IPv4, и с IPv6.
Одной из проблем этих функций является то, что они зависят от протокола, так как для них имеет значение тип преобразуемого адреса – IPv4 или IPv6. Мы разработали набор функций, названия которых начинаются с sock_
, работающих со структурами адресов сокетов независимо от протокола. Эти функции мы и будем использовать, чтобы сделать наш код не зависящим от протокола.
Большинство функций сокетов используют в качестве аргумента указатель на структуру адреса сокета. Каждый набор протоколов определяет свою собственную структуру адреса сокетов. Имена этих структур начинаются с sockaddr_
и заканчиваются уникальным суффиксом для каждого набора протоколов.
Структура адреса сокета IPv4, обычно называемая структурой адреса сокета Интернета, именуется sockaddr_in
и определяется в заголовочном файле
. В листинге 3.1 [1]1
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com.
[Закрыть]представлено определение POSIX.
Листинг 3.1. Структура адреса сокета Интернета (IPv4): sockaddr_in
struct in_addr {
in_addr_t s_addr; /* 32-разрядный адрес IPv4 */
/* сетевой порядок байтов */
};
struct sockaddr_in {
uint8_t sin_len; /* длина структуры (16) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-разрядный номер порта TCP или UDP */
/* сетевой порядок байтов */
struct in_addr sin_addr; /* 32-разрядный адрес IPv4 */
/* сетевой порядок байтов */
char sin_zero[8]; /* не используется */
};
Есть несколько моментов, касающихся структур адреса сокета в целом, которые мы покажем на примере.
■ Элемент длины sin_len
появился в версии 4.3BSD-Reno, когда была добавлена поддержка протоколов OSI (см. рис. 1.6). До этой реализации первым элементом был sin_family
, который исторически имел тип unsigned short
(целое без знака). Не все производители поддерживают поле длины для структур адреса сокета, и в POSIX, например, не требуется наличия этого элемента. Типы данных, подобные uint8_t
, введены в POSIX (см. табл. 3.1). Наличие поля длины упрощает обработку структур адреса сокета с переменной длиной.
■ Даже если поле длины присутствует, нам не придется устанавливать и проверять его значение, пока мы не имеем дела с маршрутизирующими сокетами (см. главу 18). Оно используется внутри ядра процедурами, работающими со структурами адресов сокетов из различных семейств протоколов (например, код таблицы маршрутизации).
ПРИМЕЧАНИЕ
Четыре функции, передающие структуру адреса сокета от процесса к ядру, – bind, connect, sendto и sendmsg – используют функцию sockargs в реализациях, ведущих происхождение от Беркли [128, с. 452]. Эта функция копирует структуру адреса сокета из процесса и затем явно присваивает элементу sin_len значение размера структуры, переданной в качестве аргумента этим четырем функциям. Пять функций, передающих структуру адреса сокета от ядра к процессу, – accept, recvfrom, recvmsg, getpeername и getsockname – устанавливают элемент sin_len перед возвращением управления процессу.
К сожалению, обычно не существует простого теста, выполняемого в процессе компиляции и определяющего, задает ли реализация поле длины для своих структур адреса сокета. В нашем коде мы тестируем собственную константу HAVE_SOCKADDR_SA_LEN (см. листинг Г.2), но для того чтобы определить, задавать эту константу или нет, требуется откомпилировать простую тестовую программу, использующую необязательный элемент структуры, и проверить, успешно ли выполнена компиляция. В листинге 3.3 мы увидим, что от реализаций IPv6 требуется задавать SIN6_LEN, если структура адреса сокета имеет поле длины. В некоторых реализациях IPv4 (например, Digital Unix) поле длины предоставляется для приложений, основанных на параметре времени компиляции (например, _SOCKADDR_LEN). Это свойство обеспечивает совместимость с другими, более ранними программами.
■ POSIX требует наличия только трех элементов структуры: sin_family
, sin_addr
и sin_port
. POSIX-совместимая реализация может определять дополнительные элементы структуры, и это норма для структуры адреса сокета Интернета. Почти все реализации добавляют элемент sin_zero, так что все структуры адреса сокета имеют размер как минимум 16 байт.
■ Типы элементов s_addr
, sin_family
и sin_port
мы указываем согласно POSIX. Тип данных in_addr_t
соответствует целому числу без знака длиной как минимум 32 бита, in_port_t
– целому числу без знака длиной как минимум 16 бит, a sa_family_t
– это произвольное целое число без знака. Последнее обычно представляет собой 8-разрядное целое без знака, если реализация поддерживает поле длины, либо 16-разрядное целое без знака, если поле длины не поддерживается. В табл. 3.1 перечислены эти три типа данных POSIX вместе с некоторыми другими типами данных POSIX, с которыми мы встретимся.
Таблица 3.1. Типы данных, требуемые POSIX
int8_t | 8-разрядное целое со знаком | |
uint8_t | 8-разрядное целое без знака | |
int16_t | 16-разрядное целое со знаком | |
uint16_t | 16-разрядное целое без знака | |
int32_t | 32-разрядное целое со знаком | |
uint32_t | 32-разрядное целое без знака | |
sa_family_t | семейство адресов структуры адреса сокета | |
socklen_t | длина структуры адреса сокета, обычно типа uint32_t | |
in_addr_t | IPv4-адрес, обычно типа uint32_t | |
in_port_t | порт TCP или UDP, обычно типа uint16_t |
■ Вы также встретите типы данных u_char
, u_short
, u_int
и u_long
, которые не имеют знака. POSIX определяет их с замечанием, что они устарели. Они предоставляются в целях обратной совместимости.
■ И адрес IPv4, и номер порта TCP и UDP всегда хранятся в структуре в соответствии с порядком байтов, определенным в сети ( сетевой порядок байтов– network byte order). Об этом нужно помнить при использовании этих элементов (более подробно о разнице между порядком байтов узла и порядком байтов в сети мы поговорим в разделе 3.4).
■ К 32-разрядному адресу IPv4 можно обратиться двумя путями. Например, если serv
– это структура адреса сокета Интернета, то serv.sin_addr
указывает на 32-разрядный адрес IPv4 как на структуру in_addr
, в то время как serv.sin_addr.s_addr
указывает на тот же 32-разрядный адрес IPv4 как на значение типа in_addr_t
(обычно это 32-разрядное целое число без знака). Нужно следить за корректностью обращения к адресам IPv4, особенно при использовании их в качестве аргументов различных функций, потому что компиляторы часто передают структуры не так, как целочисленные переменные.
ПРИМЕЧАНИЕ
Причина того, что sin_addr является структурой, а не просто целым числом без знака, носит исторический характер. В более ранних реализациях (например, 4.2BSD) структура in_addr определялась как объединение (union) различных структур, чтобы сделать возможным доступ к каждому из четырех байтов 32-разрядного IPv4-адреса, а также к обоим входящим в него 16-разрядным значениям. Эта возможность использовалась в адресах классов А, В и С для выборки соответствующих байтов адреса. Но с появлением подсетей и последующим исчезновением различных классов адресов (см. раздел А.4) и введением бесклассовой адресации (classless addressing) необходимость в объединении структур отпала. В настоящее время большинство систем отказались от использования объединения и просто определяют in_addr как структуру, содержащую один элемент типа in_addr_t.
■ Элемент sin_zero
не используется, но мы всегдаустанавливаем его в нуль при заполнении одной из этих структур. Перед заполнением структуры мы всегда обнуляем все ее элементы, а не только sin_zero
.
ПРИМЕЧАНИЕ
В большинстве случаев при использовании этой структуры не требуется, чтобы элемент sin_zero был равен нулю, но, например, при привязке конкретного адреса IPv4 (а не произвольного интерфейса) этот элемент обязательно должен быть нулевым [128, с. 731-732].
■ Структуры адреса сокета используются только на данном узле: сама структура не передается между узлами, хотя определенные поля (например, поля IP-адреса и порта) используются для соединения.