Иногда я захожу почитать rsdn.ru. Из чистого любопытства. Иногда там кроме "жабобыдлокодерства" обсуждается что-нибудь интересное. Сегодня я натолкнулся на
тред про TCP.
Вопрос формулируется так: Программы обмениваются сообщениями через TCP. Сообщения короткие, с десяток байт. Возможно ли такое что на принимающей стороне recv вернёт меньшее количество байт чем размер сообщения?
Обсуждение не столько интересное, сколько показательное. Правильные ответы там, естественно, прозвучали, однако завязалась мутная дискуссия, в которой много чего перепуталось. Иными словами, в широких программистских массах есть определенное недопонимание того, что происходит и почему. Видимо, надо раскрыть тему для тех, кто не дочитал Стивенса.
Ничего нового я не расскажу, все это многократно описано.
Казалось бы, все давно усвоили, что TCP - byte stream протокол и полагаться на сохранение границы сообщения нельзя, однако все равно у людей возникает соблазн думать: "ну, мы будем посылать маленькие сообщения, nagle отключим, тогда сообщения будут приходить по одному на TCP сегмент и все будет круто". Так вот - круто не будет.
Во-первых, как обычно, в том треде первым делом перепутали гарантированный IP reassembly buffer размером 576 октетов и minimum IP MTU, который на самом деле составляет 68 октетов (RFC 791). Впрочем, это уже хорошая традиция, путать эти вещи. Соответственно, при использовании PMTUD те самые "маленькие сообщения" могут стать недостаточно маленькими и начать сегментироваться. Но это теоретическая опасность - я не могу припомнить ни одной ситуации, где бы я видел такой MTU вне лаборатории.
Во-вторых, всегда есть возможность, что в сети появится какой-нибудь application level proxy, какой-нибудь socks или что-нибудь из этой оперы, который TCP поток примет, пересоберет как ему понравилось и отправит дальше. Опасность вполне реальная, но оптимистами от сетевого программирования часто игнорируется.
А могут ли возникнуть неприятности в "нормальных условиях"? Т.е. в ситуации с реалистичным MTU, без каких-либо хитрых прокси и т.д. Да, могут. И воспроизвести такую ситуацию достаточно несложно.
Напишем пару простеньких програмулек: клиент и сервер. Клиент посылает по 17 байт с TCP_NODELAY, а сервер в свою очередь, читает по 17 байт. Оба печатают чего и сколько они послали или получили. Код приводить не буду - все тривиально.
Запускаем програмульки и смотрим tcpdump'ом:
13:38:00.067070 IP 172.16.13.1.1086 > 172.16.13.2.10000: S 854406314:854406314(0) win 5840
13:38:00.067144 IP 172.16.13.2.10000 > 172.16.13.1.1086: S 734559824:734559824(0) ack 854406315 win 5840
13:38:00.067322 IP 172.16.13.1.1086 > 172.16.13.2.10000: . ack 1 win 5840
13:38:00.067832 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 1:18(17) ack 1 win 5840
13:38:00.067859 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 18 win 5840
13:38:00.072556 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 18:35(17) ack 1 win 5840
13:38:00.072556 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 35 win 5840
13:38:00.080546 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 35:52(17) ack 1 win 5840
13:38:00.080798 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 52 win 5840
13:38:00.090530 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 52:69(17) ack 1 win 5840
13:38:00.090577 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 69 win 5840
13:38:00.101722 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 69:86(17) ack 1 win 5840
13:38:00.101752 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 86 win 5840
13:38:00.102886 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 86:103(17) ack 1 win 5840
13:38:00.123053 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 103:120(17) ack 1 win 5840
13:38:00.139038 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 120 win 5840
13:38:00.139301 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 120:137(17) ack 1 win 5840
13:38:00.141417 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 137:154(17) ack 1 win 5840
13:38:00.153849 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 154:171(17) ack 1 win 5840
13:38:00.189821 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 171 win 5840
Все честно. MSS нормальный, окошки хорошие, пакетики уходят с payload'ом по 17 байт.
Соответсвенно отправитель печатает "send 17 bytes", а приемник - "recv 17 bytes" много-много раз. Пока все нормально.
Теперь пусть отправитель шлет как можно быстрее, а приемник у нас будет помедленней и между чтениями спит по 50 миллисекунд. Ничего нереалистичного в этом нет. Приемник может жить на медленной машине, заниматься какой-то обработкой и т.п.
Естественно, при этом будет расти буфер сначала на приемнике:
dg@server:~$ netstat -ant | grep 10000
tcp 0 0 0.0.0.0:10000 0.0.0.0:* LISTEN
tcp 80045 0 172.16.13.2:10000 172.16.13.1:1086 ESTABLISHED
Потом приемник окошко прикроет:
13:38:57.500247 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 96944 win 0
Буфер начнет расти на отправителе:
dg@client:~$ netstat -ant | grep 10000
tcp 0 13540 172.16.13.1:1086 172.16.13.2:10000 ESTABLISHED
Что показывает tcpdump:
13:39:01.043581 IP 172.16.13.1.1086 > 172.16.13.2.10000: . ack 1 win 5840
13:39:01.043611 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 96944 win 0
13:39:04.929485 IP 172.16.13.1.1086 > 172.16.13.2.10000: . ack 1 win 5840
13:39:04.929746 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 96944 win 0
13:39:10.449838 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 96944 win 2144
13:39:10.450307 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 96944:97344(400) ack 1 win 5840
13:39:10.450310 IP 172.16.13.1.1086 > 172.16.13.2.10000: . 97344:98804(1460) ack 1 win 5840
13:39:10.450350 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 97344 win 1744
13:39:10.549359 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 98804 win 284
13:39:10.773453 IP 172.16.13.1.1086 > 172.16.13.2.10000: P 98804:99088(284) ack 1 win 5840
13:39:10.969739 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 99088 win 0
13:39:11.252583 IP 172.16.13.1.1086 > 172.16.13.2.10000: . ack 1 win 5840
13:39:11.253306 IP 172.16.13.2.10000 > 172.16.13.1.1086: . ack 99088 win 0
Zero window probes и silly window avoidance во всей красе. А отправитель, совершенно естественно, шлет уже не по 17 байт, а кусками побольше.
А теперь устроим небольшую аварию. Типа у нас временно сломалась сеть. Бывает такое? Да конечно бывает, что за вопрос:
dg@client:~$ sudo iptables -A OUTPUT --dst 172.16.13.2 -j DROP
Теперь приемник ничего не получает, а только разгребает свой буфер по 17 байт раз в 50мс. Разгребает, разгребает и ...:
...
recv 17 bytes
recv 17 bytes
recv 1 bytes
Вот, собственно, и оно.
Теперь, если "аварию" устранить, то поток восстановится, и получатель опять сможет читать свои 17 байт. Но уже читать он будет полную херню, ибо весь поток сместился.
Итого, ответ на вопрос Возможно ли такое что на принимающей стороне recv вернёт меньшее количество байт чем размер сообщения? будет "Да, конечно, безусловно, непременно. А кто этого не понимает, будет бит по голове утяжеленным томом Стивенса."