глава 19 интерактивный поток данных tcp в предыдущей главе мы рассмотрели, как устанавливаются и разрываются tcp соединения. теперь посмотрим, как с использованием tcp передаются данные. если проанализировать tcp траффик, как, например, это сделано в [caceres et al. 1991], можно обнаружить, что примерно половина всех tcp сегментов составляет неинтерактивные данные (ftp, электронная почта, новости usenet), а другая половина принадлежит интерактивным или диалоговым приложениям (telnet и rlogin, например). по количеству байт, примерно 90% составляют неинтерактивные данные, а 10% диалоговые данные. cегменты, содержащие неинтерактивные данные, как правило, полного размера (512 байт пользовательских данных), тогда как интерактивные пакеты меньше. (также было обнаружено, что 90% пакетов, используемых telnet и rlogin, содержат меньше чем 10 байт пользовательских данных.) tcp способен обработать оба типа данных, однако при передаче разных типов данных используются разные алгоритмы. в этой главе мы рассмотрим передачу интерактивных (диалоговых) данных с использованием rlogin. мы посмотрим, как работают задержанные подтверждения и как алгоритм нагла (nagle) уменьшает количество маленьких пакетов, проходящих по глобальным сетям. тот же алгоритм применяется и в telnet. в следующей главе рассматривается передача неинтерактивных данных. давайте посмотрим, как осуществляется передача данных, при вводе интерактивной команды при rlogin соединении. большинство новичков в tcp/ip очень удивляются, когда обнаруживают, что каждое нажатие клавиши генерирует пакет данных. другими словами, при нажатии клавиши от клиента серверу посылается 1 байт за один промежуток времени (а не строка за один раз). более того, сервер rlogin отражает эхом символы, которые введены клиентом. при этом генерируется 4 сегмента: (1) интерактивный ввод символа от клиента, (2) подтверждение получение символа от сервера, (3) эхо введенного символа от сервера и (4) подтверждение на эхо от клиента. на рисунке 19.1 показан обмен данными. рисунок 19.1 один из возможных способов осуществить удаленное эхо введенного символа.
обычно сегменты 2 и 3 объединяются - подтверждение введенного символа отправляется вместе с эхо. мы опишем технику подобного комбинирования (которое называется задержанным подтверждением) в следующем разделе. rlogin не случайно выбран для иллюстрации примеров. это приложение обычно посылает от клиента серверу по одному символу. при рассмотрении telnet (глава 26), мы увидим, что там предусмотрена опция, которая позволяет отправить строку ввода от клиента серверу, что уменьшает загрузку сети. на рисунке 19.2 показан поток данных, соответствующий вводу пяти символов date\n. (мы не показали процесс установления соединения и удалили весь вывод, посвященный типу сервиса. bsd/386 устанавливает tos для rlogin соединения таким образом, минимизировать задержку.) в строке 1 от клиента серверу отправляется 1 символ (d). в строке 2 приходит подтверждение на этот символ и его эхо. (два средних сегмента на рисунке 19.1 объединены.) в строке 3 подтверждается символ, отраженный эхом. строки 4-6 соответствуют символу a, строки 7-9 соответствуют символу t, а строки 10-12 символу e. секунды задержки между строками 3-4, 6-7, 9-10 и 12-13 вызваны "человеческим фактором" (скорость ввода символов пользователем). обратите внимание на отличие в строках 13-15. в данном случае от клиента серверу посылается один символ (unix символ новой строки, который генерируется при нажатии клавиши return), однако эхом возвращается два символа. эти символы возврата каретки и пропуск строки (cr/lf), при этом курсор перемещается в крайне левую позицию следующей строки. в строке 16 показан вывод команды date, запущенной на сервере. 30 байт соответствуют следующим 28 символам sat feb 6 07:52:17 mst 1993
рисунок 19.2 сегменты, генерируемые при вводе date в rlogin соединение.
следующие 7 байт, отправленные от сервера клиенту (строка 18), это приглашение, которое выглядит следующим образом: svr4 % . в строке 19 выдается подтверждение на эти 7 байт. обратите внимание на то, как в tcp генерируются подтверждения. в строке 1 отправляется байт данных с номером последовательности 0. в строке 2 приходит подтверждение с номером последовательности 1, то есть номер последовательности последнего успешно принятого байта плюс 1. (это обычно называется номером последовательности следующего ожидаемого байта.) в строке 2 от сервера клиенту посылается байт данных с номером последовательности равным 1. он подтверждается (ack) клиентом в строке 3 путем отправки подтверждения с номером последовательности равным 2. на рисунке 19.2 необходимо обратить внимание на времена, которые мы рассмотрим в этом разделе. на рисунке 19.3 показана временная диаграмма обмена, приведенного на рисунке 19.2. (мы удалили из временной диаграммы все объявления окна и добавили выражения, которые указывают на то, какие данные передаются.) семь подтверждений ack, отправленные от bsdi к svr4, помечены как задержанные ack (delayed ack). обычно tcp не отправляет ack сразу по приему данных. вместо этого он осуществляет задержку подтверждений в надежде на то, что в этом же направлении будут отправлены данные, таким образом ack может быть отправлено вместе с данными. большинство реализаций используют задержку равную 200 миллисекунд - таким образом, tcp задерживает ack на время до 200 миллисекунд, чтобы посмотреть, не направляются ли данные в том же направлении, что и ack. рисунок 19.3 временная диаграмма потока данных для команды date, введенной в соединение rlogin.
если рассмотреть разницу во времени между приемом данных bsdi и отправкой ack, то она будет выглядеть случайной: 123,5; 65,6; 109,0; 132,3; 42,0; 140,3 и 195,8 миллисекунды. давайте посмотрим на реальные времена (начинающиеся с 0), когда отправляются ack: 139,9; 539,9; 940,1; 1339,9; 1739,9; 1940,1 и 2140,1 миллисекунды. (мы пометили эти значения звездочкой слева на рисунке 19.3.) разница между этими временами составляет несколько периодов по 200 миллисекунд. это происходит из-за того, что tcp имеет таймер, который выключается каждые 200 миллисекунд, однако он выключается в фиксированные моменты времени - каждые 200 миллисекунд с того момента, когда ядро было загружено. так как данные, которые должны быть подтверждены, приходят со случайными задержками (во времена 16,4; 474,3; 831,1 и так далее), tcp должен быть уведомлен о том, когда истекает следующий 200-миллисекундный таймер ядра. это может произойти в любой момент от 1 до 200 миллисекунд в будущем. если мы посмотрим, сколько времени требуется svr4 на то, чтобы сгенерировать эхо для каждого полученного символа, времена будут следующими: 16,5; 16,3; 16,5; 16,4 и 17,3 миллисекунды. так как это время меньше чем 200 миллисекунд, мы никогда не увидим задержанный ack на этой стороне. всегда существуют данные готовые к отправке, перед тем как истечет таймер задержанного ack. (конечно, мы можем увидеть задержанный ack, если период ожидания, около 16 миллисекунд, совпадет с одним из таймеров в 200 миллисекунд. однако, этого не произошло в примере.) мы видели тот же самый сценарий на рисунке 18.7, в случае с 500-миллисекундным таймером tcp, используемым для определения тайм-аута. оба tcp таймера, и 200-миллисекундный и 500-миллисекундный, начинают отсчет с того момента, когда ядро было загружено. когда tcp устанавливает таймер, он может выключиться в любой момент между 1-200 или 1-500 миллисекунд в будущем. требования к хостам host requirements rfc указывают на то, что tcp должен применять задержанные ack, однако задержка должна быть меньше чем 500 миллисекунд. алгоритм нагла в предыдущем разделе мы видели, что обычно от клиента к серверу через rlogin соединение передается 1 байт за один раз. при этом генерируются пакеты размером 41 байт: 20 байт - ip заголовок, 20 байт - tcp заголовок и 1 байт данных. маленькие пакеты (называемые тиниграммами, от английского tiny - крошечный, маленький) - обычно не проблема для локальных сетей, так как большинство локальных сетей не перегружаются, однако они могут привести к перегрузке глобальной сети. простое и элегантное решение было предложено в rfc 896 [nagle 1984], которое сейчас называется алгоритмом нагла (nagle algorithm). из алгоритма следует, что в tcp соединении может присутствовать только один исходящий маленький сегмент, который еще не был подтвержден. следующие маленькие сегменты могут быть посланы только после того, как было получено подтверждение. вместо того чтобы отправляться последовательно, маленькие порции данных накапливаются и отправляются одним tcp сегментом, когда прибывает подтверждение на первый пакет. красота этого алгоритма заключается в том, что он сам настраивает временные характеристики: чем быстрее придет подтверждение, тем быстрее будут отправлены данные. в медленных глобальных сетях, где необходимо уменьшить количество маленьких пакетов, отправляется меньше сегментов. (в разделе "синдром "глупого" окна" главы 22 мы увидим, что определение "маленький" означает - меньше чем размер сегмента.) на рисунке 19.3 мы видели, что для ethernet время возврата на один отправленый байт, на который приходит подтверждение и эхо, составляет примерно 16 миллисекунд. чтобы данные генерировались быстрее, мы должны печатать больше чем 60 символов в секунду. это означает, что вряд ли можно использовать этот алгоритм при отправке данных между двумя хостами, находящимися в локальной сети. однако, положение меняется, когда время возврата (rtt) увеличивается, обычно это происходит в глобальных сетях. давайте рассмотрим rlogin соединение между хостом slip и хостом vangogh.cs.berkeley.edu. чтобы выйти из нашей сети (рисунок 1.11), необходимо пройти два slip канала, а потом попасть в internet. в этом случае мы ожидаем довольно большое время возврата. на рисунке 19.4 показана временная диаграмма потока данных, которые соответствуют быстрому набору символов клиентом (предположим, что за клавиатурой хорошая стенографистка). (мы удалили всю информацию, соответствующую типу сервиса, однако оставили объявления размера окна.) рисунок 19.4 поток данных с использованием rlogin между slip и vangogh.cs.berkeley.edu.
первое, на что необходимо обратить внимание, на рисунках 19.3 и 19.4, то, что нет задержанных ack от slip к vangogh. это происходит потому, что в данном случае данные всегда готовы к отправке, перед тем как истечет таймер задержанного ack. необходимо обратить внимание на различные размеры данных, которые отправляются слева направо: 1, 1, 2, 1, 2, 2, 3, 1 и 3 байта. это объясняется тем что, что клиент собирает данные, которые необходимо послать, однако не посылает их, пока на ранее отправленные данные не приходит подтверждение. с использованием алгоритма нагла с помощью девяти сегментов (а не 16) было отправлено 16 байт. обработка сегментов 14 и 15 противоречит алгоритму нагла, необходимо посмотреть на номера последовательности, чтобы определить, что произошло в действительности. сегмент 14 это отклик на ack, полученный в сегменте 12, так как подтвержденный номер последовательности равен 54. однако перед тем как этот сегмент данных отправлен клиентом, от сервера прибывает сегмент 13. сегмент 15 содержит подтверждение (ack) на сегмент 13, номер последовательности 56. таким образом, клиент игнорировал алгоритм нагла, так как мы видим два последовательных сегмента данных, посланных от клиента к серверу. также на рисунке 19.4 необходимо обратить внимание на то, что существует один задержанный ack, передаваемый однако от сервера к клиенту (сегмент 12). мы предположили, что это задержанный ack, потому что он не содержит данных. сервер скорее всего был занят в этот момент времени, поэтому сервер rlogin не мог отправить эхо символ, перед тем как истек таймер задержанного ack. и в заключение, обратимся к размеру данных и номерам последовательности в последних двух сегментах. клиент посылает 3 байта данных (с номерами 18, 19 и 20), затем сервер подтверждает эти 3 байта (ack 21 в последнем сегменте), однако посылает назад только один байт (с номером 59). здесь происходит следующее: tcp сервер подтверждает 3 байта данных за один раз, однако эхо этих 3-х байт не будет готово к отправке назад, пока их не сгенерирует сервер rlogin. это показывает на то, что tcp может подтвердить принятые данные, до того как приложение считало и обработало эти данные. tcp подтверждение просто означает, что tcp корректно получил данные. определить, что процесс сервера не считал эти 3 байта данных можно благодаря тому, что объявленный размер окна в последнем сегменте равен 8189, а не 8192. существуют моменты, когда необходимо отключить алгоритм нагла. классический пример - сервер x window system (глава 30, раздел "x window system"): маленькие пакеты (соответствующие передвижению мыши) должны быть переданы без задержки, что обеспечивает отклик в реальном времени для пользователя. однако здесь мы продемонстрируем другой пример, который позволяет представить необходимость выключения алгоритма - ввод одной из специальных клавиш терминала во время интерактивного терминального захода. функциональные клавиши обычно генерируют несколько байт данных, которые часто начинаются с escape ascii символа. tcp клиент получит 1 байт данных (от приложения) за один момент времени, пошлет этот первый байт (ascii esc), а оставшиеся байты задержит до того момента, пока не придет подтверждение на первый отправленный байт. однако, когда сервер получает этот первый байт, он не генерирует эхо пока не будут получены все оставшиеся байты. в этом случае, обычно, включается алгоритм задержанного ack на сервере, а это означает, что оставшиеся байты не будут отправлены в течении ближайших 200 миллисекунд. в результате пользователь получит заметную задержку вывода. сокеты api используют опцию сокета tcp_nodelay, чтобы выключить алгоритм нагла. требования к хостам host requirementts rfc указывают, что tcp должен применять алгоритм нагла, однако должен существовать способ для приложения выключить его для определенного соединения.
пример мы можем пронаблюдать функционирование алгоритма нагла при нажатии клавиши, которая генерирует несколько байт. было установлено rlogin соединение от хоста slip к хосту vangogh.cs.berkeley.edu. затем мы нажали функциональную клавишу f1, которая генерирует 3 байта: escape, левую квадратную скобку и m. затем мы ввели функциональную клавишу f2, которая генерирует еще 3 байта. на рисунке 19.5 показан вывод tcpdump. (информация, соответствующая типу сервиса и определению окна, удалена.)
рисунок 19.5 работа алгоритма нагла при вводе символов, которые генерируют несколько байт данных.
на рисунке 19.6 показана временная диаграмма соответствующая этому обмену. внизу этого рисунка показано 6 байт, переданные от клиента к серверу с их номерами последовательности, и 8 байт, которые были возвращены эхом. когда первый байт ввода был прочитан клиентом rlogin и записан в tcp, он отправляется в сегменте 1. это первый из 3 байт, сгенерированных при нажатии клавиши f1. его эхо возвращено в сегменте 2, и только после этого отправляются следующие 2 байта (сегмент 3). эхо для вторых 2 байт получено в сегменте 4, а подтверждение в сегменте 5. причина того, что эхо для первого байта заняло 2 байта (сегмент 2), заключается в том, что ascii escape символ отражается эхом как 2 байта: символ "^" и левая квадратная скобка. следующие 2 введенных байта, левая скобка и м, отражаются эхом как они есть. тот же самый обмен происходит, когда нажата следующая функциональная клавиша (сегменты 6-10). как мы и ожидали, разница во времени между сегментами 5 и 10 (slip посылает подтверждение на эхо) это несколько периодов времени по 200 миллисекунд, так как оба подтверждения (ack) задержаны. рисунок 19.6 временная диаграмма для рисунка 19.5 (наблюдение за работой алгоритма нагла).
сейчас мы повторим тот же самый пример с использованием версии rlogin, которая модифицирована таким образом, что способна выключать алгоритм нагла. на рисунке 19.7 показан вывод команды tcpdump. (информация, посвященная типу сервиса и объявлению окна, удалена.)
рисунок 19.7 отключение алгоритма нагла в течении rlogin сессии.
будет более поучительно и информативно получить этот вывод и построить временную диаграмму, зная какие пакеты прошли по сети. также этот пример требует внимательного изучения номеров последовательности, которые соответствуют текущим данным. это показано на рисунке 19.8. нумерация сегментов соответствует нумерации в выводе tcpdump, приведенном на рисунке 19.7. первое отличие, которое бросается в глаза, заключается в том, что все 3 байта отправляются, когда они готовы (сегменты 1, 2 и 3). задержка не осуществляется - алгоритм нагла выключен. следующий пакет, который мы видим в выводе tcpdump (сегмент 4), содержит 5 байт от сервера с ack 4. здесь произошла ошибка. клиент немедленно отвечает с ack 2 (незадержанный), а не ack 6, так как он не ожидал прихода байта номер 5. это означает, что сегмент данных был потерян. это показано пунктирными линиями на рисунке 19.8. как мы узнали, что этот потерянный сегмент содержит байты 2, 3 и 4 вместе с ack 3? следующий байт, который мы ожидаем, это байт номер 2, как объявлено в сегменте номер 5. (когда tcp получает данные в неправильном порядке, то есть с номерами последовательности, следующими за теми, которые мы ожидаем, он обычно отвечает подтверждением, содержащим номер последовательности следующего байта, который он ожидает получить.) так как отсутствующий сегмент содержал байты 2, 3 и 4, это означает, что сервер должен получить сегмент 2, таким образом, отсутствующий сегмент должен быть указан как ack 3 (номер последовательности следующего байта, который ожидается сервером к приему). и в завершение, обратите внимание на то, что при повторной передаче, сегмент 6, содержит данные из отсутствующего сегмента и сегмента 4. это называется пересборкой пакетов (repacketization) , что мы обсудим более подробно в разделе "пересборка пакетов" главы 21. возвращаясь к обсуждению выключения алгоритма нагла, мы можем увидеть, что 3 байта, соответствующие следующей специальной клавише, которую мы нажали, отправляются как три отдельных сегмента (8, 9 и 10). теперь сервер отражает эхом байт в сегменте 8 - первым (сегмент 11), а затем отражает эхом байты из сегментов 9 и 10 (сегмент 12). в этом примере мы увидели, что использование по умолчанию алгоритма нагла может вызвать дополнительные задержки при нажатии нескольких клавиш в процессе работы интерактивных приложений по глобальным сетям. мы вернемся к этой теме в главе 21, которая посвящена тайм-аутам и повторным передачам. рисунок 19.8 временная диаграмма для рисунка 19.7 (выключение алгоритма нагла).
на рисунке 19.4 мы видели, что slip объявляет окно размером 4096 байт, а vangogh объявляет окно 8192 байта. большинство сегментов на этом рисунке содержат одно из этих двух значений. сегмент 5, однако, объявляет окно размером 4095 байт. это означает, что в tcp буфере все еще находится 1 байт, не считанный приложением (клиент rlogin). следующий сегмент от клиента объявляет окно равное 4094 байта, а это означает, что 2 байта все еще должны быть считаны приложением. сервер обычно объявляет окно равное 8192 байтам, потому что tcp серверу нечего послать до тех пор, пока сервер rlogin читает и принимает данные, а затем отражает их эхом. данные от сервера посылаются после того, как сервер rlogin считал ввод от клиента. tcp клиент, с другой стороны, часто имеет данные, которые необходимо послать, когда прибудет подтверждение, поэтому он помещает символы в буфер, ожидая прихода ack. когда tcp клиент посылает буферизированные данные, rlogin клиент не имеет возможности прочитать данные, полученные от сервера, поэтому клиент объявляет окно меньше чем 4096. диалоговые данные обычно передаются в сегментах с размером меньшим, чем максимальный размер сегмента. в случае rlogin от клиента к серверу обычно передается один байт данных. telnet позволяет посылать за один раз строку, однако большинство реализаций на сегодняшний день до сих пор посылают по одному символу. задержанное подтверждение используется принимающей стороной этих маленьких пакетов, для того чтобы послать подтверждение вместе с данными, которые возвращаются к отправителю. это, как правило, уменьшает количество сегментов, особенно в случае сессии rlogin, где сервер отражает эхом все символы, напечатанные клиентом. при работе в медленных глобальных сетях часто используется алгоритм нагла, что позволяет уменьшить количество маленьких сегментов. в случае использования алгоритма нагла отправляется только один маленький неподтвержденный пакет в один момент времени. однако существуют моменты, когда алгоритм нагла должен быть отключен, как мы показали в примерах.
|