Обратите внимание, что используются те же четыре функции ввода-вывода, что и в листинге 5.4:
fgets
,
writen
,
readline
и
fputs
, но порядок их следования внутри функции
str_cli
изменился. Раньше выполнение функции
str_cli
определялось функцией
fgets
, а теперь ее место заняла
select
. С помощью всего нескольких дополнительных строк кода (сравните листинги 6.1 и 5.4) мы значительно увеличили устойчивость клиента.
6.5. Пакетный ввод
К сожалению, наша функция
str_cli
все еще не вполне корректна. Сначала вернемся к ее исходной версии, приведенной в листинге 5.4. Эта функция работает в режиме остановки и ожидания (stop-and-wait mode), что удобно для интерактивного использования: функция отправляет строку серверу и затем ждет его ответа. Время ожидания складывается из одного периода обращения (RTT) и времени обработки сервером (которое близко к нулю в случае простого эхо-сервера). Следовательно, мы можем предположить, сколько времени займет отражение данного числа строк, если мы знаем время обращения (RTT) между клиентом и сервером.
Измерить RTT позволяет утилита
ping
. Если мы измерим с ее помощью время обращения к
connix.com
с нашего узла
solaris
, то средний период RTT после 30 измерений будет равен 175 мс. В [111, с. 89] показано, что это справедливо для дейтаграммы IP длиной 84 байт. Если мы возьмем первые 2000 строк файла
termcap
Solaris 2.5, то итоговый размер файла будет равен 98 349 байт, то есть в среднем 49 байт на строку. Если мы добавим размеры заголовка IP (20 байт) и заголовка TCP (20 байт), то средний сегмент TCP будет составлять 89 байт, почти как размер пакета утилиты
ping
. Следовательно, мы можем предположить, что общее время составит около 350 с для 2000 строк (2000x0,175 с). Если мы запустим наш эхо-клиент TCP из главы 5, действительное время получится около 354 с, что очень близко к нашей оценке.
Если считать, что сеть между клиентом и сервером является двусторонним каналом, когда запросы идут от клиента серверу, а ответы в обратном направлении, то получится изображенный на рис. 6.8 режим остановки и ожидания.
Рис. 6.8. Временная диаграмма режима остановки и ожидания: интерактивный ввод
Запрос отправляется клиентом в нулевой момент времени, и мы предполагаем, что время обращения RTT равно 8 условным единицам. Ответ, отправленный в момент времени 4, доходит до клиента в момент времени 7. Мы также считаем, что время обработки сервером нулевое и что размер запроса равен размеру ответа. Мы показываем только пакеты данных между клиентом и сервером, игнорируя подтверждения TCP, которые также передаются по сети.
Но поскольку между отправкой пакета и его приходом на другой конец канала существует задержка и канал является двусторонним, в этом примере мы используем только восьмую часть вместимости канала. Режим остановки и ожидания удобен для интерактивного ввода, но поскольку наш клиент считывает данные из стандартного потока ввода и записывает в стандартный поток вывода, а перенаправление ввода и вывода выполнить в интерпретаторе команд крайне просто, мы легко можем запустить наш клиент в пакетном режиме. Однако когда мы перенаправляем ввод и вывод, получающийся файл вывода всегда меньше файла ввода (а для эхо-сервера требуется их идентичность).
Чтобы понять происходящее, обратите внимание, что в пакетном режиме мы отправляем запросы так быстро, как их может принять сеть. Сервер обрабатывает их и отправляет обратно ответы с той же скоростью. Это приводит к тому, что в момент времени 7 канал целиком заполнен, как показано на рис. 6.9.
Рис. 6.9. Заполнение канала между клиентом и сервером: пакетный режим
Предполагается, что после отправки первого запроса мы немедленно посылаем другой запрос и т.д. Также предполагается, что мы можем отправлять запросы с той скоростью, с какой сеть способна их принимать, и обрабатывать ответы так быстро, как сеть их поставляет.
Существуют различные нюансы, имеющие отношение к передаче большого количества данных TCP (bulk data flow), которые мы здесь игнорируем. К ним относятся алгоритм медленного запуска (slow start algorithm), ограничивающий скорость, с которой данные отправляются на новое или незанятое соединение, и возвращаемые сегменты ACK. Все эти вопросы рассматриваются в главе 20 [111].