глава 20 поток неинтерактивных данных tcp в главе 15 мы видели, что tftp использует протокол с ожиданием подтверждения (stop-and-wait). отправитель блока данных требует подтверждения на этот блок, перед тем как будет отправлен следующий блок. в этой главе мы увидим, что tcp использует другую форму управления потоком данных, которая называется протоколом с изменяющимся окном (sliding window) . это позволяет отправителю передать несколько пакетов, перед тем как он остановится и будет ждать подтверждения. при этом данные передаются быстрее, так как отправитель не должен останавливаться и ждать подтверждения каждый раз после отправки пакета. также мы увидим tcp флаг push, который мы уже встречали в предыдущих примерах. рассмотрим медленный старт, технику, используемую tcp для получения потока данных при установлении соединения, а затем рассмотрим пропускную способность при передаче неинтерактивных данных. давайте начнем с рассмотрения односторонней передачи 8192 байт от хоста svr4 к хосту bsdi. мы запустили нашу программу sock (как сервер) на bsdi: bsdi % sock -i -s 7777
флаги -i и -s сообщают о необходимости запустить программу в качестве сервера, не обрабатывающего данные (при этом данные читаются из сети и отбрасываются), номер порта сервера установлен в 7777. клиент, соответственно, запускается следующим образом: svr4 % sock -i -n8 bsdi 7777
клиент осуществляет в сеть восемь записей размером 1024 байта каждая. на рисунке 20.1 показана временная диаграмма этого обмена. мы оставили первые 3 сегмента вывода, чтобы показать значение mss (максимальный размер сегмента) для каждой стороны. рисунок 20.1 передача 8192 байт от svr4 к bsdi.
во-первых, отправитель передает три сегмента данных (4-6). следующий сегмент (7) подтверждает только первые два сегмента данных. мы знаем об этом, потому что номер последовательности подтверждения равен 2049, а не 3073. сегмент 7 содержит ack с номером 2049, а не 3073 по следующей причине. когда пакет прибывает, он первоначально обрабатывается драйвером устройства, а затем помещается во входную очередь ip. три сегмента 4, 5 и 6 прибывают один за другим и помещаются во входную очередь ip в том порядке, как они были приняты. ip затем передаст их в tcp в том же самом порядке. когда tcp обрабатывает сегмент 4, в соединении генерируется задержанный ack. tcp обрабатывает следующий сегмент (5), и теперь tcp имеет два сегмента, на которые необходимо сгенерировать подтверждение (ack), поэтому генерируется подтверждение с номером 2049 (сегмент 7), а флаг задержанного ack для этого соединения снимается. tcp обрабатывает следующий входной сегмент (6), а в соединение снова генерируется задержанный ack. перед тем как прибывает сегмент 9, выключается таймер задержанного ack и генерируется подтверждение с номером 3073 (сегмент 8). в сегменте 8 окно объявляется размером 3072 байта, так как 1024 байта данных в приемном буфере tcp до сих пор не прочитаны приложением. в случае сегментов 11-16 подтверждение осуществляется на каждый сегмент. сегменты 11, 12 и 13 прибывают и помещаются во входную очередь ip. когда сегмент 11 обрабатывается tcp, соединение помечается как использующее задержанное ack. когда обрабатывается сегмент 12, генерируется ack (сегмент 14) на сегменты 11 и 12, а флаг задержанного ack для данного соединения снимается. при обработке сегмента 13 соединение вновь помечается как использующее задержанное ack, однако перед тем как задержанный ack снимается по таймеру, обрабатывается сегмент 15, при этом ack (сегмент 16) отправляется немедленно. очень важно обратить внимание на то, что ack в сегментах 7, 14 и 16 подтверждает два принятых сегмента. в случае использования протокола tcp с изменяющимся окном, принимающая сторона не должна подтверждать каждый принятый пакет. в случае tcp, подтверждения накапливаются - они подтверждают, что получатель корректно принял все байты до номера последовательности подтверждения минус один. в этом примере три из ack подтвердили 2048 байт данных, а два подтвердили 1024 байта данных. (за исключением ack, появлявшихся при установлении и разрыве соединения.) с помощью tcpdump мы посмотрим tcp в действии. порядок прохождения пакетов, который мы видим, зависит от многих факторов, большинство из которых сложно проконтролировать: реализация посылающего tcp, реализация принимающего tcp, чтение данных принимающим процессом (это зависит от процесса построения временных графиков в операционной системе) и динамики сети (коллизии в ethernet). не существует одного единственного корректного способа для двух tcp осуществить обмен данными. для того чтобы показать, как все может измениться, на рисунке 20.2 показана еще одна временная диаграмма для того же самого обмена данными между теми же самыми хостами. обмен был осуществлен через несколько минут после того, который был показан на рисунке 20.1. рисунок 20.2 еще одна передача 8192 байт от svr4 к bsdi.
здесь мы видим несколько отличий. приемник не отправляет ack с номером 3073; вместо этого он ожидает и отправляет ack с номером 4097. принимающий посылает только четыре ack (сегменты 7, 10, 12 и 15): три из них на 2048 байт и один на 1024 байт. ack на последние 1024 байта данных отправляется в сегменте 17, вместе с подтверждением на fin. (сравните сегмент 17 на этом рисунке с сегментами 16 и 18 на рисунке 20.1.) быстрый отправитель, медленный получатель на рисунке 20.3 показана еще одна временная диаграмма, которая в данном случае показывает быстрого отправителя (sparc) и медленного получателя (80386 с медленной ethernet платой). динамика снова изменилась. рисунок 20.3 отправка 8192 байт от быстрого отправителя медленному получателю.
отправитель передает четыре сегмента данных (4-7), чтобы заполнить окно объявленное получателем. затем отправитель останавливается и ожидает подтверждения. получатель отправляет подтверждение (сегмент 8), однако объявляет окно равное 0. это означает, что получатель получил данные, однако все они находятся в tcp буферах получателя, потому что приложение не имеет возможности считать данные. еще один ack (называемый обновлением окна) посылается через 17,4 миллисекунды и объявляет о том, что получатель теперь может получить следующие 4096 байт. то, что выглядит как подтверждение (ack), в действительности является обновлением окна, потому что здесь не происходит подтверждения каких-либо вновь полученных данных, а просто объявляется новый размер окна. отправитель передает свои последние четыре сегмента (10-13) и опять заполняет окно принимающего. обратите внимание, что сегмент 13 содержит два флаговых бита: push и fin. после этого следуют еще два подтверждения от принимающего. они подтверждают последние 4096 байт данных (байты от 4097 до 8192) и fin (который имеет номер 8193). протокол изменения размера окна, который мы рассмотрели в предыдущем разделе, может быть проиллюстрирован следующим образом. рисунок 20.4 иллюстрация изменения окна tcp.
на этом рисунке мы пронумеровали байты с 1 по 11. окно, которое объявляется принимающим, называется предлагаемым окном и покрывает собой байты с 4 по 9, что означает, что получатель подтвердил все байты до 3 включительно и объявляет размер окна равный 6. обратитесь снова к главе 17, где мы говорили, что размер окна связан с подтвержденным номером последовательности. отправитель рассчитывает свой возможный размер окна. рассчитанное значение указывает, какое количество данных он может отправить немедленно. с течением времени размер окна сдвигается вправо, по мере того как принимающий подтверждает данные. взаимное перемещение двух границ окна увеличивает или уменьшает его размер. для описания перемещения границ окна вправо и влево используются три термина.
на рисунке 20.5 проиллюстрированы эти три термина. левая граница окна не может быть сдвинута влево, потому что она определяется номером принятого подтверждения от удаленной стороны. рисунок 20.5 перемещение границ окна.
если приняты ack, которые требуют перемещения левой границы окна влево, это дублированные ack, они отбрасываются. если левая граница окна совпала с правой, это называется нулевым окном. при этом отправитель прекращает передачу данных. пример на рисунке 20.6 показана динамика работы протокола tcp с изменением окна для передачи данных, показанной на рисунке 20.1. рисунок 20.6 протокол изменения размера окна для рисунка 20.1.
рассматривая эти рисунки, можно сделать следующие выводы. отправитель не должен передавать полное окно данных. один сегмент от получателя подтверждает данные и раздвигает окно вправо. это происходит из-за того, что размер окна связан с номером последовательности, которая была подтверждена. размер окна может уменьшаться, как это показано для сегментов 7 и 8, однако правая граница окна не должна перемещаться влево. получатель не должен ждать, пока окно заполнится перед отправкой ack. раньше мы видели, что большинство реализаций посылают ack для каждых двух сегментов, которые были получены. мы увидим примеры подобного поведения протокола изменения окна в следующих примерах. размер окна, предлагаемый получателем, обычно определяется получающим процессом. это может оказать влияние на производительность tcp. в 4.2bsd приемные и отправляющие буферы по умолчанию устанавливаются в 2048 байт каждый. в 4.3bsd оба были увеличены до 4096 байт. как мы можем видеть из всех примеров, приведенных в тексте, sunos 4.1.3, bsd/386 и svr4 все еще используют по умолчанию размер буфера 4096 байт. другие системы, такие как solaris 2.2, 4.4bsd и aix 3.2, используют по умолчанию большие размеры буферов, 8192 или 16384 байта. api сокеты позволяют процессу устанавливать размеры отправляющего и приемного буферов. размер принимающего буфера равен максимальному размеру объявленного окна для данного соединения. некоторые приложения изменяют размеры буферов для увеличения производительности.
[mogul 1993] показывает некоторые результаты передачи файла между двумя рабочими станциями, находящимися в сети ethernet, с различными размерами приемных и передающих буферов. (для одностороннего потока данных, который осуществляется при передаче файла, размер передающего буфера на стороне отправителя, и размер приемного буфера на стороне получателя имеют большое значение.) размеры по умолчанию, составляющие 4096 байт для обоих буферов, не являются оптимальными для ethernet. увеличение пропускной способности примерно на 40% было получено просто путем увеличения обоих буферов до 16384 байт. подобный результат также показан в [papadopoulos and parulkar 1993]. в разделе "пропускная способность для неинтерактивных данных" этой главы мы увидим, как рассчитать минимальный размер буфера для заданной полосы пропускания среды передачи и времени возврата между двумя сторонами. пример установить размер буферов можно с помощью программы sock. запустим сервер следующим образом: bsdi % sock -i -s -r6144 5555
при этом размер приемного буфера (опция -r) устанавливается в 6144 байта. затем стартуем клиента на хосте sun и осуществляем одну запись размером в 8192 байта: sun % sock -i -n1 -w8192 bsdi 5555
на рисунке 20.7 показан результат. рисунок 20.7 передача данных, при которой получатель объявляет размер окна равный 6144 байта.
во-первых, обратите внимание на то, что в сегменте номер 2, получатель предлагает размер окна, равный 6144 байта. так как размер окна увеличен, клиент немедленно посылает шесть сегментов (сегменты 4-9), после чего останавливается. в сегменте 10 все данные подтверждаются (байты с 1 по 6144), и объявляет размер окна равный всего лишь 2048, возможно потому, что принимающее приложение не может считать больше чем 2048 байт. сегменты 11 и 12 завершают передачу данных от клиента, этот же последний сегмент данных содержит флаг fin. сегмент 13 содержит тот же самый номер последовательности подтверждения как и сегмент 10, однако объявляет окно большего размера. сегмент 14 подтверждает последние 2048 байт данных и fin, а сегменты 15 и 16 просто объявляют окно большего размера. сегменты 17 и 18 осуществляют нормальное закрытие. мы видели флаг push практически в каждом примере работы tcp, однако никогда не говорили о том, как он используется. назначение этого флага заключается в том, что отправитель с его помощью предлагает получателю отправить все имеющиеся у него данные получающему процессу. эти данные могут состоять из чего-либо угодно находящегося в сегменте, где взведен флаг push, вместе с любыми другими данными, которые принимающий tcp собрал для принимающего процесса. в исходной спецификации tcp предполагалось, что программный интерфейс может позволить отправляющему процессу сказать своему tcp, когда необходимо установить флаг push. для интерактивных приложений, например, когда клиент посылает команду серверу, клиент может установить флаг push и ожидать отклика от сервера. (в упражнении 1 главы 19 мы представили, что клиент установил флаг push, когда осуществлялась запись 12-байтного запроса.) так как приложению клиента разрешено потребовать от своего tcp установить флаг, осуществляется уведомление tcp клиента о том, что процесс клиента не хочет, чтобы данные заполнили tcp буфер в ожидании дополнительных данных перед отправкой сегмента серверу. точно так же, когда tcp модуль сервера получил сегмент с флагом push, он получает уведомление о необходимости передать данные процессу сервера, а не ожидать прихода дополнительных данных. в настоящее время, однако, большинство api не предоставляют приложению способа сообщить своему tcp о необходимости установить флаг push. вместо этого реализации tcp самостоятельно определяют, когда необходимо установить этот флаг. большинство berkeley реализаций автоматически устанавливают флаг push, если отправка сегмента опустошает отправляющий буфер. это означает, что обычно мы будем видеть флаг push установленным для каждой записи от приложения, потому что в момент подобной записи данные отправляются. в комментариях говорится, что этот алгоритм в основном подходит для тех реализаций, которые передают принятые данные приложению только тогда, когда буфер заполнен или сегмент принят с флагом push. не существует возможности с использованием сокет api сказать tcp взвести push флаг или сказать, был ли установлен флаг push в принятых данных.
реализации berkeley игнорируют принятый push флаг, потому что они обычно никогда не задерживают доставку принятых данных приложению. примеры на рисунке 20.1 мы видели взведенный флаг push для всех восьми сегментов данных (4-6, 9, 11-13 и 15). это объясняется тем, что клиент осуществил восемь записей по 1024 байта каждая, и каждая запись опустошила отправляющий буфер. вернемся к рисунку 20.7. мы ожидаем, что флаг push будет установлен в сегменте 12, так как это последний сегмент данных. почему флаг push был установлен в сегменте 7, когда отправитель знал, что ему придется отправить еще некоторые байты данных? причина заключается в том, что размер отправляющего буфера отправителя составляет 4096 байт, даже если мы осуществляем одну запись размером в 8192 байта. еще один момент, на который необходимо обратить внимание на рисунке 20.7, заключается в трех последовательных ack, сегменты 14, 15 и 16. мы видели два последовательных ack на рисунке 20.3, однако это объяснялось тем, что получатель объявлял окно равное 0 (останавливал отправителя), поэтому когда окно открылось, потребовался еще один ack, с ненулевым значением окна, чтобы перестартовать отправителя. на рисунке 20.7, однако, значение окна никогда не устанавливается в 0. тем не менее, когда размер окна увеличивается до 2048 байт, отправляется еще один ack (сегменты 15 и 16), чтобы осуществить обновление окна на удаленном конце. (эти два окна, обновленные в сегментах 15 и 16, не нужны, так как с удаленного конца был принят fin, после чего отправитель не будет посылать данные.) большинство реализаций посылают это обновление окна, если размер окна достиг удвоенного значения максимального размера сегмента (2048 байт в данном примере, с mss равным 1024) или 50% максимально возможного размера окна (2048 байт в этом примере, с максимальным окном 4096). мы увидим это снова в разделе "синдром "глупого" окна" главы 22, когда будем более подробно рассматривать синдром "глупого окна". другой пример, где появился флаг push - рисунок 20.3. причина, по которой мы видим этот флаг в первых четырех сегментах данных (4-7), заключается в том, что каждый из них генерируется tcp и передается в ip уровень. однако, затем tcp должен остановиться, дождаться прихода ack, чтобы отправить окно размером 4096 байт. пока ожидается ack, tcp берет последние 4096 байт данных от приложения. когда окно открывается (сегмент 9), отправляющий tcp знает, что у него есть 4 сегмента, которые он может послать немедленно, поэтому он взводит флаг push в последнем сегменте (13). во всех примерах, которые мы рассмотрели, отправитель начинает свою работу, отправив несколько сегментов в сеть. размер записей может достигать размера окна, объявленного получателем. при этом все будет в порядке, если два хоста находятся в одной и той же локальной сети, однако если между отправителем и получателем присутствуют маршрутизаторы или медленные каналы, могут возникнуть проблемы. некоторые промежуточные маршрутизаторы должны будут поместить пакеты в очередь, которая, кстати сказать, может переполниться. [jacobson 1988] показывает, как в подобных случаях может значительно понизиться пропускная способность tcp соединения. поэтому от tcp требуется, чтобы он поддерживал алгоритм, который называется медленный старт. он заключается в том, что осуществляется исследование, с какой скоростью новые пакеты должны отправляться в сеть, причем эта скорость должна соответствовать скорости, с которой пришли подтверждения с удаленного конца. при работе с медленным стартом отправляющему tcp добавляется еще одно окно: окно переполнения, которое называется cwnd. когда устанавливается новое соединение с хостом, находящимся в другой сети, размер окна переполнения устанавливается равным размеру одного сегмента (размер сегмента объявлен удаленным концом). каждый раз, когда принимается ack, окно переполнения увеличивается на один сегмент. (размер cwnd измеряется в байтах, однако при медленном старте размер всегда увеличивается на размер сегмента.) отправитель может передать объем данных величиной до минимального размера окна переполнения и объявленного окна. с помощью окна переполнения, отправитель осуществляет управление потоком, тогда как с помощью объявленного окна потоком управляет получатель. отправитель начинает работу, отправив один сегмент и ожидая ack на этот сегмент. когда ack получен, окно переполнения увеличивается с одного сегмента до двух, и в этом случае могут быть отправлены два сегмента. когда каждый из этих двух сегментов подтвержден, окно переполнения увеличивается до 4. таким образом, осуществляется экспотенциальное увеличение. в определенной точке достигается максимум передачи для данного соединения (объединенной сети), в этом случае промежуточный маршрутизатор начинает отбрасывать пакеты. это говорит о том, что размер окна переполнения отправителя стал слишком большим. когда мы будем рассматривать алгоритмы тайм-аутов и повторных передач tcp в следующей главе, то увидим как обрабатывается подобная ситуация и что происходит с окном переполнения. а сейчас давайте посмотрим, как работает медленный старт. пример на рисунке 20.8 показаны данные, которые отправляются от хоста sun на хост vangogh.cs.berkeley.edu. эти данные проходят по медленному slip каналу, который в данном случае будет являться узким местом передачи. (из этой временной диаграммы удалено все, что связанно с установлением соединения.) мы видим, что отправитель отправляет один сегмент с 512 байтами данных, а затем ожидает ack. ack получено через 716 миллисекунд, что определяет время возврата. затем окно переполнения увеличивается до двух сегментов, и эти два сегмента отправляются. когда ack получен в сегменте 5, окно переполнения увеличивается до трех сегментов. несмотря на то, что может быть послано три сегмента, отправляется только два, перед тем как не будет получен еще один ack. мы вернемся к медленному старту в разделе "алгоритм предотвращения переполнения" главы 21 и увидим, как реализована альтернативная техника, называемая предотвращением переполнения. пропускная способность для неинтерактивных данных давайте посмотрим, как используется размер окна, как осуществляется управление потоком с помощью окон и медленного старта, а также как это влияет на пропускную способность tcp соединения, по которому передаются неинтерактивные данные. на рисунке 20.9 показана работа подобного соединения, отправитель находится слева, получатель - справа. на рисунке показано 16 моментов времени. для простоты время показано дискретно. в верхней половине каждого рисунка мы показываем сегменты, переносящие данные слева направо, и пронумерованные как 1, 2, 3 и так далее. ack двигаются в другом направлении и показаны в нижней половине каждого рисунка. мы рисуем ack меньше и указываем номера сегментов, которые подтверждаются. рисунок 20.8 пример медленного старта. рисунок 20.9 моменты времени 0-15, иллюстрирующие пропускную способность при передаче неинтерактивных данных.
в момент времени 0 отправитель посылает один сегмент. так как отправитель работает с медленным стартом (окно переполнения установлено в один сегмент), он должен ждать подтверждения на этот сегмент, перед тем как продолжить работу. в моменты времени 1, 2 и 3 сегмент проходит по одному промежутку времени вправо. в момент времени 4 получатель читает сегмент и генерирует подтверждение. в моменты времени 5, 6 и 7 подтверждение движется по одному промежутку времени влево, обратно к отправителю. таким образом, время возврата (rtt) составляет 8 промежутков времени. мы специально нарисовали сегмент подтверждения (ack) меньше чем сегмент данных, так как обычно он состоит из ip заголовка и tcp заголовка. здесь мы показываем поток данных без учета направления в один и тот же момент времени. также мы делаем предположение, что ack двигается с той же самой скоростью, что и сегмент данных, что в действительности не всегда верно. в-общем, время отправки пакета зависит от двух факторов: задержки прохождения (которая вызвана конечной скоростью света, временами ожидания и аппаратурой передачи) и задержки передачи, которая зависит от скорости среды передачи (количество бит, которое может быть передано в среде передачи за секунду). для данного пути между двумя узлами задержка прохождения фиксированна, тогда как задержка передачи зависит от размера пакета. при небольших скоростях определяющими являются задержки передачи (в упражнении 2 главы 7 мы даже не принимали во внимание задержку прохождения), однако для скоростей равных гигабитам определяющей является задержка прохождения (см. рисунок 24.6).
когда получатель принимает ack, он может передать два сегмента (которые мы пронумеровали как 2 и 3) в моменты времени 8 и 9. окно переполнения сейчас составляет два сегмента. эти два сегмента двигаются вправо по направлению к приемнику, где генерируются подтверждения в моменты времени 12 и 13. промежутки времени между подтверждениями (ack) идентичны промежуткам между сегментами данных. это поведение tcp называется самонастройкой по времени (self-clocking). так как получатель может генерировать ack только тогда, когда данные получены, по промежуткам между подтверждениями можно определить скорость прибытия данных к приемнику. (в действительности, организация очереди на маршруте возврата может изменить скорость прибытия ack.) на рисунке 20.10 показаны следующие 16 временных промежутков. прибытие двух ack увеличивает окно переполнения с 2 до 4 сегментов, и эти четыре сегмента отправляются в моменты времени 16-19. первый ack возвращается в момент времени 23. четыре ack увеличили окно переполнения с 4 сегментов до 8, и эти восемь сегментов передаются в моменты времени 24-31. в момент времени 31 канал между отправителем и получателем полностью загружен. по нему нельзя передать больше данных, вне зависимости от величины окна переполнения или величины окна, объявленного получателем. в каждый момент времени получатель забирает сегмент из сети, а другой помещается в сеть отправителем. канал заполнен большим количеством сегментов данных, в нем присутствует точно такое же количество подтверждений. это идеальное состояние соединения. емкость канала в зависимости от полосы пропускания (bandwidth-delay product) теперь мы можем ответить на вопрос, какого размера должно быть окно. в нашем примере, для достижения максимальной пропускной способности, отправитель должен выдать в сеть в один момент времени восемь пакетов и на них должно быть получено подтверждение. окно, объявленное получателем, должно быть больше чем тот предел, который может быть передан отправителем. мы можем рассчитать емкость канала следующим образом:
емкость (биты) = ширина полосы (бит в секунду) х время возврата (секунда)
обычно это называется емкостью канала в зависимости от полосы пропускания (bandwidth-delay product). это значение может изменяться в очень широких пределах, в зависимости от скорости сети и времени возврата (rtt) между двумя концами. например, для телефонной линии t1 (1544000 бит в секунду), находящейся в соединенных штатах (rtt примерно равно 60 миллисекунд), емкость канала составляет 11580 байт. это значение в общем сопоставимо с размерами буферов, которые мы обсудили в разделе "размер окна" этой главы, однако телефонная линия t3 (45000000 бит в секунду), так же пролегающая в соединенных штатах, уже имеет емкость канала равную 337500 байтам, что значительно больше чем максимально возможное окно tcp (65535 байт). мы обсудим новую опцию масштабирования окна tcp в разделе "опция масштабирования окна" главы 24 и увидим, как можно обойти существующие в настоящее время ограничения tcp. рисунок 20.10 моменты времени 16-31 для примера пропускной способности при передаче неинтерактивных данных. значение 1544000 бит в секунду для телефонной линии t1 - это примерная скорость передачи битов. скорость передачи данных в действительности составляет 1536000 бит в секунду, так как 1 бит из 193 используется для разделения на фреймы. битовая скорость передачи телефонной линии t3 в действительности составляет 44736000 бит в секунду, а скорость передачи данных может достигать 44210000 бит в секунду. для наших рассуждений мы будем использовать значение 1,544 мбит/сек и 45 мбит/сек.
таким образом, ширина полосы или задержка могут оказывать влияние на емкость канала между отправителем и получателем. на рисунке 20.11 мы графически показали, как удвоение rtt удваивает емкость канала. рисунок 20.11 удвоение rtt удваивает емкость канала.
в нижней части рисунка 20.11, с более длинным rtt, канал может содержать восемь сегментов вместо четырех. точно так же, на рисунке 20.12 показано, что удвоение полосы передачи также удваивает емкость канала. рисунок 20.12 удвоение полосы передачи удваивает емкость канала.
в нижней части рисунка 20.12 мы предположили, что скорость сети удвоена, что, в свою очередь, позволяет нам посылать четыре сегмента за половину отрезка времени, показанного на верхней части рисунка. и снова емкость канала была увеличена в два раза. (мы предположили, что сегменты в верхней части этого рисунка имеют тот же размер, то есть содержат такое же количество бит, как и сегменты, находящиеся в нижней половине.) переполнение может возникнуть, когда данные прибывают из быстрого канала (локальная сеть), а затем отправляются в медленный канал (глобальная сеть). переполнение также может возникнуть, когда несколько входных потоков прибывают на маршрутизатор, выходная пропускная способность которого меньше чем сумма входящих данных. на рисунке 20.13 показана типичная ситуация, когда быстрый канал переполняет медленный. мы считаем подобную ситуацию типичной, потому что большинство хостов подключены к локальным сетям, а также через маршрутизатор подключены к более медленным глобальным сетям. (здесь как и прежде, мы предполагаем, что размер всех сегментов данных (9-20) в верхней части рисунка одинаков, и все размеры подтверждений в нижней части рисунка также одинаковы.) на этом рисунке мы пометили маршрутизатор r1 как "узкое место", потому что именно в этой точке происходит переполнение. маршрутизатор может принимать пакеты из локальной сети, которая находится слева от него, быстрее, чем они могут быть отправлены в глобальную сеть, которая находится справа. (обычно r1 и r3 это один и тот же маршрутизатор, так же как r2 и r4, хотя это и не обязательно; могут быть использованы асимметричные маршруты.) рисунок 20.13 переполнение, вызванное разными скоростями каналов.
когда маршрутизатор r2 помещает принятые пакеты в локальную сеть, находящуюся на рисунке справа от него, он сохраняет те же промежутки, который были в глобальной сети, находящейся на рисунке слева от него, несмотря на то, что ширина полосы локальной сети больше. точно так же, промежутки между подтверждениями, которые двигаются в обратном направлении, точно такие же, как и промежутки между подтверждениями, двигавшимися по более медленному каналу. на рисунке 20.13 мы предположили, что отправитель не использует медленный старт и посылает сегменты, которые мы пронумеровали 1-20, с такой скоростью, которая обеспечивается локальной сетью. (при этом сделано предположение, что принимающий хост объявил размер окна равный по меньшей мере 20 сегментам.) промежутки между подтверждениями соответствуют ширине полосы самого медленного канала. мы считаем, что маршрутизатор, являющийся узким местом, имеет достаточный размер буферов для всех 20 сегментов. если это не так, маршрутизатор будет отбрасывать пакеты. мы посмотрим, как избежать этого, когда будем говорить о предотвращении переполнения в разделе "алгоритм предотвращения переполнения" главы 21. tcp предоставляет режим срочности (urgent mode), который позволяет одному концу сообщить другому о том, что в обычный поток данных каким-либо образом были помещены "срочные данные". удаленный конец, таким образом, уведомляется о том, что в поток данных помещены срочные данные, и уже от него зависит (от принимающего конца) как с ними поступить. уведомление о присутствии срочных данных в потоке осуществляется путем установки двух полей в tcp заголовке (рисунок 17.2). устанавливается бит urg, а 16-битный указатель срочности содержит положительное смещение, которое должно быть добавлено к полю номера последовательности в tcp заголовке, чтобы получить номер последовательности последнего байта срочных данных. до сих пор продолжаются дебаты на предмет того, куда должен указывать указатель срочности: или на последний байт срочных данных или на байт, следующий за последним байтом срочных данных. исходная спецификация tcp позволяет использовать обе интерпретации, однако требования к хостам host requirements rfc указывают, какая из них верна: указатель срочности указывает на последний байт срочных данных. проблема, однако, для большинства реализаций (как, например, реализаций berkeley) заключается в том, что они все еще поддерживают неверную интерпретацию. те же реализации, которые соответствуют требованиям к хостам host requirements rfc, считаются правильными, однако не могут общаться корректно с большинством других хостов.
tcp должен информировать получающий процесс о том, что принят указатель срочности; либо он уже принят, либо двигается в потоке данных. затем принимающее приложение читает поток данных; оно должно быть в состоянии определить, когда появится указатель срочности. приложение находится в "срочном режиме", все время, пока читает данные с текущей позиции до указателя срочности. после того как указатель срочности принят, приложение возвращается в нормальный режим. сам tcp может сказать очень немного о срочных данных. не существует возможности точно определить, где в потоке начинаются срочные данные. информация, которая посылается по соединению от tcp, сообщает в том, что режим срочности начался (бит urg в tcp заголовке), а также указывает на последний байт срочных данных. вся остальная работа оставлена приложению. к сожалению, большинство реализаций неверно называют режим срочности tcp данными, выходящими за полосу (out-of-band) . если приложение действительно хочет выделить канал, выходящий за полосу, то самый простой способ добиться этого - создать второе tcp соединение. (некоторые транспортные уровни предоставляют то, что большинство пользователей считают действительно данными, выходящими за полосу: логически выделенный канал данных, который, однако, использует то же самое соединение, что и стандартный поток данных. tcp этого не предоставляет.) путаница между режимом срочности tcp и данными, выходящими за полосу, также объясняется доминирующим программным интерфейсом, сокеты api устанавливают соответствие между режимом срочности tcp и тем, что сокеты называют данными, выходящими за полосу.
для чего используется режим срочности? рассмотрим два наиболее широко используемых приложения telnet и rlogin. когда пользователь нажимает клавишу прерывания, используется режим срочности. (мы покажем это в главе 26.) в случае ftp, когда пользователь прерывает передачу файла, также используется режим срочности. (показано в примерах к главе 27.) telnet и rlogin используют режим срочности в направлении от сервера к клиенту, потому что поток данных в этом направлении может быть остановлен клиентом tcp (путем объявления размера окна равного 0). однако, если процесс сервера входит в режим срочности, сервер tcp немедленно отправляет указатель срочности и устанавливает флаг urg, даже если он не может послать какие-либо данные. когда tcp клиент получает это уведомление, он, в свою очередь, уведомляет процесс клиента; таким образом, клиент может считать свой ввод от сервера, чтобы открыть окно и разрешить прохождение потока данных. что произойдет, если отправитель войдет в режим срочности несколько раз, перед тем как получатель обработает все данные до прихода первого указателя срочности? указатель срочности все еще двигается в потоке данных, а его предыдущее положение для получателя потеряно. для получателя существует только один указатель срочности и его значение перезаписывается, когда приходит новое значение указателя срочности с удаленного конца. это означает, что если содержимое потока данных, которое формируется отправителем, когда он входит в режим срочности, важно для получателя, эти байты данных должны быть специально помечены (каким-либо образом) отправителем. мы увидим, что telnet помечает все свои командные байты в потоке данных, ставя перед ними байт 255. пример давайте посмотрим, как tcp посылает срочные данные, даже когда окно получателя закрыто. мы стартуем программу sock на хосте bsdi и выдержим паузу в течении 10 секунд после того, как соединение будет установлено (опция -p), перед тем как начать читать из сети. это позволяет удаленному концу заполнить отправляемое окно. bsdi % sock -i -s -p10 5555
затем мы стартуем клиента на хосте sun, указав ему установить отправляющий буфер размером 8192 байта (опция -s) и осуществить шесть записей в сеть, каждая размером 1024 байта (опция -n). также мы указали -u5, что заставляет клиента записать 1 байт данных и войти в режим срочности перед записью в сеть пятого буфера. также мы установили флаг отладки, чтобы посмотреть в каком порядке будут осуществлены записи: sun % sock -v -i -n6 -s8192 -u5 bsdi 5555 connected on 140.252.13.33.1305 to 140.252.13.35.5555 so_sndbuf = 8192 tcp_maxseg = 1024 wrote 1024 bytes wrote 1024 bytes wrote 1024 bytes wrote 1024 bytes wrote 1 byte of urgent data wrote 1024 bytes wrote 1024 bytes
мы установили размер буфера отправителя в 8192 байта, чтобы позволить отправляющему приложению немедленно записать все свои данные. на рисунке 20.14 показан вывод tcpdump для этого обмена. (все имеющее отношение к установлению соединения удалено.) в строках 1-5 показано, как отправитель заполняет окно приемника четырьмя сегментами по 1024 байта. затем отправитель останавливается, потому что окно приемника заполнено. (подтверждение в строке 4 подтверждает данные, однако не перемещает правую границу окна.) после того как приложение осуществило четыре обычные записи данных, приложение пишет 1 байт данных и входит в режим срочности. в строке 6 показан результат этой записи. указатель срочности установлен в 4098. указатель срочности отправляется с флагом urg, даже если отправитель не может послать какие-либо данные. пять из этих подтверждений (ack) отправляются примерно в течение 13 миллисекунд (строки 6-10). первое отправляется, когда приложение пишет 1 байт и входит в режим срочности. следующие два отправляются, когда приложение осуществляет две последние записи, каждая по 1024 байта. (даже если tcp не может отправить эти 2048 байт данных, каждый раз, когда приложение осуществляет запись, вызывается функция вывода tcp, и когда она видит, что осуществлен вход в режим срочности, то посылает еще одно уведомление о срочности.) четвертый из этих ack появляется, когда приложение со своей стороны закрывает соединение. (в этом случае снова вызывается функция вывода tcp.) отправляющее приложение прекратило свою работу через несколько миллисекунд после старта - перед тем как принимающее приложение осуществило свою первую запись. tcp поставило в очередь все данные и отправляет их по возможности. (именно поэтому мы указали размер буфера равный 8192 байта - таким образом, все данные могут поместиться в буфер.) пятый из этих ack сгенерирован на получение ack в строке 4. отправляющий tcp возможно уже поставил в очередь четвертый сегмент для вывода (строка 5), перед тем как прибыл этот ack. приход этого ack от удаленной стороны также приводит к вызову подпрограммы вывода tcp.
рисунок 20.14 вывод tcpdump для режима срочности tcp.
затем получатель подтверждает последние 1024 байта данных (строка 11), а также объявляет размер окна равный 0. отправитель отвечает еще одним сегментом, содержащим уведомление о срочности. получатель объявляет окно равное 2048 байт в строке 13, когда приложение активизируется и читает некоторые данные из приемного буфера. отправляются следующие два сегмента размером 1024 байта (строки 14 и 15). в первом сегменте установлено уведомление о срочности, указатель срочности также находится в этом сегменте. второй сегмент выключает уведомление срочности. когда получатель вновь открывает окно (строка 16), отправитель посылает последний байт данных (с номером 6145) и осуществляет нормальное закрытие соединения. на рисунке 20.15 показаны номера последовательности 6145 байт данных, которые были отправлены. мы видим, что номер последовательности байта, записанного при входе в режим срочности, равен 4097, однако значение указателя срочности на рисунке 20.14 равно 4098. это является подтверждением того, что данная реализация (sunos 4.1.3) устанавливает указатель срочности на 1 байт позади последнего байта срочных данных. рисунок 20.15 пример того, как приложение пишет tcp сегменты в режиме срочности.
этот рисунок также позволяет нам увидеть, как tcp осуществляет сборку пакетов из данных, которые записаны приложением. один байт, который был выдан при входе в режим срочности, отправляется вместе со следующими 1023 байтами данных, находящимися в буфере. следующий сегмент также содержит 1024 байта данных, а последний сегмент содержит 1 байт данных. как мы уже сказали раньше в этой главе, не существует одного единственного способа для обмена неинтерактивными данными с использованием tcp. это динамический процесс, который зависит от большого количества факторов, некоторые из которых мы можем контролировать (как, например, размеры отправляющего и приемного буферов), а некоторые мы контролировать не можем (как, например, переполнение сети или характеристики конкретной реализации). в этой главе мы рассмотрели несколько передач информации с использованием tcp, объяснили все характеристики и алгоритмы, которые необходимо было рассмотреть подробно. фундаментальной концепцией эффективной передачи неинтерактивных данных является протокол изменяющегося окна tcp. также мы увидели, что для того, чтобы получить максимально высокую скорость передачи данных с использованием tcp, необходимо поддерживать заполненным канал между отправителем и получателем. мы оценили емкость этого канала как "емкость в зависимости от полосы пропускания" и увидели взаимосвязь между этим параметром и размером окна. мы вернемся к этой концепции в разделе "производительность tcp" главы 24, когда будем рассматривать производительность tcp. также мы рассмотрели флаг push, который используется в tcp. мы видим его в выводе практически всех отладочных программ, однако не можем управлять установкой этого флага. и последнее: мы рассмотрели срочные данные tcp, которые часто по ошибке называются "данными, выходящими за полосу". режим срочности tcp это всего лишь уведомление от отправителя получателю о том, что были отправлены срочные данные, а также указание на номер последовательности последнего байта срочных данных. программные интерфейсы для приложений, используемые для срочных данных, часто не очень оптимальны, что ведет к некоторой неразберихе. упражнения
|