Головоломки 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 ~]# 

Що сталося? Давайте розберемося:

  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-from stream 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 read bytes 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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.