Системные вызовы getsockname() и getpeername() извлекают локальный адрес, к которому привязан сокет, и, соответственно, адрес удаленной стороны, к которой подключен этот сокет.
Мы рассмотрели ряд подробностей работы протокола TCP, включая его состояния и диаграмму их переходов, а также процедуры установки и разрыва соединений. Одновременно было показано, почему состояние TIME_WAIT играет важную роль в обеспечении надежности протокола TCP. И хотя в случае перезапуска сервера TIME_WAIT может привести к ошибке «этот адрес уже занят», мы объяснили, как избежать такой ситуации с помощью параметра сокета SO_REUSEADDR и в то же время позволить данному состоянию выполнять свою функцию.
Команды netstat и tcpdump могут пригодиться для мониторинга и отладки приложений, которые используют сокеты.
Системные вызовы getsockopt() и setsockopt() позволяют извлекать и изменять параметры, влияющие на работу сокетов.
В Linux новый сокет, создаваемый с помощью вызова accept(), не наследует флаги состояния открытого файла слушающего сокета, а также флаги и атрибуты файлового дескриптора, связанные с вводом/выводом, основанным на сигналах. Хотя параметры сокета все же наследуются. Мы отметили, что стандарт SUSv3 никак не оговаривает это поведение, вследствие чего оно может варьироваться в зависимости от реализации.
В отличие от TCP, протокол UDP не обладает механизмами обеспечения надежности, но, как мы могли убедиться, имеет ряд достоинств, которые делают его более подходящим для отдельных приложений.
В завершение мы кратко рассмотрели несколько продвинутых возможностей сокетов.
Ознакомьтесь с источниками, приведенными в разделе 55.14.
57.1. Представьте, что программа из листинга 57.2 (is_echo_cl.c) была модифицирована и теперь вместо применения вызова fork() для создания потомков, работающих параллельно, используется всего один процесс, который сначала копирует свой стандартный ввод в сокет, а затем считывает ответ сервера. С какой проблемой можно столкнуться при работе с этим клиентом (см. рис. 54.8)?
57.2. Реализуйте вызов pipe() по примеру socketpair(). Задействуйте вызов shutdown(), чтобы сделать итоговый канал однонаправленным.
57.3. Реализуйте замену sendfile() с применением вызовов read(), write() и lseek().
57.4. Напишите программу на основе вызова getsockname(), демонстрирующую, что сокет, для которого операция listen() выполняется без предварительного вызова bind(), привязывается к динамическому порту.
57.5. Напишите клиентскую и серверную программы, позволяющие выполнять произвольные консольные команды на удаленном компьютере. (Если вы не собираетесь реализовывать в этом приложении никаких механизмов безопасности, то следует сделать так, чтобы сервер работал от имени обычной учетной записи и не мог причинить существенного вреда в случае использования злоумышленниками.) Клиент должен запускаться с помощью двух аргументов командной строки:
$./is_shell_cl server-host 'some-shell-command'
Подключившись к серверу, клиент отправляет ему заданную команду, после чего закрывает свой записывающий канал сокета путем вызова shutdown(), чтобы на другом конце можно было увидеть конец файла. Серверу следует обрабатывать каждое входящее соединение с помощью отдельного дочернего процесса (то есть параллельно). Каждый потомок должен прочитать данные из своего сокета (пока не столкнется с завершением файла) и затем запустить командную оболочку для выполнения соответствующей команды. Несколько подсказок:
• за пример запуска консольных команд возьмите реализацию вызова system() из раздела 27.7;
• используйте вызов dup2(), чтобы продублировать дескриптор сокета для стандартных потоков stdout и stderr — благодаря этому запущенная команда автоматически будет записывать свой вывод в сокет.
57.6. В подразделе 57.13.1 отмечалось: в качестве альтернативы внеканальным данным между клиентом и сервером можно было бы создать два TCP-соединения: одно — для обмена обычной информацией, а другое — для высокоприоритетных данных. Напишите клиентскую и серверную программы, которые реализуют этот принцип. Вот несколько подсказок.
• Сервер должен каким-то образом знать, какие два сокета принадлежат клиенту. Этого можно добиться, создав на клиентской стороне слушающий сокет и привязав его к динамическому порту (с номером 0). Получив номер своего динамического порта (с помощью вызова getsockname()), клиент соединяет другой свой сокет со слушающим сокетом сервера и отправляет сообщение, содержащее номер динамического порта клиента. Затем клиент ждет, чтобы сервер мог подключиться к его слушающему сокету и установить соединение для «приоритетных» данных, направленное в обратную сторону (сервер может получить IP-адреса клиента во время выполнения вызова accept() для обычного соединения).