A1. Echo-сервер і клієнт
Echo-сервер повертає те, що отримав, і на вигляд це п’ять рядків коду.
На практиці на ньому вперше ламається уявлення про TCP як про «канал,
що доставляє повідомлення»: send на одному боці не дорівнює одному recv
на іншому, а UDP, навпаки, зберігає межі, але нічого не гарантує.
Модуль 3 показує, що сокет у ядрі є чергою
байтів у буфері, а модуль 11 пояснює, чому TCP не має
поняття повідомлення. Тут ви напишете обидва сервери й клієнт, а перевірка
покаже, що станеться, коли повідомлення приходить шматками, склеєним,
завеликим або від десятка клієнтів одразу.
Після цієї роботи ви зможете:
- написати TCP- і UDP-сервер на
socket,bind,listen,accept,recv,send,recvfrom,sendto; - читати й писати в TCP-сокет у циклі, не покладаючись на те, що один виклик обробить усі дані;
- обслуговувати кількох клієнтів одночасно через
forkабоpollі пояснити, чим ці підходи відрізняються; - закривати з’єднання так, щоб не втратити останні дані, не накопичувати дескриптори й зомбі, а порт після перезапуску лишався доступним;
- пояснити, чому UDP-датаграма приходить цілою або не приходить зовсім.
Цей самий цикл «читаємо частину, обробляємо, пишемо частину» лежить в основі будь-якого мережевого сервера, від проксі до брокера повідомлень. Помилка з частковим читанням у production виглядає як «іноді обрізається відповідь, а на тестах усе гаразд».
Завдання
Section titled “Завдання”Написати на C сервер echo-server і клієнт echo-client.
./echo-server 7007 # слухає TCP і UDP на порту 7007./echo-client tcp 127.0.0.1 7007 # stdin → сервер → stdout./echo-client udp 127.0.0.1 7007 # кожен рядок stdin — одна датаграмаСервер має вміти:
- приймати TCP-з’єднання і UDP-датаграми на одному номері порту, який передано першим аргументом;
- повертати TCP-клієнту рівно ті байти, які той надіслав, у тому самому
порядку, незалежно від того, як їх розбило
recv: довільні байти (зокрема нульові), шматки по одному байту, два «повідомлення» в одному сегменті, 2 МіБ за одне з’єднання; - тримати з’єднання відкритим, доки клієнт його не закриє: серія запитів і відповідей в одному з’єднанні працює;
- обслуговувати багатьох клієнтів одночасно; мовчазний клієнт, який підключився й нічого не шле, або клієнт, який шле багато й не читає відповідь, не зупиняє інших;
- після
shutdown(SHUT_WR)від клієнта повернути все, що ще не відправлено, і закрити з’єднання зі свого боку; - повертати кожну UDP-датаграму як окрему датаграму відправнику;
- не завершуватися, коли клієнт розірвав з’єднання (
RST,SIGPIPE); - не втрачати дескриптори й не накопичувати процеси-зомбі;
- запускатися на тому самому порту відразу після того, як попередній екземпляр було вбито з відкритим з’єднанням.
Клієнт має вміти:
- у режимі
tcpвідправляти весь stdin, одночасно друкувати у stdout те, що повернулось, а на кінці stdin закрити свою половину з’єднання (shutdown(SHUT_WR)) і дочекатися, поки сервер закриє свою; - у режимі
udpвідправляти кожен рядок stdin окремою датаграмою й виводити відповідь; якщо відповіді немає 2 секунди, написати про це в stderr і завершитись із ненульовим кодом; - при недоступному сервері завершуватись із повідомленням у stderr і ненульовим кодом.
Чого робити не треба. IPv6, TLS, epoll, ліміти на кількість клієнтів
і розбір протоколу поверх echo. Якщо цікавить epoll, його розібрано
в A7 курсу «Операційні системи».
Обмеження. C і системні виклики. Розбір рядків, printf("%s")
і strlen на даних з мережі заборонені: дані можуть містити нульові байти.
Перевірці потрібен python3, права root не потрібні.
Готово, коли:
./check.sh ./echo-server ./echo-clientпроходить усі перевірки;- ви можете пояснити, навіщо в сервері на
pollпотрібна черга на запис, а уfork-варіанті цикл навколоsend; - у звіті є відповіді на запитання з етапу 3.
Перед початком
Section titled “Перед початком”- Прочитайте в модулі 11, як TCP розрізає потік на сегменти і що означає поняття «межі повідомлень». У модулі 3 подивіться, де в ядрі лежать черги сокета.
- Розпакуйте архів курсу: перевірка лежить
у
labs/a1-echo/check.sh, поруч єMakefile. - Знадобляться
gcc,python3,ssіstrace. Підійде будь-який Linux, зокрема контейнер і WSL2; права root не потрібні.
-
UDP-сервер.
Найпростіше з усього:
socket(SOCK_DGRAM),bind, циклrecvfrom→sendtoна адресу відправника. Перевірте вручну:Terminal window echo hello | nc -u -w1 127.0.0.1 7007Надішліть датаграму більшу за буфер, який ви виділили, і подивіться, що залишиться від решти (підказка:
man 2 recv, розділ проMSG_TRUNC). -
TCP-сервер для одного клієнта.
socket(SOCK_STREAM),SO_REUSEADDR,bind,listen,accept, циклrecv→sendдоrecv == 0. Перевіртеnc 127.0.0.1 7007.Поставте
SO_REUSEADDR, а потім приберіть і спробуйте перезапустити сервер, поки клієнт ще підключений:ss -tan | grep 7007покажеTIME_WAIT, через якийbindвідмовляє без цього прапора. -
Межі повідомлень.
Запустіть
strace -e trace=recvfrom,sendto,read,writeна сервері й надішліть 2 МіБ:head -c 2M /dev/urandom | nc 127.0.0.1 7007 | wc -c. Подивіться, які значення повертаєrecvі чи збігаються вони зsendклієнта.Запишіть у звіт: скільки байтів читав за раз ваш сервер, чи бувало, що
sendзаписував менше, ніж просили, і за яких умов (блокуючий чи неблокуючий сокет) це стається. -
Багато клієнтів.
Оберіть один підхід.
forkна з’єднання: у дитиніclose(listen_fd), у батькаclose(conn_fd), а зомбі прибираєSIGCHLDізSA_NOCLDWAITабоsignal(SIGCHLD, SIG_IGN). Або один процес наpoll: сокети неблокуючі, а те, що не вдалося відправити одразу, лежить у черзі на запис, покиpollне скажеPOLLOUT. Тоді читання з цього клієнта краще призупинити, щоб черга не росла без меж.Чекер приймає обидва підходи. Обидва можна реалізувати, і друга реалізація дасть вам відчуття, де межа між ними.
-
Закриття.
Обробіть
recv == 0(клієнт закрив свою половину),ECONNRESETіSIGPIPE. Переконайтеся черезls /proc/<pid>/fd | wc -l, що після сотні з’єднань число дескрипторів не зросло. -
Клієнт.
TCP: цикл на
pollза stdin і сокетом, щоб велике введення не зависло наwrite, поки сервер чекає, що його відповідь хтось прочитає. На кінці stdinshutdown(SHUT_WR)і читання до кінця потоку. UDP:getline,send,pollіз таймаутом на 2 секунди,recv.
Перевірка
Section titled “Перевірка”cd labs/a1-echomake./check.sh ./echo-server ./echo-clientДругий аргумент необов’язковий: якщо його немає, але поруч лежить
./echo-client, перевіряється він. Чекер сам вибирає вільний порт, запускає
ваш сервер і перевіряє:
- TCP як потік байтів: короткий рядок, усі 256 значень байта, 2 МіБ, шматки
з паузами, тисячу однобайтових
send, два повідомлення в одному сегменті, серію повідомлень в одному з’єднанні; - кількох клієнтів: мовчазні, тридцять одночасних, той, що не читає;
- закриття:
shutdown(SHUT_WR),RST, витік дескрипторів і зомбі; - UDP: датаграми від 1 до 4000 байтів, збереження меж, відповідь саме відправнику;
- перезапуск на тому самому порту одразу після
kill -9; - клієнт: проти власного echo-сервера чекера, з великим введенням, порожнім введенням і відсутнім сервером.
Скрипт не залежить від того, fork у вас чи poll, на яких адресах ви
слухаєте і що друкуєте в stdout. Він нічого не виставляє й нікуди нічого
не надсилає.
Часті помилки
Section titled “Часті помилки”Один recv — одне повідомлення. На loopback і коротких рядках це
виглядає правдою. Проте 2 МіБ, або клієнт, який пише по байту, або
мережа з нормальною затримкою роздрібнять потік так, як ви не очікували.
Кордони повідомлень у TCP мусить задавати ваш протокол (довжина, роздільник),
а echo просто не має права їх вигадувати.
send записав менше, ніж просили. Для блокуючого сокета це
трапляється рідко, для неблокуючого є нормою. У сервері на poll невідправлений
залишок обов’язково треба зберегти.
strlen і %s. Перший нульовий байт обрізає «рядок». Echo працює
з масивом байтів і його довжиною з recv.
Порт зайнятий після перезапуску. Не виставлено SO_REUSEADDR. Подивіться
ss -tan state time-wait.
Сервер вмирає з «Broken pipe». Клієнт пішов, а ви пишете в закритий
сокет: прийшов SIGPIPE. Ігноруйте його або передавайте MSG_NOSIGNAL
і дивіться на EPIPE.
Зомбі у fork-версії. Батько не збирає дітей. Виставте
signal(SIGCHLD, SIG_IGN) або обробник із waitpid(-1, ..., WNOHANG).
Клієнт зависає на великому введенні. Він пише все, не читаючи відповідь,
а сервер, що чекає читача, перестає читати. Обидва заблоковані на write.
Клієнтові потрібен poll або окремий потік.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Додати в сервер epoll (A7 курсу ОС)
і порівняти на 10 000 з’єднань. Поставити між клієнтом і сервером
tc netem із втратами (не в кожному контейнері він працює, див.
огляд) і подивитися в tcpdump, як TCP повторює сегменти,
а UDP ні. Зробити UDP-клієнт, який сам повторює запит, якщо відповіді
немає, і зрозуміти, чому з цього виростає A4.