Интересует именно почему есть такое ограничение, как оно технически обосновывается. Про "сырые" сокеты и мультиплексирование мне известно.
Как я понимаю, порты в принципе придуманы, чтобы разделить службы, соединения и т. п. на одном узле. Ограничение "один порт — один узел" выглядит в этом свете логичным. Но при этом есть SO_REUSEADDR, позволяющая нескольким сокетам привязываться к одной и той же паре (адрес, порт). А вот возможности привязать один сокет к нескольким портам нет, хотя, казалось бы, это шанс сэкономить системный вызов (мультиплексор) и ресурсы (дополнительные сокеты), упрощение программирования, способ сделать какое-никакое резервирование.
RFC 768 (UDP) такого ограничения не накладывает. RFC 4960 (SCTP) требует использовать одинаковый порт для всех адресов на одном конце ассоциации (п. 1.2), хотя непонятно, зачем это нужно. Понятно, почему TCP требует одного порта — по определению своего соединения:
[Port] concatenated with the network and host addresses from the internet communication layer, this forms a socket. A pair of sockets uniquely identifies each connection.
UDP интересует больше всего.
Попытка чтения исходников Linux:
подтвердила, что сокеты хранятся в хэш-таблице по ключу, вычисляемому от порта либо адреса и порта, но все равно непонятно, что мешало бы в такую таблицу добавить несколько записей об одном сокете, чтобы из него вычитывать пакеты, направленные на разные порты;
показала, что есть конкретная проверка, запрещающая это делать, и вряд ли она там просто так.
Подскажите, пожалуйста, какую реальную причину я упускаю.
PB>А вот возможности привязать один сокет к нескольким портам нет, хотя, казалось бы, это шанс сэкономить системный вызов (мультиплексор) и ресурсы (дополнительные сокеты), упрощение программирования, способ сделать какое-никакое резервирование.
Как раз для упрощения и сделано. Ситуация, когда нужно один и тот же сервис подвесить сразу на много портов, видится весьма... гм... надуманной, что ли. Какие сценарии вы можете представить?
Программирование заметно усложнилось бы, т.к. потребовались бы дополнительные средства типа "из какого порта прилетел этот буфер с данными". Хотя бы для того, чтобы потом указать source port в исходящем UDP.
Re: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, PlushBeaver, Вы писали:
>подтвердила, что сокеты хранятся в хэш-таблице по ключу, вычисляемому от порта либо адреса и порта, но все равно непонятно, что мешало бы в такую таблицу
>добавить несколько записей об одном сокете, чтобы из него вычитывать пакеты, направленные на разные порты
Как? В пришедшем пакете есть только инфа о sport, dport, saddr и daddr и всё. Какому именно соединению или сокету
пакет предназначен можно определить только по dport+daddr, если разные dport будут соответствовать одному
и тому же сокету то что будет если в системе появится второй сокет с таким же dport+daddr? Как определить здесь к какому из двух сокетов направлен пакет с данным daddr?
G>А что тогда в source port в UDP header пихать, когда write в этот сокет?
Я же написал: "Программирование заметно усложнилось бы, т.к. потребовались бы дополнительные средства типа "из какого порта прилетел этот буфер с данными". Хотя бы для того, чтобы потом указать source port в исходящем UDP. "
Если бы был такой интерфейс multi-bind, все бы пришлось врукопашную. Как в sendto() можно указать dport/daddr, так указывали бы еще и sport/saddr.
Re[2]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, SkyDance, Вы писали:
SD>Как раз для упрощения и сделано. Ситуация, когда нужно один и тот же сервис подвесить сразу на много портов, видится весьма... гм... надуманной, что ли. Какие сценарии вы можете представить?
1. Ответ с того же IP, на который пришёл запрос, для системы со многими адресами.
Критично важно на границе сетей разной видимости (DMZ, шлюз на границе), не всегда обеспечивается раутингом.
Реально сейчас из-за недоработки этой возможности средства типа named плодят по сокету на найденный локальный IP. Так что никакой надуманности, чистая реальность.
2. Биндинг сервиса на вообще любой порт в пределах IP, для облегчения коннекта при жёстких полиси отправителя. Использовалось, например, в ICQ. Средствами пакетного фильтра делалось, в терминах ipfw,
fwd 127.0.0.1,5440 tcp from any to me in recv fxp0
, аналогично для UDP. Ответ должен был приходить с той же комбинации хост+порт, хотя бы для прохождения через NAT.
SD>Программирование заметно усложнилось бы, т.к. потребовались бы дополнительные средства типа "из какого порта прилетел этот буфер с данными". Хотя бы для того, чтобы потом указать source port в исходящем UDP.
Усложнение несущественно. Опции IP_RECVDSTADDR и IP_SENDSRCADDR существуют в BSD системах уже много лет и реально используются. У Linux есть аналог IP_PKTINFO.
The God is real, unless declared integer.
Re[2]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, smeeld, Вы писали:
>>подтвердила, что сокеты хранятся в хэш-таблице по ключу, вычисляемому от порта либо адреса и порта, но все равно непонятно, что мешало бы в такую таблицу
>>добавить несколько записей об одном сокете, чтобы из него вычитывать пакеты, направленные на разные порты
S>Как? В пришедшем пакете есть только инфа о sport, dport, saddr и daddr и всё. Какому именно соединению или сокету S>пакет предназначен можно определить только по dport+daddr, если разные dport будут соответствовать одному S>и тому же сокету то что будет если в системе появится второй сокет с таким же dport+daddr?
То же, что происходит, когда такой второй сокет появляется сейчас? Через SO_REUSEPORT это можно сделать. В какой сокет в результате придёт пакет, определяется по round-robin методу.
S> Как определить S>здесь к какому из двух сокетов направлен пакет с данным daddr?
Ну ведь определяют же?
The God is real, unless declared integer.
Re[3]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, SkyDance, Вы писали:
SD>Если я верно понял тредстартера, он хочет один сокет на несколько dport+daddr, но никак не несколько сокетов на один dport+daddr.
ТС говорит про привязку одного и того же struct sock нескольким dport+daddr. Выше описал, что
если создать второй сокет с таким же dport+daddr, кроме уже существующего, то как система определит
какому из двух сокетов с одинаковыми dport+daddr предназначается пришедший пакет?
Re[3]: Почему сокету нельзя сделать bind() к двум портам?
N>То же, что происходит, когда такой второй сокет появляется сейчас? Через SO_REUSEPORT это можно сделать. В какой сокет в результате придёт пакет, определяется по round-robin методу.
На самом деле создаётся несколько сокетов откликающиеся на один dport, и помещаются в один хеш
извлекаются роундом как Вы сказали так,
но это только для сокетов в состоянии listen, и ТС говорил про создание одного сокета, откликающегося на разные dport,
и что будет если привязать dport к сокету, а потом создать новый сокет с таким же dport и забыть сделать REUSEPORT, то
система встанет в ступор, так как определить какому из сокетов направлен пришедший пакет с данным dport возможности не будет,
Re[4]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, smeeld, Вы писали:
N>>То же, что происходит, когда такой второй сокет появляется сейчас? Через SO_REUSEPORT это можно сделать. В какой сокет в результате придёт пакет, определяется по round-robin методу. S>На самом деле создаётся несколько сокетов откликающиеся на один dport, и помещаются в один хеш S>извлекаются роундом как Вы сказали так, S>но это только для сокетов в состоянии listen,
Предположим (я уже плохо помню специфику конкретно свежих Linux)
S> и ТС говорил про создание одного сокета, откликающегося на разные dport,
Да, поэтому и непонятно, к чему ты всё время вспоминаешь ортогональный случай.
S>и что будет если привязать dport к сокету, а потом создать новый сокет с таким же dport и забыть сделать REUSEPORT, то S>система встанет в ступор, так как определить какому из сокетов направлен пришедший пакет с данным dport возможности не будет,
Она не впадёт в ступор — она максимум проблемного это будет посылать на один и тот же сокет, а второй проигнорирует.
Но опять-таки это не случай ТС, и вспоминать его нет смысла.
The God is real, unless declared integer.
Re[2]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, smeeld, Вы писали:
S> если разные dport будут соответствовать одному S> и тому же сокету то что будет если в системе появится второй сокет с таким же dport+daddr? Как определить S> здесь к какому из двух сокетов направлен пакет с данным daddr?
Речь о другом ограничении: что по двум разным ключам (daddr, dport) нельзя держать одинаковые значения (указатели на одну и ту же структуру). Но даже если с SO_REUSEADDR появятся другие сокеты на том же (daddr, dport), сработает round-robin, о которой вы говорите с netch80, чему помешают одинаковые значения, не ключи?
Я совсем не знаю механизмов ядра, и могу ошибаться, но как понял фрагмент, на который вы дали
ссылку, round-robin организуется изменением поля sk_refcnt той структуры-сокета, которую выбрали при очередном поиске. Раз сокет один, это подвинуло бы его во всех круговых очередях, где он участвует. Тогда был бы такой проблемный сценарий (полужирным отмечено, кому достается пакет):
Правка, забыто: Пусть сокет s1 привязан к e1, s2 привязан к e2, s3 привязан к e1 и e2 (чего сейчас нельзя).
Куда приходит пакет
Очередь e1
Очередь e2
e1
s1,s3
s2,s3
e2
s3,s1
s3,s2
e1
s1,s3
s2,s3
e2
s3,s1
s3,s2
e1
s1,s3
s2,s3
Но даже если так — получается, причина запрета только в реализации, и с точки зрения логики работы сетевого стека препятствий нет?
Здравствуйте, SkyDance, Вы писали:
SD> Какие сценарии вы можете представить?
Необходимость только принимать с разных, но не всех, портов, причем с минимальным временем от прихода пакета на интерфейс до его обработки. В такой задаче экономия от вызова мультиплексора заметна, и упрощение программирования очевидно. О других случаях (про некоторые, честно сказать, не думал) уже написал netch80.
SD> Программирование заметно усложнилось бы, т.к. потребовались бы дополнительные средства типа "из какого порта прилетел этот буфер с данными".
На стороне получателя это известно. Какой был source port у пакета UDP, пользователю recvfrom() знать не нужно. Про TCP более-менее понятно, откуда запрет.
SD> Хотя бы для того, чтобы потом указать source port в исходящем UDP.
Даже когда это требуется, подошел бы первый привязанный, случайный привязанный или выбранный по round-robin, как обсуждается ниже.
Re[3]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, PlushBeaver, Вы писали:
>round-robin организуется изменением поля sk_refcnt той структуры-сокета, которую выбрали при очередном поиске.
Нет sk_refcnt это типа shared_ptr-а, сокет не удаляется пока какой-нибудь поток его использует
в своих операциях, удаляется только если sk_refcnt становится ноль тут и тут
Как роунд организован там по ссылке в коде всё расписано, чисто манипуляцией при поиске по хешу.
Re[4]: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, smeeld, Вы писали:
S>Как роунд организован там по ссылке в коде всё расписано, чисто манипуляцией при поиске по хешу.
Почитал внимательнее: камень преткновения действительно в этом месте, но проблема не совсем в round-robin, она общая для TCP и UDP, по меньшей мере.
UDP мне показалось проще разобрать. Путь выполнения при получении пакета UDP такой:
Предпоследняя выбирает "корзинку" хэш-таблицы и ищет только в ней функцией compute_score() или compute_score2() — обе явными проверками не позволят передать пакет сокету, поля в структуре которого не соответствуют адресу и порту назначения пакета. Так делается потому, что хэш-таблица не запоминает, под каким точно ключом-endpoint в нее помещено значение-структура сокета, потому что его можно извлечь из значения. Это экономит память, но это же и не дает привязать сокет к двум endpoints, потому что значение-структура сокета хранит всего одну пару (адрес, порт).
Видимо, ответ на мой вопрос: потому что такова реализация, а в принципе можно было бы и привязывать.
Спасибо за помощь!
Re: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, PlushBeaver, Вы писали:
PB>Интересует именно почему есть такое ограничение, как оно технически обосновывается. Про "сырые" сокеты и мультиплексирование мне известно.
Собственно, основные ограничения — это функциональность существующих транспортных протоколов (TCP/UDP) и их заголовки. В принципе, ничто не мешает написать свой транспортник с биндингом на набор портов, кроме того что существующие маршрутизаторы его будут дропать, скорее всего. Плюс сшивка разных потоков данных в единую логическую сущность будет той ещё задачей. А так — просто появится набор дополнительных констант, которые надо будет юзать вместо обычных при создании сокета:
struct sockaddr_multiport my_addr; // наш собственный дескриптор для IP и списка портов
sock = socket(AF_INET, SOCK_MULTIPORT, 0); // наш собственный тип протокола
bind(sock, (struct sockaddr *) &my_addr, sizeof(struct sockaddr_multiport));
Или так:
// пользуем штатную структуру, по одному разу на портstruct sockaddr_in my_addr1;
struct sockaddr_in my_addr2;
sock = socket(AF_INET, SOCK_MULTIPORT, 0); // наш собственный тип протокола
// поочерёдно биндимся на каждый endpoint
bind(sock, (struct sockaddr *) &my_addr1, sizeof(struct sockaddr_in));
bind(sock, (struct sockaddr *) &my_addr2, sizeof(struct sockaddr_in));
в винде есть т.н. "dual-stack" сокеты, позволяющие принимать соединия с IPv6 и IPv4 интерфейсов через один сокет. Это можно трактовать, как bind сокета к двум портам ( один на IPv4 интерфейсе, другой на IPv6 ). Если что, мы этого не одобряем.
_>в винде есть т.н. "dual-stack" сокеты, позволяющие принимать соединия с IPv6 и IPv4 интерфейсов через один сокет. Это можно трактовать, как bind сокета к двум портам ( один на IPv4 интерфейсе, другой на IPv6 ). Если что, мы этого не одобряем.
Эти дуал сокеты работают только в бинде к LOOPBACK, к любому другому адресу — нет. В документации этого нет, но на деле работает только с лупбек. У меня например, и у народа в интере.
Re: Почему сокету нельзя сделать bind() к двум портам?
Здравствуйте, PlushBeaver, Вы писали:
PB>Интересует именно почему есть такое ограничение, как оно технически обосновывается. Про "сырые" сокеты и мультиплексирование мне известно.
На мой взгляд, проблема чисто реализационная. Когда система получает пакет для IP:Port, то ей приходится пробегать через несколько сокетов заинтересованных в таком пакете. Прежде всего это может быть listener, ну и сокеты уже привязаные к какому-то удалённому адресу. Таким образом, для того, чтобы решить кому передавать пакет, приходится смотреть на атрибуты сокета. Если позволить привязывание сокета к нескольким адресам, то придётся расширять эти атрибуты. Это дополнительное усложнение системы. В принципе сделать можно, но возникает вопрос обоснованности данного усложнения. Если подобное использование нужно в редких случаях, то предпочтут простоту. Ведь как правило, усложняя в одном месте, будешь вынужден усложнять во многих других.
Кстати, опция SO_REUSEADDR не очень позволяет привязываться к одному и тому же адресу. Если верить Stevens "Unix Network Programming", то эта опция позволяет 4 возможных варианта
1. Привязать локальный адрес если есть сокеты привязанные к этому локальному и к какому-то удалённому адресу. То есть создать новый listener даже если есть соединения установленные старым listener. Создание 2-х listener на одном адресе невозможно. Это обычный случай.
2. Привязать локальный адрес, если есть сокеты привязанные к этому адресу по wildcard. То есть, если один сокет привязан на "все" адреса, то можно привязать сокет на конкретный адрес. Соединения для этого адреса будут идти только на его сокет. Wildcard будет получать только соединения не перехваченные сокетами с конкретными адресами. Это создаёт дыру в безопасности, поэтому очень часто такое использование разрешается только если wildcard поднимается последним, или же вообще не разрешается.
3. Для привязывания нескольких сокетов в одном процессе к одному порту но разным локальным IP адресам. Это на случай если система не поддерживает IP_RECVDSTADDR. В какой-то мере это частный случай для 2.
4. Если транспортный протокол позволяет, то с этой опцией можно привязать несколько сокетов к одному адресу. Стандартный случай — мультикаст и броадкаст адреса. В этом случае, каждый сокет привязанный к адресу получает копию пакета. В случаях unicast адресов всё зависит от реализации.