[Перевод] Головоломки TCP

1c0ac26feba04897addf31831cc60ea4.jpg

Говорят, что нельзя полностью понять систему, пока не поймёшь её сбои. Ещё будучи студентом я ради забавы написал реализацию TCP, а потом несколько лет проработал в IT, но до сих пор продолжаю глубже и глубже изучать работу TCP — и его ошибки. Самое удивительное, что некоторые из этих ошибок проявляются в базовых вещах. И они неочевидны. В этой статье я преподнесу их как головоломки, в стиле Car Talk или старых головоломок Java. Как и любые другие хорошие головоломки, их очень просто воспроизвести, но решения обычно удивляют. И вместо того, чтобы фокусировать наше внимание на загадочных подробностях, эти головоломки помогают изучить некоторые глубинные принципы работы TCP.

Необходимые условия
Эти головоломки подразумевают наличие базовых знаний о работе TCP на Unix-подобных системах. Но вам не нужно быть мастером, чтобы вникнуть в них. Например:
  • Информацию о состояниях сеанса TCP, трёх этапах соединения и об этапах его завершения можно найти в Википедии.
  • Программы, как правило, взаимодействуют с сокетами, используя read, write, connect, bind, listen и accept. Помимо этого, есть также send и recv, но в наших примерах они будут вести себя как read и write.
  • Я буду использовать в этой статье poll. Хотя многие системы используют что-то более эффективное, например, kqueue или epoll, в рамках нашей задачи все эти инструменты будут эквивалентны. Что касается приложений, использующих операции блокирования, а не какой-либо из этих механизмов: один раз поняв, как ошибки TCP отражаются на poll, вам будет проще догадаться, какой эффект они окажут на любые операции блокирования.

Вы можете повторить все эти примеры самостоятельно. Я использовал две виртуальные машины, запущенные с помощью VMware Fusion. Результаты получились такие же, как на production-сервере. Для тестирования я использовал nc(1) на SmartOS, и не поверю, что любая из воспроизводимых неполадок будет специфична для конкретной ОС. Для отслеживания системных вызовов и сбора грубой информации о таймингах я использовал утилиту truss (1) из проекта illumos. Вы можете получить подобную информацию с помощью dtruss (1m) под OS X или strace (1) под GNU/Linux.

nc(1) очень простая программа. Мы будем использовать её в двух режимах:

  • Как сервер. В этом режиме nc создаст сокет, будет прослушивать его, вызовет accept и заблокирует, пока не будет установлено соединение.
  • Как клиент. В этом режиме nc создаст сокет и установит соединение с удалённым сервером.

В обоих режимах после установки соединения каждая из сторон использует poll для ожидания стандартного ввода или подключения сокета, имеющего готовые для чтения данные. Входящие данные выводятся в терминал. Данные, которые вы вводите в терминал, отправляются через сокет. При нажатии CTRL-C сокет закрывается и процесс останавливается.
В примерах мой клиент будет называться kang, а сервер — kodos.Разминка: нормальный разрыв TCP
Начнём с базовой ситуации. Представим, что мы настроили сервер на kodos:

Сервер

[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp:  1464310423.7650  [ Fri May 27 00:53:43 UTC 2016 ]
 0.0027 bind(3, 0x08065790, 32, SOV_SOCKBSD)            = 0
 0.0028 listen(3, 1, SOV_DEFAULT)                       = 0
accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) (sleeping...)

(Напоминаю, что в этих примерах я использую truss для вывода системных вызовов, которые делает nc. Информация о времени выводится с помощью флага -d, а -t позволяет выбрать, какие из вызовов мы хотим увидеть.)

Теперь я устанавливаю соединение на kang:

Клиент

[root@kang ~]# truss -d -t connect,pollsys,read,write,close nc 10.88.88.140 8080
Base time stamp:  1464310447.6295  [ Fri May 27 00:54:07 UTC 2016 ]
...
 0.0062 connect(3, 0x08066DD8, 16, SOV_DEFAULT)         = 0
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

На kodos мы видим:

Сервер

23.8934 accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) = 4
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)

Подключение TCP находится в состоянии ESTABLISHED, а оба процесса в poll. Мы можем увидеть это на каждой системе с помощью netstat:

