TCP/UDP, модель OSI, сокеты и WebSocket
Кратко о теме
Заголовок раздела «Кратко о теме»Этот блок вопросов почти целиком строится вокруг одной модели в голове: между вашим http.Client и чужим сервером лежит стек, где каждый уровень решает ровно одну задачу и ничего не знает о содержимом уровня выше. IP отвечает за то, чтобы пакет доехал от одного хоста до другого через маршрутизаторы — без гарантий доставки, без порядка, с возможной фрагментацией. Транспортный уровень поверх него решает две новые задачи: демультиплексирование (какому именно процессу на хосте адресован этот кусок данных — отсюда порты) и, опционально, надёжность. UDP делает только первое и добавляет 8 байт заголовка. TCP делает и второе — и платит за это соединением, состояниями, буферами и заголовком минимум в 20 байт.
Практически все различия TCP и UDP выводятся из одного решения: TCP — это надёжный поток байт с установленным соединением, UDP — ненадёжная передача отдельных датаграмм без соединения. Из «надёжности» следуют номера последовательности, подтверждения, ретрансмиссии и таймеры; из «потока» — то, что границы сообщений теряются и вам нужен свой фрейминг; из «соединения» — рукопожатие, состояние на обеих сторонах и упорядоченное закрытие; из необходимости не топить сеть и получателя — два независимых механизма ограничения скорости: окно приёмника (rwnd, защита получателя) и окно перегрузки (cwnd, защита сети). В UDP нет ничего из этого, поэтому его выбирают там, где либо задержка важнее полноты (голос, видео, игры), либо надёжность строится своя поверх (QUIC, WireGuard, кастомные протоколы), либо обмен настолько короткий, что соединение — чистые накладные расходы (DNS, NTP, syslog).
Вторая половина блока — модель OSI и её роль в разговоре. На собеседовании OSI используется как общий словарь: сказать «TCP — четвёртый уровень», «балансировщик L4 против L7», «TLS — это между 4 и 7, формально представление». Важно понимать, что OSI — эталонная семиуровневая модель ISO/IEC 7498-1, а реальный интернет живёт по более грубой модели TCP/IP из четырёх уровней; попытка честно разложить HTTPS или WebSocket по семи уровням всегда упирается в натяжки, и хороший кандидат это проговаривает.
Третья тема, которая почти всегда всплывает рядом, — сокеты и то, как всё это выглядит из кода. Сокет — абстракция ОС над транспортом: файловый дескриптор, к которому привязаны домен (AF_INET/AF_INET6/AF_UNIX), тип (SOCK_STREAM/SOCK_DGRAM/SOCK_RAW) и состояние. В Go это спрятано за net.Listener/net.Conn, а планировщик поверх epoll/kqueue делает так, что блокирующий с виду conn.Read паркует горутину, а не поток ОС. Отсюда же вырастает WebSocket-часть блока: WebSocket — это обычное TCP-соединение, поднятое через HTTP-Upgrade, которое затем живёт часами, и все вопросы про него на самом деле про управление тысячами долгоживущих соединений и про то, как найти нужное.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Писали WebSocket с нуля или использовали Centrifugo? Почему?
Заголовок раздела «Писали WebSocket с нуля или использовали Centrifugo? Почему?»Коротко. Это вопрос про инженерное решение «строить или взять», а не про факт. Правильная структура ответа: назвать реальный выбор, объяснить его через требования (сколько соединений, нужны ли presence/history/восстановление после разрыва, кто клиенты, есть ли команда на поддержку), и честно перечислить, что вы получили и чем заплатили.
Глубже. Каркас хорошего ответа. Своя реализация поверх библиотеки (github.com/gorilla/websocket или github.com/coder/websocket, бывший nhooyr.io/websocket) оправдана, когда логика соединения тесно вплетена в домен, поток событий небольшой и предсказуемый, а зоопарк готовых фич не нужен: вы получаете полный контроль над фреймингом, авторизацией, бэкпрешером и метриками, но обязаны сами написать ping/pong и детект мёртвых соединений, ограничение размера сообщения, ретраи и восстановление пропущенных событий, шардирование по нодам и graceful shutdown. Centrifugo — отдельный сервер реального времени, который даёт из коробки каналы и подписки, JWT-аутентификацию, presence, history с восстановлением пропущенных сообщений после reconnect, несколько транспортов (WebSocket, SSE, HTTP-streaming, WebTransport) и масштабирование через Redis; ваш бэкенд просто публикует событие по HTTP/GRPC API и не держит соединения вообще. Цена — ещё один компонент в проде, свой протокол и клиентские SDK, ограниченная гибкость там, где нужна нетривиальная логика на соединении.
Что должно прозвучать обязательно: критерий выбора, а не вкусовщина. Например: «до нескольких тысяч соединений, события односторонние, гарантии at-most-once устраивают — написали сами на gorilla/websocket, hub + Redis Pub/Sub для межнодовой рассылки; если бы понадобились history и presence на сотнях тысяч подключений, тащить это самому было бы дороже, чем поднять Centrifugo». Типичная ошибка — ответ «написали сами, потому что не хотелось лишней зависимости»: интервьюер сразу спросит про reconnect и потерю событий, и станет видно, что задачу не додумали.
Как организовать поиск и управление WebSocket-соединениями для отправки события конкретному клиенту?
Заголовок раздела «Как организовать поиск и управление WebSocket-соединениями для отправки события конкретному клиенту?»Коротко. Внутри процесса — реестр (hub): шардированная map[userID]map[connID]*Client под RWMutex, у каждого клиента своя горутина-писатель и буферизованный канал send chan []byte, чтобы писать в сокет строго из одного места. Между процессами — либо шина (Redis Pub/Sub, NATS, Kafka) с рассылкой события всем нодам, либо реестр «userID → нода» во внешнем хранилище и точечная доставка на нужную ноду.
Глубже. Ключевые инварианты. В один *websocket.Conn нельзя писать из двух горутин одновременно — отсюда паттерн «одна горутина читает, одна пишет», а вся отправка идёт через канал. Канал обязан быть буферизованным и с политикой на переполнение: медленный клиент не должен блокировать publisher — при заполненном буфере соединение закрывают (default: close(c.send)), иначе получите утечку памяти и залипшие горутины. У пользователя может быть несколько соединений (вкладки, устройства), поэтому вложенная map, а не map[userID]*Client. Один глобальный мьютекс на сотнях тысяч соединений становится точкой контеншена — шардируйте по userID % N.
package hub
import ( "sync"
"github.com/gorilla/websocket")
type Client struct { UserID int64 Conn *websocket.Conn Send chan []byte}
type shard struct { mu sync.RWMutex users map[int64]map[*Client]struct{}}
type Hub struct{ shards [64]*shard }
func New() *Hub { h := &Hub{} for i := range h.shards { h.shards[i] = &shard{users: make(map[int64]map[*Client]struct{})} } return h}
func (h *Hub) shardFor(userID int64) *shard { return h.shards[uint64(userID)%uint64(len(h.shards))]}
func (h *Hub) Add(c *Client) { s := h.shardFor(c.UserID) s.mu.Lock() defer s.mu.Unlock() if s.users[c.UserID] == nil { s.users[c.UserID] = make(map[*Client]struct{}) } s.users[c.UserID][c] = struct{}{}}
func (h *Hub) Remove(c *Client) { s := h.shardFor(c.UserID) s.mu.Lock() defer s.mu.Unlock() if set, ok := s.users[c.UserID]; ok { delete(set, c) if len(set) == 0 { delete(s.users, c.UserID) } }}
// SendTo кладёт сообщение во все соединения пользователя, не блокируясь на медленных.func (h *Hub) SendTo(userID int64, msg []byte) { s := h.shardFor(userID) s.mu.RLock() defer s.mu.RUnlock() for c := range s.users[userID] { select { case c.Send <- msg: default: // клиент не успевает — рвём соединение, писатель увидит закрытый канал _ = c.Conn.Close() } }}Межнодовая часть. Самый простой рабочий вариант: каждая нода подписана на общий канал Redis/NATS, событие публикуется один раз, ноды фильтруют по своему реестру — просто, но трафик рассылки растёт линейно от числа нод. Точечный вариант: в Redis хранится SADD ws:user:{id} {nodeID} с TTL и heartbeat, publisher читает набор нод и шлёт только туда — экономнее, но нужно чистить протухшие записи при падении ноды. Sticky-сессии на балансировщике (по cookie или consistent hash от userID) уменьшают разброс, но ломаются при масштабировании и деплое. Обязательные дополнения к любому варианту: ping/pong с SetReadDeadline/SetPongHandler для детекта мёртвых TCP-соединений (без этого «полуоткрытые» соединения живут часами), SetReadLimit, монотонный seq на событиях и восстановление пропущенного при reconnect, graceful shutdown с рассылкой Close-фрейма 1001.
В чем отличия TCP/UDP?
Заголовок раздела «В чем отличия TCP/UDP?»Коротко. TCP — соединение, надёжная доставка с подтверждениями и ретрансмиссиями, гарантированный порядок, поток байт без границ сообщений, управление потоком и перегрузкой, заголовок от 20 байт. UDP — без соединения, доставка без гарантий и без порядка, датаграмма как единица (границы сохраняются), никакого контроля перегрузки, заголовок 8 байт. Отсюда всё остальное: TCP — там, где важна целостность данных, UDP — там, где важна задержка или нужен свой транспорт поверх.
Глубже. Разложу по осям, каждую из которых интервьюер может углубить.
Соединение. TCP — трёхстороннее рукопожатие, состояние на обеих сторонах (конечный автомат: LISTEN, SYN-SENT, ESTABLISHED, FIN-WAIT-1/2, TIME-WAIT…), упорядоченное закрытие четырьмя сегментами. UDP состояния не имеет: sendto просто отправляет датаграмму, ядро ничего не помнит. Поэтому UDP-сервер тривиально масштабируется и переживает рестарт, а TCP-сервер держит килобайты памяти на каждое соединение.
Надёжность. В TCP каждый байт нумерован, получатель шлёт кумулятивные ACK (плюс SACK по RFC 2018), отправитель хранит неподтверждённые данные и повторяет их по RTO или по трём дублирующим ACK (fast retransmit). В UDP потерянная датаграмма просто исчезает — приложение либо не заметит, либо восстановит само.
Порядок и границы. TCP — байтовый поток: два Write по 100 байт могут прийти одним Read на 200 или тремя по 60/60/80, поэтому нужен свой фрейминг (длина-префикс, разделитель, HTTP Content-Length). UDP сохраняет границы: один WriteTo = одна датаграмма = один ReadFrom, но порядок между датаграммами не гарантирован.
Скорость передачи. TCP имеет управление потоком (окно приёмника, чтобы не переполнить буфер получателя) и управление перегрузкой (cwnd, slow start, CUBIC по умолчанию в Linux, опционально BBR) — соединение само подстраивается под сеть. UDP шлёт ровно столько, сколько шлёт приложение; при отсутствии собственного контроля перегрузки это может убить канал, поэтому серьёзные UDP-протоколы (QUIC, RTP + RTCP) реализуют контроль сами.
Разное полезное. Контрольная сумма в TCP обязательна, в UDP над IPv4 опциональна (нулевое поле = не считалась), над IPv6 обязательна. TCP умеет только unicast, UDP — ещё broadcast и multicast. Максимальный практический размер UDP-датаграммы без IP-фрагментации — около 1472 байт при MTU 1500 (или ~508 байт, если нужна гарантия на любом пути); TCP сам режет данные по MSS. В Go эта разница видна в API: net.Conn (Read/Write) для TCP против *net.UDPConn (ReadFromUDP/WriteToUDP) для UDP.
Какие протоколы прикладного уровня обеспечивают TCP и UDP?
Заголовок раздела «Какие протоколы прикладного уровня обеспечивают TCP и UDP?»Коротко. Поверх TCP: HTTP/1.1 и HTTP/2, TLS/HTTPS, WebSocket, SSH, SMTP/IMAP/POP3, FTP, LDAP, BGP, wire-протоколы баз данных (PostgreSQL, MySQL, Redis RESP, MongoDB), AMQP, Kafka protocol, gRPC. Поверх UDP: DNS (обычно), DHCP, NTP, SNMP, TFTP, syslog, RTP/RTCP, QUIC (а значит и HTTP/3), WireGuard, VXLAN, mDNS, RADIUS, STUN.
Глубже. Важно уметь объяснить, почему протокол выбрал тот или иной транспорт. DNS-запрос — это один короткий вопрос и один короткий ответ; поднимать ради этого соединение с рукопожатием дороже самого обмена, поэтому UDP; но при ответе больше 512 байт (или при EDNS0 больше согласованного размера) выставляется флаг TC и клиент переспрашивает по TCP, а зонные трансферы AXFR идут только по TCP. NTP и SNMP — то же самое: короткий обмен, потеря не страшна, повторим. RTP для голоса и видео — потерянный кадр бессмысленно ретранслировать, к моменту прибытия он уже не нужен, а вот задержка от head-of-line blocking убила бы разговор.
Отдельно стоит QUIC (RFC 9000): он работает поверх UDP, но реализует надёжность, порядок в рамках потока, контроль перегрузки и встроенный TLS 1.3 сам, в userspace. Причины — возможность обновлять транспорт без правки ядер и middleboxes, отсутствие head-of-line blocking между независимыми стримами (в HTTP/2 поверх TCP потеря одного сегмента тормозит все стримы) и миграция соединения между IP по Connection ID. Это хороший ответ на провокацию «UDP же ненадёжный, как на нём работает HTTP/3». Ещё пара протоколов умеет оба транспорта: SIP, LDAP (CLDAP по UDP), а Kubernetes-сервисы вообще заставляют помнить, что protocol: UDP требует отдельной настройки.
Что такое сокеты и какими они бывают?
Заголовок раздела «Что такое сокеты и какими они бывают?»Коротко. Сокет — предоставляемая ОС абстракция конечной точки связи: файловый дескриптор, через который приложение читает и пишет данные. Задаётся тройкой «домен (семейство адресов) — тип — протокол»: например AF_INET + SOCK_STREAM + TCP, AF_INET + SOCK_DGRAM + UDP, AF_UNIX + SOCK_STREAM для локального IPC.
Глубже. По домену: AF_INET (IPv4), AF_INET6 (IPv6), AF_UNIX/AF_LOCAL (локальные, адрес — путь в ФС или abstract namespace в Linux; быстрее TCP на localhost, умеют передавать файловые дескрипторы и учётные данные процесса через SCM_RIGHTS/SCM_CREDENTIALS), AF_PACKET (сырые кадры канального уровня, нужен CAP_NET_RAW), AF_NETLINK (общение с ядром Linux), AF_VSOCK (хост ↔ виртуалка). По типу: SOCK_STREAM (надёжный упорядоченный поток байт — TCP), SOCK_DGRAM (датаграммы — UDP), SOCK_RAW (свой заголовок IP/ICMP, нужен для ping и traceroute), SOCK_SEQPACKET (надёжные упорядоченные датаграммы с сохранением границ — SCTP, AF_UNIX). По роли TCP-сокеты делятся на слушающие (после bind + listen, у них две очереди — SYN queue для полуоткрытых и accept queue для готовых, somaxconn) и присоединённые (результат accept или connect, идентифицируются 4-кортежем src IP:port — dst IP:port).
В Go системные вызовы спрятаны: net.Listen("tcp", ":8080") — это socket+bind+listen, ln.Accept() — accept, net.Dial — socket+connect. Все сокеты по умолчанию переводятся в неблокирующий режим и регистрируются в netpoller (epoll на Linux, kqueue на BSD), поэтому «блокирующий» conn.Read на самом деле паркует горутину, а не поток ОС — отсюда дешёвая модель «горутина на соединение». Достучаться до дескриптора можно через syscall.Conn/RawConn (например, чтобы выставить SO_REUSEPORT или TCP_NODELAY через x/sys/unix), но выдёргивать fd через File() не стоит: это переводит сокет в блокирующий режим и выводит его из netpoller.
TCP vs UDP?
Заголовок раздела «TCP vs UDP?»Коротко. См. выше про отличия TCP/UDP: TCP — соединение, надёжность, порядок, поток байт, контроль перегрузки; UDP — датаграммы без соединения и гарантий, минимум накладных расходов.
Глубже. Если вопрос задан в такой короткой форме, интервьюер обычно ждёт, что вы структурируете ответ сами. Удобный способ — пройтись по заголовкам. UDP-заголовок 8 байт: порт источника, порт назначения, длина, контрольная сумма — и это буквально всё, что протокол умеет: демультиплексирование и проверка целостности. TCP-заголовок 20 байт без опций: те же два порта, но дальше — 32-битный sequence number, 32-битный acknowledgment number, data offset, зарезервированные биты, восемь флагов (CWR, ECE, URG, ACK, PSH, RST, SYN, FIN), 16-битное окно, контрольная сумма, urgent pointer и опции (MSS, Window Scale, SACK-Permitted, Timestamps). Каждое поле — это ответ на вопрос «а как TCP делает X»: sequence и ack — надёжность и порядок, window — управление потоком, флаги — конечный автомат соединения, опции — расширения, договариваемые в рукопожатии. Показать эту связку «поле → функция» ценнее, чем перечислить сравнение по памяти.
Чем отличаются и когда используются протоколы TCP, UDP?
Заголовок раздела «Чем отличаются и когда используются протоколы TCP, UDP?»Коротко. Отличия — см. выше. Критерий выбора: если потеря байта делает данные бесполезными (файл, платёж, SQL-запрос, HTML) — TCP; если ценность данных быстро истекает или надёжность строится своя (голос, видео, телеметрия, игры, DNS, QUIC) — UDP.
Глубже. Практический список сценариев. TCP: веб (HTTP/1.1, HTTP/2), API и gRPC, базы данных и брокеры (Postgres, Redis, Kafka), передача файлов, SSH, почта, репликация — всё, где нужен ровно тот же байтовый поток на другом конце. UDP: DNS и другие короткие «вопрос-ответ» обмены, NTP, метрики в StatsD (потерять один семпл дешевле, чем блокировать приложение), логи по syslog, VoIP и видеоконференции (RTP), онлайн-игры (позиция игрока через 200 мс уже неактуальна, ретрансмиссия вредна), обнаружение сервисов через multicast/mDNS, VPN-туннели (WireGuard), оверлейные сети (VXLAN, Geneve), QUIC/HTTP/3.
Важный нюанс, который стоит проговорить: «UDP быстрее TCP» — неточная формулировка. Скорость передачи одного пакета одинакова, разница в том, что TCP добавляет задержку установления соединения (1 RTT, плюс 1–2 RTT на TLS) и задержку на восстановление после потерь (head-of-line blocking: полученные данные лежат в буфере ядра и не отдаются приложению, пока не приедет пропущенный сегмент). Именно вторую проблему UDP-протоколы вроде QUIC и RTP решают, а не «отсутствие оверхеда» — их заголовки часто больше TCP-шного. И обратно: в датацентре с потерями около нуля TCP по пропускной способности обычно выигрывает у наивного UDP-протокола, потому что за него уже написали cwnd, SACK, pacing и оффлоады в NIC (TSO/GRO).
What is TCP window size?
Заголовок раздела «What is TCP window size?»Коротко. Window size — 16-битное поле в TCP-заголовке, в котором получатель сообщает отправителю, сколько ещё байт тот может послать сверх последнего подтверждённого, не дожидаясь ACK. Это управление потоком (flow control): защита буфера получателя. Максимум в самом поле — 65535 байт, но опция Window Scale (RFC 7323) умножает его на 2^n (n до 14), что даёт окно почти до 1 ГБ.
Глубже. Ключевое, что нужно развести в ответе: окон два, и они разные. rwnd — то самое поле в заголовке, его объявляет получатель исходя из свободного места в приёмном буфере. cwnd — окно перегрузки, оно не передаётся по сети вообще, живёт только у отправителя и оценивает, сколько сеть готова принять без потерь (slow start с экспоненциальным ростом, затем congestion avoidance; в Linux по умолчанию CUBIC, альтернатива — BBR). Фактически в полёте может находиться min(rwnd, cwnd) байт. Вопрос «почему соединение медленное, хотя канал широкий» чаще всего решается через BDP = пропускная способность × RTT: для 1 Гбит/с и RTT 100 мс это ~12.5 МБ, и без масштабирования окна вы упрётесь в 65535 байт за RTT, то есть примерно 5 Мбит/с, сколько бы полосы ни было.
Window Scale согласуется только в SYN и SYN-ACK — задним числом включить нельзя; если один из middleboxes вырежет опцию, соединение деградирует. В Linux размеры буферов задаются net.ipv4.tcp_rmem/tcp_wmem (min/default/max) и автоматически тюнятся при tcp_moderate_rcvbuf=1; явный SO_RCVBUF отключает автотюнинг, поэтому руками его лучше не трогать. Отдельные эффекты, которые полезно упомянуть: zero window — получатель объявил окно 0 (приложение не читает из сокета), отправитель переходит на zero-window probes и ждёт window update; silly window syndrome — обмен крошечными окнами, лечится алгоритмом Кларка на стороне получателя и алгоритмом Нагла на стороне отправителя. В Go напрямую окно не настраивается, но (*net.TCPConn).SetReadBuffer/SetWriteBuffer меняют размеры буферов сокета, а SetNoDelay(false) включает Нагла (по умолчанию Go его выключает, то есть TCP_NODELAY включён).
TCP - это протокол какого уровня модели OSI?
Заголовок раздела «TCP - это протокол какого уровня модели OSI?»Коротко. Транспортного — четвёртого (L4). В модели TCP/IP это тоже транспортный уровень, второй снизу из четырёх.
Глубже. Ответ стоит дополнить одной фразой про то, что даёт этот уровень: адресацию до процесса (порты, демультиплексирование) и сквозную (end-to-end) доставку между хостами, тогда как сетевой уровень L3 (IP) отвечает за доставку между узлами и маршрутизацию, а канальный L2 (Ethernet) — за передачу кадра в пределах одного сегмента. Соседи TCP по уровню: UDP, SCTP, DCCP. Часто следом спрашивают про QUIC — формально это транспортный протокол, но реализованный поверх UDP в пространстве пользователя, поэтому «L4 поверх L4»; и про TLS — его обычно относят к L5/L6, а на практике говорят «между транспортом и приложением». Практическое применение этой терминологии — балансировщики: L4-балансировщик (LVS/IPVS, AWS NLB) распределяет соединения по 4-кортежу и не смотрит в содержимое, L7-балансировщик (nginx, Envoy, ALB) разбирает HTTP и маршрутизирует по хосту, пути и заголовкам.
Как запустить два приложения на одном порту? Возможно ли это и какие есть способы?
Заголовок раздела «Как запустить два приложения на одном порту? Возможно ли это и какие есть способы?»Коротко. Да, несколькими способами. Самый прямой — SO_REUSEPORT (Linux 3.9+): несколько процессов биндятся на один и тот же адрес:порт, ядро само распределяет входящие соединения между ними по хешу 4-кортежа. Альтернативы: бинд на разные IP одного хоста, разные протоколы (TCP и UDP на порту 8080 — это два разных сокета), передача одного слушающего дескриптора между процессами (systemd socket activation, graceful restart), и реверс-прокси, который слушает один порт и раздаёт по Host/SNI/пути.
Глубже. Важно не путать SO_REUSEADDR и SO_REUSEPORT. SO_REUSEADDR в Linux позволяет забиндиться на порт, где висят сокеты в TIME_WAIT, и биндиться на конкретные разные адреса — но два процесса на один и тот же 0.0.0.0:8080 он не пустит. SO_REUSEPORT — именно про несколько слушающих сокетов на идентичном адресе; для безопасности ядро требует, чтобы все они принадлежали одному пользователю (иначе чужой процесс мог бы «подхватывать» ваш трафик). Балансировка по умолчанию — хеш от 4-кортежа; при добавлении/удалении слушателя раскладка меняется и часть соединений в accept-очереди уходящего сокета может быть сброшена, что портит идею zero-downtime deploy. Более тонкое управление даёт SO_ATTACH_REUSEPORT_EBPF.
package netx
import ( "context" "net" "syscall"
"golang.org/x/sys/unix")
func listenReusePort(addr string) (net.Listener, error) { lc := net.ListenConfig{ Control: func(network, address string, c syscall.RawConn) error { var serr error if err := c.Control(func(fd uintptr) { serr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) }); err != nil { return err } return serr }, } return lc.Listen(context.Background(), "tcp", addr)}Остальные способы. Разные адреса: один процесс на 127.0.0.1:8080, другой на 10.0.0.5:8080 — конфликта нет, потому что сокеты идентифицируются полным адресом; при этом бинд на 0.0.0.0:8080 заблокирует оба. Наследование дескриптора: родитель открывает слушающий сокет и передаёт fd потомку (exec с сохранением fd или через AF_UNIX + SCM_RIGHTS) — так работают zero-downtime рестарты у nginx и библиотек вроде cloudflare/tableflip, и так же systemd socket activation передаёт готовый сокет в LISTEN_FDS. L7-мультиплексирование: nginx/Traefik/Envoy на 443, дальше по SNI или Host в разные апстримы; сюда же трюк с определением протокола по первым байтам (soheilhy/cmux), позволяющий отдать gRPC и обычный HTTP с одного порта. Что точно не сработает без опций: два обычных net.Listen("tcp", ":8080") — второй получит bind: address already in use (EADDRINUSE).
Сколько можно одновременно создать tcp соединений c одного хоста?
Заголовок раздела «Сколько можно одновременно создать tcp соединений c одного хоста?»Коротко. Соединение идентифицируется 4-кортежем (src IP, src port, dst IP, dst port), поэтому теоретического лимита «на хост» нет — ограничение возникает только на пару «один клиентский IP → один конкретный сервер IP:port» и равно числу свободных эфемерных портов, в Linux по умолчанию около 28 000 (net.ipv4.ip_local_port_range = 32768–60999). К разным адресатам с того же хоста можно открыть кратно больше — упрётесь в лимит файловых дескрипторов и память.
Глубже. Разбирать надо отдельно клиента и сервер.
Клиент. Лимит эфемерных портов действует только в рамках одного назначения: 28k соединений к 10.0.0.1:5432 и ещё 28k к 10.0.0.2:5432 — это нормально. Расширить: увеличить ip_local_port_range (до ~64510 портов), добавить исходящих IP-адресов и раскладывать соединения по ним, использовать IP_BIND_ADDRESS_NO_PORT (позволяет ядру выбрать порт с учётом адреса назначения, когда вы явно биндите src IP), включить net.ipv4.tcp_tw_reuse=1 для переиспользования сокетов в TIME_WAIT для исходящих соединений. Про TIME_WAIT: сторона, инициировавшая закрытие, держит сокет 2×MSL (в Linux фиксированные 60 секунд), и при высоком темпе коротких соединений именно это, а не сам лимит портов, съедает диапазон — правильное лечение обычно keep-alive и пул соединений, а не выкручивание sysctl. Отдельно: tcp_tw_recycle из ядра удалён (был опасен за NAT), советовать его нельзя.
Сервер. На одном слушающем порту сервер может обслуживать сотни тысяч и миллионы соединений: кортеж различается клиентским IP и портом. Реальные потолки — ulimit -n / fs.file-max (дескрипторы), память ядра на сокет и буферы (в сумме единицы-десятки КБ на соединение, tcp_mem, tcp_rmem, tcp_wmem), размер accept-очереди (somaxconn, backlog в listen), таблица conntrack, если на пути NAT или iptables (nf_conntrack_max), и CPU на epoll и переключения. Классическая формулировка «C10K/C10M» — про то, что архитектурный предел давно не в протоколе. В Go добавляется свой множитель: горутина на соединение стоит от 8 КБ стека плюс буферы bufio в net/http (по 4 КБ на чтение и запись), поэтому миллион WebSocket-соединений на одной ноде на стандартном стеке не поднять — придётся идти в epoll вручную или разносить по нодам. Хороший ответ обязательно называет 4-кортеж как источник ответа, а не голые цифры.
Чем отличается UDP от TCP?
Заголовок раздела «Чем отличается UDP от TCP?»Коротко. См. выше про отличия TCP/UDP. Со стороны UDP формулировка такая: UDP — это тонкий слой над IP, добавляющий только порты и контрольную сумму; всё остальное (надёжность, порядок, дедупликация, контроль скорости, фрагментация на уровне приложения) остаётся вашей задачей.
Глубже. Что именно приходится дописывать, если вы выбираете UDP, — полезный список, отличающий предметный ответ от заученного. Нужны собственные идентификаторы сообщений и дедупликация (датаграмма может прийти дважды — например при ретрансмиссии на маршрутизаторе), собственные таймауты и повторы, собственный контроль перегрузки или хотя бы rate limiting, обработка переупорядочивания, и учёт MTU: датаграмма больше пути MTU будет фрагментирована на IP-уровне, а потеря одного фрагмента убивает всю датаграмму, поэтому серьёзные протоколы выставляют DF и делают PMTUD, ограничиваясь ~1200 байтами (так поступает QUIC). Ещё один UDP-специфичный класс проблем — amplification-атаки: сервер, отвечающий на маленький запрос большим ответом (DNS, NTP monlist, memcached), становится усилителем DDoS, потому что source IP в UDP не проверен рукопожатием. Поэтому QUIC ограничивает объём ответа неподтверждённому клиенту тройным размером полученного и делает Retry-токены.
В структре TranscationService какой есть еще способы работы с объектами AccountRepository и MessageBroker?
Заголовок раздела «В структре TranscationService какой есть еще способы работы с объектами AccountRepository и MessageBroker?»Коротко. Вопрос — обрывок обсуждения live-coding задачи, но восстановимая суть очевидна: интервьюер спрашивает про способы связывания сервиса с зависимостями. Правильный ответ — зависимости объявляются как интерфейсы, определённые на стороне потребителя (в пакете сервиса), и внедряются через конструктор; альтернативы — передача через параметры метода, функциональные опции, генерация/контейнер DI (wire, fx) и, как антипаттерн, глобальные переменные и создание зависимостей внутри сервиса.
Глубже. В Go идиоматичный вариант такой: пакет transaction объявляет минимальные интерфейсы под свои нужды (AccountRepository с двумя-тремя методами, MessageBroker с Publish), а конкретные *postgres.AccountRepo и *kafka.Producer их удовлетворяют неявно, ничего про сервис не зная. Это даёт подмену в тестах (фейк вместо мока), обратное направление зависимостей (домен не импортирует инфраструктуру) и узкие интерфейсы вместо «жирного» репозитория.
package transaction
import "context"
type AccountRepository interface { Balance(ctx context.Context, accountID int64) (int64, error) Apply(ctx context.Context, accountID, delta int64) error}
type MessageBroker interface { Publish(ctx context.Context, topic string, payload []byte) error}
type Service struct { repo AccountRepository broker MessageBroker}
func NewService(repo AccountRepository, broker MessageBroker) *Service { return &Service{repo: repo, broker: broker}}Что ещё стоит назвать как «другие способы» и с какой оговоркой. Передача зависимости аргументом метода — уместна для контекстных вещей (транзакция БД, логгер с полями), но не для стабильных коллабораторов. Функциональные опции NewService(repo, WithBroker(b)) — когда зависимость опциональна и есть разумный no-op по умолчанию. DI-фреймворки (google/wire — кодогенерация на этапе сборки, uber-go/fx — рефлексия в рантайме) — оправданы на больших графах зависимостей. Отдельная содержательная часть ответа: связка репозитория и брокера в одной операции — это проблема двойной записи; корректно её решают паттерном transactional outbox (запись в БД и запись события в таблицу outbox в одной транзакции, отдельный воркер читает outbox и публикует в брокер с at-least-once и идемпотентным консьюмером), а не вызовом broker.Publish сразу после repo.Apply. Для управления транзакцией через интерфейс обычно вводят TxManager с методом вида WithinTx(ctx, func(ctx context.Context) error) error, чтобы сервис не знал про *sql.Tx.
Какие хендшейки можешь предложить при TCP коннекшне?
Заголовок раздела «Какие хендшейки можешь предложить при TCP коннекшне?»Коротко. На самом TCP — трёхстороннее рукопожатие (SYN → SYN+ACK → ACK) для установления и четырёхстороннее (FIN/ACK × 2) для закрытия, плюс вариации: одновременное открытие, TCP Fast Open (данные в SYN, RFC 7413), SYN-cookies при переполнении SYN-очереди, RST как аварийное закрытие. Поверх TCP обычно идёт ещё одно-два рукопожатия: TLS (2 RTT в 1.2, 1 RTT в 1.3, 0-RTT при возобновлении), затем прикладное — HTTP Upgrade 101 для WebSocket, preface+SETTINGS для HTTP/2, StartupMessage/аутентификация для PostgreSQL, PROXY protocol перед всем этим у балансировщиков.
Глубже. Детали трёхстороннего рукопожатия, которые спрашивают следом. Клиент шлёт SYN со своим начальным ISN (случайный, чтобы старые сегменты прошлого соединения не были приняты за новые) и опциями MSS, Window Scale, SACK-Permitted, Timestamps — все они согласуются только здесь. Сервер отвечает SYN+ACK со своим ISN и подтверждением client_isn+1, кладя полуоткрытое соединение в SYN queue. Клиент шлёт ACK, соединение переезжает в accept queue, откуда его забирает accept(). Отсюда два лимита: net.ipv4.tcp_max_syn_backlog и somaxconn/backlog. При SYN-флуде включается tcp_syncookies: сервер не хранит состояние, а кодирует его в ISN и восстанавливает из ACK — ценой потери согласованных опций.
Три рукопожатия нужны именно потому, что каждая сторона должна и объявить свой ISN, и получить подтверждение чужого; двух сообщений для двусторонней синхронизации не хватает. Закрытие требует четырёх сегментов, потому что TCP-соединение полнодуплексное и каждое направление закрывается отдельно (можно жить в полузакрытом состоянии — в Go это (*net.TCPConn).CloseWrite()); часто FIN и ACK сервера объединяются, и наблюдается три сегмента. Инициатор закрытия уходит в TIME_WAIT на 2×MSL, чтобы «догоняющие» сегменты не попали в новое соединение с тем же кортежем и чтобы гарантированно доставить последний ACK. TCP Fast Open позволяет отправить данные уже в SYN, используя выданный ранее cookie, экономя RTT — но поддержка в интернете неровная из-за middleboxes.
Отправляем запрос на rutube.ru , как он доходит до TCP уровня, куда он отправит запрос? Как этот запрос сервер обработает?
Заголовок раздела «Отправляем запрос на rutube.ru , как он доходит до TCP уровня, куда он отправит запрос? Как этот запрос сервер обработает?»Коротко. Приложение резолвит rutube.ru в IP (кеш → /etc/hosts → рекурсивный DNS-запрос по UDP:53), берёт порт 443 из схемы URL, ядро по таблице маршрутизации выбирает исходящий интерфейс и src IP, назначает эфемерный порт и делает трёхстороннее рукопожатие с этим IP:443; дальше TLS с SNI rutube.ru и HTTP-запрос. На той стороне пакет доезжает до балансировщика/сервера, ядро по 4-кортежу находит слушающий сокет, кладёт готовое соединение в accept-очередь, приложение делает accept, читает запрос и отвечает.
Глубже. Разберём по шагам, потому что вопрос именно про «покажи, что ты понимаешь всю цепочку».
Резолвинг. В Go это net.Resolver: сначала стандартная логика — сначала кеш процесса отсутствует (Go его не кеширует!), читается /etc/hosts, затем /etc/resolv.conf, запрос уходит по UDP на 53-й порт рекурсора; при ответе с флагом TC повтор по TCP. Go по умолчанию использует чистый Go-резолвер, но переключается на cgo-резолвер при сложной конфигурации NSS; управляется GODEBUG=netdns=go|cgo. Ответ может быть CNAME на CDN и несколькими A/AAAA-записями; Go при Dial пробует адреса по очереди (Happy Eyeballs для IPv6/IPv4).
Выбор пути. Ядро смотрит routing table (ip route get 1.2.3.4), выбирает интерфейс, src IP и next-hop; для next-hop нужен MAC — ARP-запрос или запись в neighbour cache. Эфемерный порт выделяется из ip_local_port_range, соединение однозначно определяется 4-кортежем. Формируется сегмент SYN, оборачивается в IP-пакет, тот — в Ethernet-кадр.
По дороге. Домашний роутер делает SNAT (подменяет src IP:port и запоминает трансляцию), пакет идёт через провайдера по BGP-маршрутам, скорее всего попадает не на «настоящий rutube», а на ближайший PoP CDN благодаря anycast или geo-DNS.
Рукопожатия. SYN/SYN-ACK/ACK, затем TLS: ClientHello с SNI (по нему фронт понимает, чей это домен, ещё до HTTP), ALPN (h2, http/1.1), в TLS 1.3 — один RTT. Затем собственно HTTP-запрос: GET / HTTP/1.1 c Host: rutube.ru либо HTTP/2-фреймы.
Обработка на сервере. NIC принимает кадр, DMA в кольцевой буфер, прерывание/NAPI, softirq поднимает пакет по стеку: проверка Ethernet → IP (checksum, адрес назначения) → TCP (checksum, поиск сокета по 4-кортежу в хеш-таблице). Если это SYN и есть слушающий сокет на 443 — запись в SYN queue, ответ SYN-ACK; после ACK клиента соединение переезжает в accept queue (её переполнение = дропы или RST, видно в netstat -s | grep -i listen). Приложение (nginx/Envoy) через epoll узнаёт о готовности, вызывает accept, читает байты, терминирует TLS, парсит HTTP, по Host/пути выбирает апстрим и проксирует его — уже своим TCP-соединением из keep-alive пула. Ответ идёт обратно тем же 4-кортежем, роутер разворачивает NAT-трансляцию.
В Go-варианте клиента вся первая половина — это http.Transport: он держит пул keep-alive соединений (MaxIdleConnsPerHost), поэтому второй запрос к тому же хосту рукопожатий уже не делает; DialContext отвечает за резолв и connect, TLSHandshakeTimeout и ResponseHeaderTimeout — за отдельные фазы. Полезная деталь для ответа: http.DefaultClient не имеет общего таймаута, и это классическая продовая проблема.
Что означает поле Reserved?
Заголовок раздела «Что означает поле Reserved?»Коротко. В TCP-заголовке Reserved — биты, зарезервированные для будущих расширений протокола; отправитель обязан выставлять их в ноль, получатель — игнорировать. В актуальной спецификации (RFC 9293) это 4 бита между полем Data Offset и восемью управляющими флагами.
Глубже. История поля хорошо показывает, зачем такие резервы вообще нужны. В исходном RFC 793 после Data Offset шли 6 зарезервированных битов и 6 флагов (URG, ACK, PSH, RST, SYN, FIN). RFC 3168 забрал два зарезервированных бита под ECN — так появились CWR (Congestion Window Reduced) и ECE (ECN-Echo), и осталось 4 резервных. Ещё один бит на время был занят экспериментальным Nonce Sum (RFC 3540), но этот механизм признан устаревшим (RFC 8311), и бит вернулся в резерв. То есть поле — это заранее оставленное место для эволюции протокола без ломки формата заголовка.
Практические следствия. Middleboxes, которые «нормализуют» трафик и обнуляют или отбрасывают пакеты с непонятными битами, годами тормозили внедрение новых механизмов TCP — это часть аргументации в пользу того, что QUIC вынес транспорт в userspace и зашифровал почти весь свой заголовок. Если вопрос задавали в контексте другого протокола, ответ по смыслу тот же: Reserved/Padding встречается в заголовке UDP-Lite, в IPv4 (флаг «Reserved bit», он же evil bit из первоапрельского RFC 3514), в заголовке WebSocket-фрейма (биты RSV1–RSV3, где RSV1 задействован расширением permessage-deflate) — везде это «зарезервировано, ставь ноль, если не договорились об обратном».
Был ли у вас опыт работы с протоколом WebSocket на Go?
Заголовок раздела «Был ли у вас опыт работы с протоколом WebSocket на Go?»Коротко. Вопрос про личный опыт. Отвечать надо конкретикой: какая библиотека, сколько соединений, как решались ping/pong, бэкпрешер, авторизация, масштабирование на несколько нод, что пошло не так в проде.
Глубже. Каркас сильного ответа, по частям. Библиотека и почему: github.com/gorilla/websocket — самая распространённая, зрелая, низкоуровневый API с явными дедлайнами; github.com/coder/websocket (ранее nhooyr.io/websocket) — современнее, context-aware, поддерживает wsjson и работает в WASM; golang.org/x/net/websocket использовать не стоит, он давно не развивается и его собственная документация советует альтернативы. Механика соединения: апгрейд из HTTP (Upgrader.CheckOrigin — обязательно ограничить, иначе CSWSH-атака), одна горутина на чтение и одна на запись, SetReadLimit, SetReadDeadline + SetPongHandler, тикер, шлющий ping — без этого мёртвые соединения за NAT висят до часов, потому что TCP сам по себе молчащий разрыв не замечает. Бэкпрешер: буферизованный send-канал и разрыв медленного клиента вместо неограниченного роста памяти. Авторизация: браузерный WebSocket не умеет ставить заголовки, поэтому либо cookie, либо токен в query/подпротоколе, с проверкой до апгрейда и с повторной проверкой прав при подписке на канал. Масштабирование: реестр соединений плюс Redis Pub/Sub или NATS для межнодовой доставки (см. вопрос выше). Эксплуатация: Sec-WebSocket-Protocol для версионирования, коды закрытия (1000 нормальное, 1001 уход, 1006 аварийный обрыв — его нельзя отправить, он только локальный), graceful shutdown, метрики по числу соединений и лагу очередей.
Типичные ошибки кандидатов: сказать «использовал gorilla, всё просто» без упоминания ping/pong и медленных клиентов; писать в соединение из нескольких горутин; не ограничивать CheckOrigin; не понимать, что балансировщик обязан пропускать Upgrade/Connection и иметь большие таймауты idle (иначе он рвёт соединение каждые 60 секунд).
В чем ключевые различия между протоколами TCP и UDP?
Заголовок раздела «В чем ключевые различия между протоколами TCP и UDP?»Коротко. См. выше про отличия TCP/UDP. Если нужно назвать «ключевые», то их пять: наличие соединения, гарантия доставки, гарантия порядка, семантика передачи (поток байт против датаграмм) и наличие управления потоком/перегрузкой.
Глубже. Формат, который хорошо звучит вслух, — таблица «свойство → TCP → UDP → следствие для приложения»: соединение (есть / нет → у TCP цена 1 RTT на старт и состояние на сервере); надёжность (ретрансмиссии / нет → у UDP приложение обязано терпеть потери или восстанавливать само); порядок (гарантирован / нет → у TCP head-of-line blocking); границы сообщений (стираются / сохраняются → у TCP нужен фрейминг); контроль скорости (rwnd + cwnd / ничего → UDP-приложение может само себя утопить); заголовок (20+ байт / 8 байт); адресация (unicast / плюс multicast и broadcast); контрольная сумма (обязательна / опциональна в IPv4). После таблицы обязательно один-два примера выбора из своей практики — иначе ответ выглядит зазубренным.
В чем разница между TCP и UDP?
Заголовок раздела «В чем разница между TCP и UDP?»Коротко. См. выше — та же разница: TCP надёжен, упорядочен, требует соединения и управляет скоростью; UDP этого не делает и потому дешевле и предсказуемее по задержке.
Глубже. На повторный вопрос в такой форме полезно ответить не списком, а причинно: «UDP — это ровно IP плюс порты; всё, что отличает TCP, — это надстройка ради одного свойства, надёжного упорядоченного потока, и каждый механизм TCP можно вывести из этого требования». Затем показать вывод: чтобы знать, что дошло — нужны номера и ACK; чтобы повторить недошедшее — буфер отправки и таймер RTO с оценкой RTT; чтобы не переполнить получателя — окно приёмника; чтобы не переполнить сеть — окно перегрузки; чтобы стороны согласовали начальные номера и опции — рукопожатие; чтобы не перепутать старые сегменты с новыми — случайные ISN и TIME_WAIT. Такой ответ показывает понимание, а не память.
В чем отличие TCP от UDP?
Заголовок раздела «В чем отличие TCP от UDP?»Коротко. См. выше. Одной фразой: TCP даёт приложению иллюзию надёжной двусторонней трубы байт, UDP — просто отправляет пакет и забывает о нём.
Глубже. Полезно закрыть этот блок вопросов тем, чего в стандартном сравнении обычно нет и что отличает уверенный ответ. Первое: «TCP гарантирует доставку» — неточно; TCP гарантирует, что либо данные будут доставлены в порядке и без искажений, либо соединение будет разорвано с ошибкой; успешный возврат из Write не означает, что байты дошли до приложения на той стороне (они лишь скопированы в буфер отправки ядра), поэтому подтверждения на уровне приложения всё равно нужны. Второе: контрольная сумма TCP/UDP 16-битная и слабая, от повреждений в памяти/на дисках она не спасает — для целостности данных нужен свой хеш. Третье: в мире контейнеров разница вылезает в неожиданных местах — UDP-трафик через kube-proxy и conntrack ведёт себя иначе, чем TCP (записи conntrack «залипают» на старый под, потому что закрытия соединения нет).
Что такое модель OSI?
Заголовок раздела «Что такое модель OSI?»Коротко. OSI (Open Systems Interconnection, стандарт ISO/IEC 7498-1) — эталонная семиуровневая модель сетевого взаимодействия: физический, канальный, сетевой, транспортный, сеансовый, представления, прикладной. Каждый уровень пользуется услугами нижнего и предоставляет услуги верхнему, а взаимодействует логически с одноимённым уровнем на другом хосте.
Глубже. Главная идея модели — инкапсуляция: данные приложения на каждом уровне вниз оборачиваются в свой заголовок (сегмент TCP → пакет IP → кадр Ethernet), а на приёме разворачиваются в обратном порядке; каждый уровень видит содержимое верхнего как непрозрачный payload. Это позволяет менять реализацию одного уровня, не трогая остальные — Wi-Fi вместо Ethernet ничего не меняет для TCP.
Столь же важно понимать ограничения. OSI создавалась как стандарт для собственного стека протоколов ISO, который в итоге проиграл TCP/IP; реальный интернет соответствует четырёхуровневой модели TCP/IP (RFC 1122): link, internet, transport, application. Поэтому уровни 5 и 6 в жизни почти не выделяют: HTTP-сессии, TLS, сериализация JSON/protobuf — всё это внутри «приложения». Полезность OSI сегодня — общий словарь для разговора: «L2-коммутатор», «L3-маршрутизация», «L4-балансировка по 4-кортежу», «L7-роутинг по Host», «проблема на седьмом уровне». Проговорить эту оговорку в ответе — плюс: показывает, что вы знаете модель, а не заучили список.
Что такое сетевая модель OSI и из чего состоит?
Заголовок раздела «Что такое сетевая модель OSI и из чего состоит?»Коротко. См. выше про OSI. Состав снизу вверх: L1 физический (биты, среда), L2 канальный (кадры, MAC-адресация, коммутация, ARP), L3 сетевой (пакеты, IP-адресация, маршрутизация, ICMP), L4 транспортный (сегменты/датаграммы, порты, TCP/UDP), L5 сеансовый (установление и управление сессиями), L6 представления (кодирование, сериализация, сжатие, шифрование), L7 прикладной (HTTP, DNS, SMTP и прочее, чем пользуется программа).
Глубже. Для каждого уровня стоит держать наготове тройку «единица данных — адресация — типичное устройство/протокол»: L1 — бит, адресации нет, повторители и хабы, витая пара/оптика; L2 — кадр, MAC-адрес, коммутатор, Ethernet/Wi-Fi/ARP/VLAN; L3 — пакет, IP-адрес, маршрутизатор, IPv4/IPv6/ICMP/OSPF/BGP; L4 — сегмент (TCP) или датаграмма (UDP), номер порта, файрвол/NAT/L4-балансировщик, TCP/UDP/SCTP; L5 — сессия, RPC и NetBIOS исторически, в современном стеке — сессии TLS, cookie, sticky-сессии; L6 — представление, сюда условно относят TLS, форматы (JSON, protobuf, ASN.1), сжатие; L7 — данные, HTTP/DNS/SMTP/SSH/gRPC. Мнемоника для порядка снизу вверх: «Please Do Not Throw Sausage Pizza Away». Дальше интервьюеры любят спрашивать «а куда положить TLS/QUIC/WebSocket» — правильный ответ, что жёсткого места у них нет, и объяснить почему: TLS работает поверх транспорта и под приложением (L5–L6 условно), QUIC — транспорт, реализованный поверх UDP в userspace, WebSocket — прикладной протокол, устанавливаемый через HTTP-апгрейд и дальше работающий как собственный фрейминг поверх TCP.
Знаешь протоколы TCP/UDP?
Заголовок раздела «Знаешь протоколы TCP/UDP?»Коротко. Да — и дальше не «да», а сжатый структурированный ответ: оба транспортного уровня, оба мультиплексируют по портам; TCP — с соединением, надёжный, упорядоченный, поток байт, с управлением потоком и перегрузкой; UDP — без соединения, ненадёжный, датаграммный, минимальный.
Глубже. Открытый вопрос такого вида — приглашение показать глубину самостоятельно. Хорошая стратегия: назвать различия одним абзацем (как выше), а затем добавить один-два «якоря», по которым интервьюер может копнуть и увидеть, что вы знаете предмет: например, упомянуть трёхстороннее рукопожатие и TIME_WAIT, окна rwnd/cwnd и BDP, алгоритм Нагла и TCP_NODELAY (в Go он включён по умолчанию), SACK, keep-alive, и со стороны UDP — отсутствие контроля перегрузки, ограничение по MTU и то, что QUIC строит надёжный транспорт поверх UDP. Плохая стратегия — ответить односложно «да, знаю» либо, наоборот, вывалить всё подряд без структуры.
В каких задачах используется UPD?
Заголовок раздела «В каких задачах используется UPD?»Коротко. (В вопросе опечатка, речь про UDP.) Там, где задержка важнее полноты данных или где соединение — избыточные накладные расходы: DNS, DHCP, NTP, SNMP, syslog, метрики StatsD, VoIP и видеоконференции (RTP/RTCP, WebRTC), онлайн-игры, обнаружение устройств через multicast/mDNS/SSDP, VPN и оверлеи (WireGuard, VXLAN, GRE-over-UDP), а также как основа для собственных транспортов — QUIC и HTTP/3.
Глубже. Стоит объяснить принцип, а не только перечислить. Первый класс задач — данные с истекающим сроком годности: аудио-фрейм, устаревший на 300 мс, уже не нужен, повторная передача только увеличит задержку и джиттер; вместо ретрансмиссии применяют FEC и адаптивные кодеки, а RTCP собирает статистику потерь для подстройки битрейта. Второй класс — однократный короткий обмен: DNS-запрос и ответ помещаются в один пакет, и рукопожатие TCP удвоило бы латентность резолвинга. Третий класс — широковещание и групповая рассылка: TCP их принципиально не умеет, а обнаружение сервисов и IPTV на них построены. Четвёртый — свой транспорт: если нужна надёжность с другими свойствами (мультиплексирование без head-of-line blocking, миграция соединения, 0-RTT), проще построить её в userspace над UDP, чем ждать обновления ядер, — именно так появился QUIC. Пятый — туннелирование: инкапсулировать чужой трафик в TCP плохо из-за эффекта «TCP meltdown» (два наложенных механизма ретрансмиссии конфликтуют), поэтому VPN-протоколы предпочитают UDP.
Обратная сторона, которую полезно упомянуть: UDP чаще режут корпоративные файрволы, он подвержен amplification-атакам без проверки source IP, и на нём легко случайно устроить перегрузку канала, потому что тормозить вас никто не будет.
До 5000 операций (deposit/withdraw).
Заголовок раздела «До 5000 операций (deposit/withdraw).»Коротко. Обрывок исходника, вопрос не восстанавливается — это фрагмент условия live-coding задачи (ограничение на объём входных данных), а не вопрос по сетям.
Глубже. Если такая формулировка всплывает в задаче, единственное содержательное, что из неё следует: 5000 операций — крошечный объём, поэтому упор проверяющего не на асимптотику, а на корректность конкурентного доступа к балансу (мьютекс или актор-горутина с каналом команд, атомарность пары «проверка баланса → списание», отсутствие отрицательного баланса при гонке) и на аккуратную обработку ошибок недостатка средств.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- «UDP быстрее TCP» без уточнений. Скорость канала одинакова; UDP выигрывает в задержке установления и в отсутствии head-of-line blocking, а не «потому что легче».
- «TCP гарантирует доставку». TCP гарантирует либо доставку в порядке, либо ошибку соединения; успешный
Writeозначает лишь копирование в буфер ядра, поэтому подтверждения на прикладном уровне всё равно нужны. - Путать окно приёмника (rwnd, поле в заголовке, защищает получателя) с окном перегрузки (cwnd, только у отправителя, защищает сеть) и не знать про Window Scale и BDP.
- Считать, что число TCP-соединений ограничено 65535. Ограничение действует на пару «src IP → dst IP:port» и равно числу эфемерных портов; соединение идентифицируется 4-кортежем, поэтому сервер на одном порту держит миллионы соединений.
- Путать
SO_REUSEADDRиSO_REUSEPORT: первый — про TIME_WAIT и разные адреса, второй — про несколько слушателей на одном и том же адресе с балансировкой в ядре. - Забывать, что TCP — поток байт без границ сообщений, и удивляться «склейке» и «разрыву» сообщений при
Read; свой фрейминг обязателен. - В WebSocket: писать в соединение из нескольких горутин, не делать ping/pong и
SetReadDeadline, не ограничиватьCheckOrigin, не иметь политики для медленного клиента — все четыре ошибки проявляются только в проде. - Излагать OSI как истину в последней инстанции и мучительно раскладывать по семи уровням TLS, WebSocket и QUIC вместо того, чтобы сказать, что реальный стек — четырёхуровневый TCP/IP, а OSI — общий словарь.
Что почитать
Заголовок раздела «Что почитать»- RFC 9293 «Transmission Control Protocol (TCP)» — актуальная спецификация, заменившая RFC 793: https://www.rfc-editor.org/rfc/rfc9293.html
- RFC 768 «User Datagram Protocol» (всего три страницы) и RFC 7323 «TCP Extensions for High Performance» (Window Scale, Timestamps): https://www.rfc-editor.org/rfc/rfc768.html, https://www.rfc-editor.org/rfc/rfc7323.html
- RFC 6455 «The WebSocket Protocol» и RFC 9000 «QUIC: A UDP-Based Multiplexed and Secure Transport»: https://www.rfc-editor.org/rfc/rfc6455.html, https://www.rfc-editor.org/rfc/rfc9000.html
- Документация пакета
netв Go и исходники netpoller: https://pkg.go.dev/net, https://github.com/golang/go/tree/master/src/runtime (netpoll.go,netpoll_epoll.go) - Beej’s Guide to Network Programming — про сокеты и системные вызовы вживую: https://beej.us/guide/bgnet/