[Перевод] Головоломки TCP
Говорят, что нельзя полностью понять систему, пока не поймёшь её сбои. Ещё будучи студентом я ради забавы написал реализацию 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 ~]#
Что случилось? Давайте разберемся:
- Осуществляя выход из процесса, мы отправили SIGINT на сервер. После выхода закрылись дескрипторы файлов.
- Когда закрывается последний дескриптор для сокета
ESTABLISHED
, стек TCP наkodos
отправляет через соединение FIN и переходит в состояниеFIN_WAIT_1
. - Стек TCP на
kang
получает пакет FIN, переводит собственное соединение в состояниеCLOSE_WAIT
и отправляет в ответ ACK. Пока клиентnc
блокирует сокет — он готов к чтению, ядро будит этот тред с помощьюPOLLIN
. - Клиент
nc
видитPOLLIN
для сокета и вызываетread
, который тут же возвращает 0. Это означает конец соединения.nc
решает, что мы закончили работу с сокетом, и закрывает его. - Тем временем, стек TCP на
kodos
получает ACK и переходит в состояниеFIN_WAIT_2
. - Пока клиент
nc
на kang закрывает свой сокет, стек TCP наkang
отправляет FIN наkodos
. Соединение наkang
переходит в состояниеLAST_ACK
. - Стек TCP на
kodos
получает FIN, соединение переходит в состояниеTIME_WAIT
, и стек наkodos
подтверждает FIN. - Стек TCP на
kang
получает ACK для FIN и полностью удаляет соединение. - Спустя две минуты соединение 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
не станет закрывать сокет в этой ситуации.
Что случится с установленным неактивным 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
не закроет файловый дескриптор.
Выводы:
- Жесткое отключение энергии сильно отличается от аккуратного выключения. При тестировании распределённых систем нужно отдельно проверять и этот сценарий. Не ждите, что всё будет так же, как и при обычной остановке процесса (kill).
- Бывают ситуации, когда одна сторона уверена, что TCP-соединение установлено, а другая — не уверена, и эта ситуация никогда не будет решена автоматически. Управлять решением таких проблем можно с использованием keep-alive для соединений на уровне приложения или TCP.
- Единственная причина, по которой
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 минут, в соответствии с настройками этой системы.Выводы:
- Когда удалённая система вместо перезапуска электропитания просто выключается, то первая система может узнать об этом, только отправив данные. В ином случае она никогда не узнает об обрыве соединения.
- Когда система слишком долго пытается отправить данные и не получает ответа, 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.
Важно отметить, что эти ошибки не означают, будто что-то пошло не так с вашими операциями чтения или записи. Это лишь означает, что закрыт сам сокет.