Сервер

[root@kodos ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080    10.88.88.139.33226   1049792      0 1049800      0 ESTABLISHED
...

Клиент
[root@kang ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.33226   10.88.88.140.8080    32806      0 1049800      0 ESTABLISHED
...

Вопрос: когда мы завершим один из процессов, что случится с другим? Поймёт ли он, что произошло? Как он это поймёт? Попробуем предугадать поведение конкретных системных вызовов и объяснить, почему каждый из них делает то, что делает.

Нажмём CTRL-C на kodos:

Сервер

pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)
^C127.6307          Received signal #2, SIGINT, in pollsys() [default]

А вот что мы видим на kang:

Клиент

pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
126.1771        pollsys(0x08045670, 2, 0x00000000, 0x00000000)  = 1
126.1774        read(3, 0x08043670, 1024)                       = 0
126.1776        close(3)                                        = 0
[root@kang ~]# 

Что случилось? Давайте разберемся:
  1. Осуществляя выход из процесса, мы отправили SIGINT на сервер. После выхода закрылись дескрипторы файлов.
  2. Когда закрывается последний дескриптор для сокета ESTABLISHED, стек TCP на kodos отправляет через соединение FIN и переходит в состояние FIN_WAIT_1.
  3. Стек TCP на kang получает пакет FIN, переводит собственное соединение в состояние CLOSE_WAIT и отправляет в ответ ACK. Пока клиент nc блокирует сокет — он готов к чтению, ядро будит этот тред с помощью POLLIN.
  4. Клиент nc видит POLLIN для сокета и вызывает read, который тут же возвращает 0. Это означает конец соединения. nc решает, что мы закончили работу с сокетом, и закрывает его.
  5. Тем временем, стек TCP на kodos получает ACK и переходит в состояние FIN_WAIT_2.
  6. Пока клиент nc на kang закрывает свой сокет, стек TCP на kang отправляет FIN на kodos. Соединение на kang переходит в состояние LAST_ACK.
  7. Стек TCP на kodos получает FIN, соединение переходит в состояние TIME_WAIT, и стек на kodos подтверждает FIN.
  8. Стек TCP на kang получает ACK для FIN и полностью удаляет соединение.
  9. Спустя две минуты соединение TCP на kodos закрывается, и стек полностью удаляет соединение.

Очерёдность этапов может незначительно меняться. Также kang может вместо FIN_WAIT_2 проходить через состояние CLOSING.

Вот так, согласно netstat, выглядит финальное состояние:

Сервер

[root@kodos ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080    10.88.88.139.33226   1049792      0 1049800      0 TIME_WAIT

На kang для этого соединения нет никаких исходящих данных.

Промежуточные состояния проходят очень быстро, но вы можете отследить их с помощью DTrace TCP provider. Поток пакетов можно посмотреть с помощью snoop (1m) или tcpdump (1).

Выводы: Мы увидели нормальный путь прохождения системных вызовов во время установки и закрытия соединения. Обратите внимание, что kang незамедлительно обнаружил факт закрытия соединения на kodos — он был разбужен из poll, а возвращение нуля read обозначило завершение потока передачи. В этот момент kang решил закрыть сокет, что привело к закрытию соединения с kodos. Мы вернёмся к этому позже и посмотрим, что будет, если kang не станет закрывать сокет в этой ситуации.

Головоломка 1: Перезапуск электропитания
Что случится с установленным неактивным TCP подключением при перезапуске питания одной из систем?

Поскольку в процессе запланированной перезагрузки многие процессы завершаются корректным образом (с использованием команды «reboot»), то результат будет такой же, если ввести в консоль kodos команду «reboot» вместо завершения работы сервера с помощью CTRL-C. Но что случится, если в предыдущем примере мы просто отключим электропитание для kodos? В конечном итоге kang об этом узнает, верно?

Давайте проверим. Устанавливаем подключение:

Сервер

[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp:  1464312528.4308  [ Fri May 27 01:28:48 UTC 2016 ]
 0.0036 bind(3, 0x08065790, 32, SOV_SOCKBSD)            = 0
 0.0036 listen(3, 1, SOV_DEFAULT)                       = 0
 0.2518 accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) = 4
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)

Клиент
[root@kang ~]# truss -d -t open,connect,pollsys,read,write,close nc 10.88.88.140 8080
Base time stamp:  1464312535.7634  [ Fri May 27 01:28:55 UTC 2016 ]
...
 0.0055 connect(3, 0x08066DD8, 16, SOV_DEFAULT)         = 0
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

Для эмуляции перезапуска электропитания я воспользуюсь функцией «reboot» из VMware. Обратите внимание, что это будет настоящий перезапуск — всё, что приводит к постепенному выключению, больше похоже на первый пример.

Спустя 20 минут kang всё в том же состоянии:

Клиент

pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

Мы склонны верить, что работа TCP заключается в постоянном поддержании абстракции (а именно, TCP-соединения) между несколькими системами, так что подобные случаи сломанной абстракции выглядят удивительно. И если вы считаете, что это какая-то проблема nc (1), то вы ошибаетесь. «netstat» на kodos не показывает никакого соединения с kang, но при этом kang покажет полностью рабочее подключение к kodos:

Клиент

[root@kang ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.50277   10.88.88.140.8080    32806      0 1049800      0 ESTABLISHED
...

Если оставить всё как есть, то kang никогда не узнает, что kodos был перезагружен.

Теперь предположим, что kang пытается отправить данные kodos. Что произойдёт?

Клиент

pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
kodos, are you there?
3872.6918       pollsys(0x08045670, 2, 0x00000000, 0x00000000)  = 1
3872.6920       read(0, " k o d o s ,   a r e   y".., 1024)     = 22
3872.6924       write(3, " k o d o s ,   a r e   y".., 22)      = 22
3872.6932       pollsys(0x08045670, 2, 0x00000000, 0x00000000)  = 1
3872.6932       read(3, 0x08043670, 1024)                       Err#131 ECONNRESET
3872.6933       close(3)                                        = 0
[root@kang ~]#

Когда я ввожу сообщение и жму Enter, kodos просыпается, читает сообщение из stdin и отправляет его через сокет. Вызов write успешно завершён! nc возвращается в poll в ожидании следующего события, и в конце концов приходит к выводу, что сокет не может быть прочитан без блокировки, после чего вызывает read. В этот раз read падает со статусом ECONNRESET. Что это значит? Документация к read (2) говорит нам:
[ECONNRESET]
Была попытка чтения из сокета, то соединение было принудительно закрыто пиром.

Другой источник содержит чуть больше подробностей:
   ECONNRESET
              Аргумент filedes ссылается на сокет с установленным соединением. Оно было принудительно закрыто пиром и больше недействительно. Операции ввода/вывода больше не могут выполняться с filedes.

Эта ошибка не означает какую-то конкретную проблему с вызовом read. Она лишь говорит о том, что сокет был отключён. По этой причине большинство операций с сокетом приведут к ошибке.

Так что же случилось? В тот момент, когда nc на kang попытался отправить данные, стек TCP всё ещё не знал, что подключение уже мертво. kang отправил пакет данных на kodos, который ответил RST, потому что ничего не знал о подключении. kang увидел RST и прервал подключение. Файловый дескриптор сокета закрыть невозможно, — файловые дескрипторы работают не так, —, но последующие операции будут неудачными со статусом ECONNRESET, пока nc не закроет файловый дескриптор.

Выводы:

  1. Жесткое отключение энергии сильно отличается от аккуратного выключения. При тестировании распределённых систем нужно отдельно проверять и этот сценарий. Не ждите, что всё будет так же, как и при обычной остановке процесса (kill).
  2. Бывают ситуации, когда одна сторона уверена, что TCP-соединение установлено, а другая — не уверена, и эта ситуация никогда не будет решена автоматически. Управлять решением таких проблем можно с использованием keep-alive для соединений на уровне приложения или TCP.
  3. Единственная причина, по которой kang всё-таки узнал об исчезновении удалённой стороны, заключается в том, что он отправил данные и получил ответ, сигнализирующий об отсутствии подключения.

Возникает вопрос:, а что если kodos по какой-то причине не отвечает на отправку данных? Головоломка 2: Отключение электропитания
Что случится, если конечная точка TCP соединения отключится от сети на какое-то время? Узнают ли об этом остальные узлы? Если да, то как? И когда?

Вновь установим соединение с помощью nc:

Сервер

[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp:  1464385399.1661  [ Fri May 27 21:43:19 UTC 2016 ]
 0.0030 bind(3, 0x08065790, 32, SOV_SOCKBSD)        = 0
 0.0031 listen(3, 1, SOV_DEFAULT)           = 0
accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) (sleeping...)
 6.5491 accept(3, 0x08047B3C, 0x08047C3C, SOV_DEFAULT, 0) = 4
pollsys(0x08045680, 2, 0x00000000, 0x00000000) (sleeping...)

Клиент
[root@kang ~]# truss -d -t open,connect,pollsys,read,write,close nc 10.88.88.140 8080
Base time stamp:  1464330881.0984  [ Fri May 27 06:34:41 UTC 2016 ]
...
 0.0057 connect(3, 0x08066DD8, 16, SOV_DEFAULT)         = 0
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

Теперь внезапно выключим питание kodos и попытаемся отправить данные с kang:

Клиент

pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

114.4971        pollsys(0x08045670, 2, 0x00000000, 0x00000000)  = 1
114.4974        read(0, "\n", 1024)                             = 1
114.4975        write(3, "\n", 1)                               = 1
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

Вызов write завершается нормально, и я долго ничего не вижу. Только через пять минут появляется:

Клиент

pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
425.5664        pollsys(0x08045670, 2, 0x00000000, 0x00000000)  = 1
425.5665        read(3, 0x08043670, 1024)                       Err#145 ETIMEDOUT
425.5666        close(3)                                        = 0

Эта ситуация очень похожа на ту, когда мы перезапускали электропитание вместо полного отключения. Есть два отличия:
  • системе потребовалось 5 минут на осознание ситуации,
  • статус ошибки был ETIMEDOUT.

Снова обратите внимание — это истёкший тайм-аут read. Мы бы увидели ту же самую ошибку и при других операциях с сокетом. Это происходит потому, что сокет входит в состояние, когда истёк тайм-аут подключения. Причина этого в том, что удалённая сторона слишком долго не подтверждала пакет данных — 5 минут, в соответствии с настройками этой системы.

Выводы:

  1. Когда удалённая система вместо перезапуска электропитания просто выключается, то первая система может узнать об этом, только отправив данные. В ином случае она никогда не узнает об обрыве соединения.
  2. Когда система слишком долго пытается отправить данные и не получает ответа, TCP-соединение закрывается и все операции с сокетом будут завершаться с ошибкой ETIMEDOUT.

Головоломка 3: Нарушение соединения без его падения
На этот раз вместо того, чтобы описывать вам специфическую ситуацию и спрашивать, что происходит, я поступлю наоборот: опишу некое наблюдение и посмотрю, сможете ли вы понять, как такое произошло. Мы обсуждали несколько ситуаций, в которых kang может верить, что он подключён к kodos, но kodos об этом не знает. Возможно ли для kang быть подключённым к kodos так, чтобы kodos не знал об этом в течение неопределённого срока (т.е. проблема не решится сама собой), и при этом не было бы отключения или перезапуска электропитания, никакой другой ошибки операционной системы kodos или сетевого оборудования?

Подсказка: рассмотрим вышеописанный случай, когда соединение застряло в статусе ESTABLISHED. Справедливо считать, что ответственность за решение этой проблемы несёт приложение, так как оно держит сокет открытым и может обнаружить посредством отправки данных, когда соединение было прервано. Но что если приложение уже не держит сокет открытым?

В разминке мы рассматривали ситуацию, когда nc на kodos закрыло сокет. Мы сказали, что nc на kang прочитало 0 (указатель окончания передачи) и закрыло сокет. Допустим, сокет остался открытым. Очевидно, что из него невозможно было бы читать. Но касательно TCP ничего не говорится о том, что вы не можете отправлять дополнительные данные той стороне, которая послала вам FIN. FIN означает лишь закрытие потока данных в том направлении, по которому был послан FIN.

Чтобы продемонстрировать это, мы не можем использовать nc на kang, потому что оно автоматически закрывает сокет после получения 0. Поэтому, я написал демо-версию nc, под названием dnc, которая пропускает этот момент. Также dnc явным образом выводит системные вызовы, которые она совершает. Это даст нам шанс отследить состояния TCP.

Сперва настроим подключение:

Сервер

[root@kodos ~]# truss -d -t bind,listen,accept,poll,read,write nc -l -p 8080
Base time stamp:  1464392924.7841  [ Fri May 27 23:48:44 UTC 2016 ]
 0.0028 bind(3, 0x08065790, 32, SOV_SOCKBSD)            = 0
 0.0028 listen(3, 1, SOV_DEFAULT)                       = 0
accept(3, 0x08047B2C, 0x08047C2C, SOV_DEFAULT, 0) (sleeping...)
 1.9356 accept(3, 0x08047B2C, 0x08047C2C, SOV_DEFAULT, 0) = 4
pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)

Клиент
[root@kang ~]# dnc 10.88.88.140 8080
2016-05-27T08:40:02Z: establishing connection
2016-05-27T08:40:02Z: connected
2016-05-27T08:40:02Z: entering poll()

Теперь убедимся, что на обеих сторонах подключение находится в статусе ESTABLISHED:

Сервер


[root@kodos ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080    10.88.88.139.37259   1049792      0 1049800      0 ESTABLISHED

Клиент
[root@kang ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.37259   10.88.88.140.8080    32806      0 1049800      0 ESTABLISHED

На kodos применим CTRL-C для процесса nc:

Сервер

pollsys(0x08045670, 2, 0x00000000, 0x00000000) (sleeping...)
^C[root@kodos ~]# 

На kang сразу увидим следующее:

Клиент

2016-05-27T08:40:12Z: poll returned events 0x0/0x1
2016-05-27T08:40:12Z: reading from socket
2016-05-27T08:40:12Z: read end-of-stream from socket
2016-05-27T08:40:12Z: read 0 bytes from socket
2016-05-27T08:40:12Z: entering poll()

Теперь посмотрим на статус подключений TCP:

Сервер

[root@kodos ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.140.8080    10.88.88.139.37259   1049792      0 1049800      0 FIN_WAIT_2

Клиент
[root@kang ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.37259   10.88.88.140.8080    1049792      0 1049800      0 CLOSE_WAIT

Это имеет смысл: kudos отправил FIN на kang. FIN_WAIT_2 показывает, что kodos получил ACK от kang в ответ на посланный им FIN, а CLOSE_WAIT показывает, что kang получил FIN, но не отправил FIN в ответ. Это вполне нормальное состояние TCP-подключения, которое может длится бесконечно. Представьте, что kodos отправил запрос kang и не планировал отправлять ничего больше; kang может часами счастливо отправлять данные в ответ. Только в нашем случае kodos фактически закрыл сокет.

Давайте подождём минуту и вновь проверим статус TCP-подключений. Выяснилось, что на kodos подключение полностью пропадает, но всё ещё существует на kang:

Клиент

[root@kang ~]# netstat -f inet -P tcp -n

TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
–––––––––––––––––––– –––––––––––––––––––– ––––– –––––- ––––– –––––- ––––––––––-
10.88.88.139.37259   10.88.88.140.8080    1049792      0 1049800      0 CLOSE_WAIT

Мы столкнулись с менее известной ситуацией, связанной со TCP-стеком: когда приложение закрыло сокет, стек отправил FIN, удалённый стек его распознал FIN, а локальный стек ожидает фиксированный период времени и закрывает соединение. Причина? Удалённая сторона была перезагружена. Этот случай аналогичен тому, когда подключение на одной стороне находится в статусе ESTABLISHED, а другая сторона об этом не знает. Разница заключается лишь в том, что приложение закрыло сокет, и нет никакого другого компонента, который мог бы разобраться с проблемой. В результате TCP-стек ждёт установленный период времени и закрывает соединение (ничего не посылая на другую сторону).

Вопрос вдогонку: что случится, если в этой ситуации kang отправит данные к kodos? Не забывайте, kang всё ещё считает, что соединение открыто, хотя на стороне kodos оно уже завершено.

Клиент

2016-05-27T08:40:12Z: entering poll()
kodos, are you there?
2016-05-27T08:41:34Z: poll returned events 0x1/0x0
2016-05-27T08:41:34Z: reading from stdin
2016-05-27T08:41:34Z: writing 22 bytes read from stdin to socket
2016-05-27T08:41:34Z: entering poll()
2016-05-27T08:41:34Z: poll returned events 0x0/0x10
2016-05-27T08:41:34Z: reading from socket
dnc: read: Connection reset by peer

Это то же самое, что мы видели в Головоломке 1: write() успешно выполняется, так как TCP-стек ещё не знает, что соединение закрыто. Но затем идёт RST, который пробуждает находящийся в poll() тред, и последующий запрос read() возвращает ECONNRESET.

Выводы:

  • Возможна ситуация, когда обе стороны не сходятся во мнениях относительно статуса соединения, хотя при этом не было ошибки операционной системы, сети или железа.
  • В описанном выше случае kang не имеет возможности узнать, ожидает ли kodos получения данных от kang, или же kodos закрыл сокет и не прослушивает его (по крайней мере, не без отправки пакета). Поэтому не стоит проектировать систему, которая при нормальных условиях эксплуатации в течение длительного времени будет использовать сокеты в подобных полуоткрытых состояниях.

Заключение
TCP обычно представляется нам как протокол, который поддерживает абстракцию — «TCP-соединение» — между двумя системами. Мы знаем, что из-за некоторых программных или сетевых проблем соединение упадёт. Но не всегда очевидно, что бывают случаи возникновения сбоев самой абстракции, из-за чего системы расходятся во мнении по поводу состояния подключения. Например:
  • Одна система может считать, что у неё есть рабочее соединение с удаленной системой, которая, в свою очередь, ничего не знает об этом соединении.
  • Это может происходить без каких-либо ошибок операционной системы, сети или другого оборудования.

Такое поведение не говорит о недостатках TCP. Наоборот, в подобных случаях TCP ведёт себя наиболее разумно, с учетом ситуации. Если бы мы пытались реализовать свой собственный механизм передачи данных вместо TCP, то подобные случаи напомнили бы нам о том, насколько сложные проблемы могут возникнуть. Это внутренние проблемы, связанные с распределёнными системами, а TCP-подключение по сути является распределённой системой.

Тем не менее, наиболее важный урок, который можно вынести из всего этого, заключается в том, что понятие «TCP-соединения, охватывающего несколько систем» — это удобная фикция. Когда что-то идёт не так, очень важно, чтобы две разные машины одновременно пытались согласованное представление о соединении. Приложение начинает решать возникающие проблемы в тех случаях, когда машины действую по-разному (для этого часто используется механизм keep-alive).

Кроме того, дескриптор файла «оторван» от соответствующего TCP-соединения. Соединения существуют (в разных, связанных с закрытием состояниях) даже тогда, когда приложение закрыло дескриптор файла. А иногда дескриптор файла может быть открыт, хотя TCP-соединение было закрыто в результате ошибки.

Остальные уроки, о которых стоит помнить:

  • Неаккуратная перезагрузка системы (когда падает операционная система) — это не то же самое, что обычный выход или закрытие процесса. Важно тестировать этот случай отдельно. Перезагрузка, когда удалённая система возвращается в онлайн — это не то же самое, что отключение удалённой машины.
  • От ядра не поступает упреждающих сигналов, когда закрывается TCP-сокет. Вы можете узнать об этом, только вызывая read(), write(), или выполняя другие операции с дескриптором файла сокетом. Если ваша программа по какой-то причине этого не делает, то вы никогда не узнаете об ошибке соединения.

Некоторые малоизвестные моменты:

  • ECONNRESET — это ошибка сокета, которую мы можем получить от read(), write() и других операций. Она означает, что удалённый компьютер послал RST.
  • ETIMEDOUT — это ошибка сокета, которую можно получить от read(), write() и остальных операций. Она означает, что истёк некий таймаут, имеющий отношение к соединению. В большинстве случаев это происходит, когда удалённая сторона слишком долго не признаёт пакет. Обычно это пакеты данных, пакет FIN или сигнал KeepAlive.

Важно отметить, что эти ошибки не означают, будто что-то пошло не так с вашими операциями чтения или записи. Это лишь означает, что закрыт сам сокет.

Комментарии (0)

© Habrahabr.ru