UDP и проблема доставки ответа

Original author: Matt Palmer
  • Translation
image
Ниже — перевод статьи о проблеме работы с udp в сетевых приложениях. Переводчик позволил себе сменить примеры: в исходном тексте другие сетевые адреса и код на ruby. В переводе использован простенький скрипт на перле. Суть проблемы и решение от этого не меняются.
Кроме того, местами добавлены мои комментарии (в скобках, выделены курсивом).
Картинка для привлечения внимания взята из текста замечательной книги «learnyousomeerlang.com»

Тяжкая работа лёгких протоколов



Иногда начинает казаться, что протоколы без установки соединения не оправдывают всей той кутерьмы, которую вызывают.

Для примера разберём ситуацию с получением ответа, когда UDP датаграмма с начальным запросом посылается на дополнительный IP адрес на интерфейсе (alias или secondary IP).
Есть интерфейс eth1:
$ ip a add 192.168.1.235/24 dev eth1 && ip a ls dev eth1       
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:30:84:9e:95:60 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.47/24 brd 192.168.1.255 scope global eth1
    inet 192.168.1.235/24 scope global secondary eth1
    inet6 fe80::230:84ff:fe9e:9560/64 scope link 
       valid_lft forever preferred_lft forever


Как обычно выглядит код для получения пакета по udp? Ну, echo сервер может выглядеть как-то очень похоже на то, что под катом:
echo_server.pl
#!/usr/bin/perl
use IO::Socket::INET;

# flush after every write
$| = 1;

my ($socket,$received_data);
my ($peeraddress,$peerport);

$socket = new IO::Socket::INET ( 
    MultiHomed => '1',
    LocalAddr => $ARGV[0],
    LocalPort => defined ($ARGV[1])?$ARGV[1]:'5000',
    Proto => 'udp'
) or die "ERROR in Socket Creation : $! \n";
print "Waiting for data...";
while(1)
{
$socket->recv($recieved_data,1024);
$peer_address = $socket->peerhost();
$peer_port = $socket->peerport();
chomp($recieved_data);
print "\n($peer_address , $peer_port) said : $recieved_data";

#send the data to the client at which the read/write operations done recently.
$data = "echo: $recieved_data\n";
$socket->send("$data");

}

$socket->close();



Это достаточно простой скрипт на перле, который покажет от кого пришёл udp пакет, содержимое пакета и отправит этот пакет обратно отправителю. Проще некуда. Теперь запустим наш сервер:
$ ./echo_server.pl
Waiting for data...

Посмотрим, что он слушает:
$ netstat -unpl | grep perl
udp        0      0 0.0.0.0:5000            0.0.0.0:*                           9509/perl

И после этого подключимся с удалённой машины к нашему серверу по основному IP:
-bash-3.2$ nc -u 192.168.1.47 5000
test1
echo: test1
test2
echo: test2

Как это выглядит в tcpdump'е на нашей машине (ну, или должно выглядеть):
-bash-3.2$ tcpdump -i eth1 -nn  port 5000
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes
17:41:00.517186 IP 192.168.3.11.44199 > 192.168.1.47.5000: UDP, length 6
17:41:00.517351 IP 192.168.1.47.5000 > 192.168.3.11.44199: UDP, length 12
17:41:02.307634 IP 192.168.3.11.44199 > 192.168.1.47.5000: UDP, length 6
17:41:02.307773 IP 192.168.1.47.5000 > 192.168.3.11.44199: UDP, length 12

Просто фантастика — я отправляю пакет и получаю пакет обратно. В netcat'е мы получаем обратно что бы мы не напечатали (смешной эффект, если печатать «стрелочки»).

А теперь тоже самое на вторичный адрес на том же интерфейсе:
-bash-3.2$ nc -u 192.168.1.235 5000
test1
test2

Как это сумасшествие выглядит в tcpdump'е на этот раз:
-bash-3.2$ tcpdump -i eth1 -nn  port 5000
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes
17:48:32.467167 IP 192.168.3.11.34509 > 192.168.1.235.5000: UDP, length 6
17:48:32.467292 IP 192.168.1.47.5000 > 192.168.3.11.34509: UDP, length 12
17:48:33.667182 IP 192.168.3.11.34509 > 192.168.1.235.5000: UDP, length 6
17:48:33.667332 IP 192.168.1.47.5000 > 192.168.3.11.34509: UDP, length 12


Ну и естественно, никакой уважающий себя сетевой стек не собирается принимать пакеты с совершенно незнакомого адреса, даже если порты стоят правильные. Таким образом, клиент никогда не получит обратные пакеты и будет думать, что сервер просто отбрасывает его запросы.

То что происходит, на первый взгляд кажется полным бредом. Но на самом деле, это обычный дефект для протокола без установки сессии, такого как UDP. Видите ли, наш сокет слушает любой адрес, (пустой параметр LocalAddr при создании сокета передаётся системе как адрес вида «0.0.0.0», любой доступный, что заставляет сокет слушать на всех доступных адресах. И нет, я тоже не знаю, почему это так. Это не особенно интуитивное действие). Когда мы получаем пакет в нашем приложении с помощью socket->recv(), мы не получаем информации о том, на какой конкретно адрес был послан пакет. Мы лишь знаем, что операционная система решила, что пакет был для нас (вот вам и инкапсуляция). Всё что мы знаем, это откуда пакет пришёл к нам. И из-за того, что ядро не хранит никакой информации о соединениях для сокета (ядро штука логичная, просили без соединений — будет без соединений), когда приходит время отправлять пакет обратно, всё что мы можем сообщить это «куда» отправить пакет. (В перле это делается в неявном виде. С объектом $socket связаны адрес и порт отправителя датаграммы, так что в вызове send его указывать не надо).
Но настоящая мозголомка начинается, когда мы пытаемся проставить адрес отправителя в ответной датаграмме. Ещё раз: ядро не хранит никакой информации об отправителе или получателе, так как мы работаем без соединений. И посколько мы слушаем «любой» интерфейс, операционная система думает, что у неё есть карт-бланш на отправку пакета с того адреса, который ей «по душе». В линуксе, похоже, выбирается основной адрес того интерфейса, с которого пакет будет отправлен. (А на самом деле, адрес определяется в соответсвии с RFC1122, 3.3.4.2 «Multihoming Requirements», по таблице маршрутизации — примечание переводчика). Так что для распространнёного случая «один адрес — один интерфейс» — всё работает. Но как только дело доходит до менее распространённых ситуаций, начинают проявляться нюансы.
Решение в том, чтобы создавать сокеты слушающие конкретные адреса. И отправлять пакет с этих сокетов: ядро будет знать, с какого адреса вы хотите отправлять пакеты и всё будет отлично. Выглядит достаточно просто, ага? Ну и естественно, любое вменяемое сетевое приложение уже так делает, ага? Так что очевидно, что реализация UDP в Ruby просто дерьмо(в оригинале исходники на руби, — примечание переводчика;). Именно так я подумал в начале, и не виню вас, если вы подумали так же. Но пока РУБИкон войны с авторами Ruby'ового UDPSocket'а не перейдён, давайте проведём небольшой эксперимент с другими часто используемыми приложениями. Например, SNMPd. Демон из пакета net-snmpd в убунте подвержен той же проблеме, что и наше тестовое приложение выше. Не похоже, что это какие-то новые грабли, на которые только наступили и рассыпались кучей патчей для испрвалений.
Так что в целом, все страдают одной и той же «болезнью». Под «всеми» подразумевается «некоторые UDP сервера». Есть некоторое количество ПО, которое не подвержено подобной проблеме с алиасами на интерфейсах. На ум сразу приходит Bind и NTPd работает нормально, если запущен после того, как вы сконфигурировали все интерфейсы. В чём же разница? Разница в том, что эти сервисы несколько «умнее» и биндятся на все адреса в системе по отдельности. На примере bind'а:
$ netstat -lun |grep :53
 udp        0      0 192.168.1.47:53          0.0.0.0:*                          
 udp        0      0 192.168.1.47:53          0.0.0.0:*                          
 udp        0      0 127.0.0.1:53             0.0.0.0:*  

Это очень круто и решает проблему. Исключение составляет ситуация, когда вы добавляете лишний алиас уже после того, как демон стартовал. Bind не подцепит новый адрес и вам придётся рестартовать сервер. Кроме того, это несколько усложняет код, так как вам приходится как ладить с кучей сокетов внутри программы (например, использовать select() вместо простого блокирования на попытке приёма.) В общем-то, никто не любит лишних сложностей, но с этой можно справится. Однако, настоящая проблема это правило «не добавляйте адресов после старта демона». Необходимость проверять, не добавилось ли в системе ip-адресов, и рестартовать службу после добавляения адреса станет настоящей проблемой.
Однако, есть некоторый workaround и для этой проблемы. Здесь мы вспомним про ntpd. Порты, которые слушает он, выглядят следующим образом:
$ netstat -nlup | grep 123
udp        0      0 192.168.1.235:123       0.0.0.0:*                                    
udp        0      0 192.168.1.47:123        0.0.0.0:*                                          
udp        0      0 127.0.0.3:123           0.0.0.0:*                                         
udp        0      0 127.0.0.1:123           0.0.0.0:*                                          
udp        0      0 0.0.0.0:123             0.0.0.0:* 
udp6       0      0 fe80::230:84ff:fe9e:123 :::*                                              
udp6       0      0 ::1:123  


NTPd слушает каждый адрес в отдельности и дополнительно слушает на любом адресе, доступном системе. Я не знаю точно, зачем это нужно. Если просто слушать на каждом адресе в отдельности, то всё будет хорошо, как в случае с байндом. Но если вы добавляете ещё один адрес на интерфейс после старта ntpd, то начинает проявляться та же самая проблема, что и в случае с udp-echo-сервером. Так что я не думаю, что слушание на «любом» интерфейсе даёт какой либо плюс. Однако, это заставляет ntpd вести себя несколько отлично от Bind'а: когда вы посылаете пакет на интерфейс, добавляенный после старта Bind'а, то он просто игнорирует вас (у него нет сокета, который бы слушал ваши запросы). Ntpd же пытается отправить ответ и страдает от проблемы неправильного адреса в ответах. (Зато можно менять primary адреса на интерфейсах и создавать новые интерфейсы, примечание переводчика).

На текущий момент, лучшим решением кажется следовать пути Bind'а и ntpd и слушать на всех адреса в отдельности с «фокусом» от ntpd: слушать дополнительно и на 0.0.0.0. При этом, если я получил пакет на 0.0.0.0, то надо запускать сканирования доступных в системе адресов и биндится дополнительно и на них. Это должно решить проблему.
Осталось только заставить это работать (и решить кучу проблем, которые наверняка вылезут на пути). Пожелайте мне удачи. Крики боли и мучений которые вы слышите (не имеет значение, где вы находитесь) наверняка мои.

UPD: в комментариях появилось интересное пояснение от Quasar_ru. Всё таки реализация UDP в скриптовых языках неоднозначна: на чистом С можно написать такое клиентское приложение, которое сможет принять ответ от сервера с другого адреса. Польза от такой реализации — спорная, но всё же реализация возможна.
Share post

Similar posts

Comments 37

    +3
    КДПВ честно? ВЛНС, ведь простая, ну хоть обновили статью старенькую…
      +3
      ННПВЭШ!
        +3
        РПЧХ!
          +19
          ЧЗХ?
            +5
            один я не понимаю?
            • UFO just landed and posted this here
      +37
      Как в старом анекдоте: «я знаю замечательную шутку про UDP, но боюсь, что она до вас не дойдет».
        +22
        А вы слышали шутку про ICMP?
          +33
          Я знаю отличную шутку про TCP, но если она до вас не дойдет, то я повторю.

            +25
            Была еще одна шутка, про ARP. Кто-нибудь её знает? Расскажите!..
              +27
              Стойте, стойте все! Я расскажу шутку про прерывания!
                +19
                А теперь все слушайте анекдот про Broadcast.
                  +7
                  А я могу про Multicast в соседний топик рассказать!
                    –3
                    А я ща боян про электричество…
                      +4
                      crea7or, я тут шутку про unicast слышал…
                        –1
                        Кто-нибудь знает, где найти шутки про OSPF?
            +31
            Про MTU тоже есть кла
              0
              Вонни?
                +3
                Ну и естественно, никакой уважающий себя сетевой стек не собирается принимать пакеты с совершенно незнакомого адреса, даже если порты стоят правильные.


                Не понял, почему UDP сокет не должен принимать пакеты прилетевшие к нему? Какое ему дело с какого IP они прилетели? По какому признаку оно отличает свой/несвой?

                У вас везде маска 24, шлюз между подсетью 3 и подсетью 1 есть? Это я на всякий случай спросил)
                  0
                  Это статья. Потом ее кто-то перевел. А потом спроксировали сюда. Автор статьи без адреса. Переводчик тоже. И все это вписывается в маску темы.
                  Одно непонятно, кто будет отвечать?
                    0
                    Какой вы оптимист, однако.
                    Перевод мой. Отвечать могу я (и за перевод, и по содержанию).
                    +3
                    Насколько я понимаю, сокеты делятся на принимающие данные с любого удалённого адреса и те, которые ждут пакеты с конкретного адреса. Для первых ещё не определена ответная сторона. Они слушающие. Вторые полностью заданы. Они используются для передачи данных конкретному адресату. При получении пакета из слушающего сокета создаётся сокет для передачи данных. Это позволяет обслуживать несколько клиентов одновременно (или псевдопараллельно). Где в ядро происходит это копирование, я сейчас не готов рассказать (я просто плохо понимаю, что там происходит при приёме пакета).
                    Со стороны отправителя сетевой стек ждёт ответа с того адреса, куда он отправил данные. Например:
                    oxpa@oxpa-desktop:~$ netstat -unp
                    Proto Recv-Q Send-Q Local Address Foreign Address State       PID/Program name
                    udp        0      0 192.168.0.2:38940       8.8.8.8:53              ESTABLISHED 19597/operapluginwr
                    

                    Видно, что задан как локальный адрес (192.168...), так и удалённый (8.8.8.8). Только эти два хоста могут обмениваться данными. Данные от третьей машины будут отброшены 192.168.0.2 как «шум».
                    А с другой стороны, вот ntp слушает сеть:
                    udp        0      0 127.0.0.1:123           0.0.0.0:*   
                    

                    Ответная сторона не определена.
                    В этом случае, я получу данные от любой машины. Данные мне пожет послать кто угодно.
                    Обычно, клиентские приложения рассчитаны на ответ с того адерса, куда послан запрос. Такое что слушаем на одном порту, а отвечаем на другом — редкость. Примером может служить DHCP, где и получатель и отправитель на первых этапах обмена данными неизвестны.
                    Если получилось путано — переспросите, я поясню иначе. На другом примере ;)

                    Шлюз, естественно, есть между этими сетями.
                      +5
                      Если не делать connect(2) для UDP сокета, то он будет получать пакеты от любого хоста, не фильтруя их, то есть:
                          data->sock = socket ( PF_INET, SOCK_DGRAM, 0 );
                          if ( data->sock == -1 ) {
                              data->logger->log ( LOGGER_ERROR, "Can't allocate socket.\n" );
                              return -1;
                          }
                      
                          if ( bind ( data->sock, ( struct sockaddr* ) &data->si, sizeof ( data->si ) ) < 0 ) {
                              close ( data->sock );
                              data->sock = -1;
                              data->logger->log ( LOGGER_ERROR, "Can't bind socket.\n" );
                              return -1;
                          }


                      а теперь я буду ловить все пакеты на приходящие на порт указанный при bind'е:

                      rxed = recvfrom ( d->sock, rx_buff, sizeof ( rx_buff ), 0, ( struct sockaddr* ) &si_other, &slen );
                      


                      Выдержка из man connect(2)

                      int connect(int sockfd, const struct sockaddr *addr,
                                         socklen_t addrlen);
                      
                      


                      The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr. The addrlen argument specifies the size of addr. The format of the address in addr is determined by the address space of the socket sockfd; see socket(2) for further details.

                      If the socket sockfd is of type SOCK_DGRAM then addr is the address to which datagrams are sent by default, and the only address from which datagrams are received.


                      Так что все зависит от разработчика, как он сделает так и будет, а не от того, уважает себя TCP/IP стек или нет :-)
                        0
                        вы не могли бы привести полный исходник. Я пока не очень понимаю вашу мысль. То есть всё что вы говорите — верно, но какое имеет отношение к описанному выше я пока не пойму.
                          +2
                          Примеры echo клиента и сервера на C можно взять например здесь.

                          Я не в курсе реализации сокетов в скриптовых интерпретаторах, но в статье были фразы типа:

                          Ну и естественно, никакой уважающий себя сетевой стек не собирается принимать пакеты с совершенно незнакомого адреса, даже если порты стоят правильные.

                          Но на самом деле, это обычный дефект для протокола без установки сессии, такого как UDP.

                          Ещё раз: ядро не хранит никакой информации об отправителе или получателе, так как мы работаем без соединений.


                          Для UDP и имплементации TCP/IP в ядре Linux (да и для многих других имплементаций) эти утверждения не верные, так как нет проблем сделать connect(2), который заставит ядро думать о том, что соединение установлено с конкретным узлом и нет проблем сделать bind к конкретному локальному интерфейсу, вот собственно и все.
                            +1
                            Поймите меня правильно: я примерно представляю себе как устроен echo сервер. Проблем с реализацией у меня не возникало ни разу. Вы же предлагаете изменение, суть которого я не очень понимаю. Потому я прошу вас привести конкретную реализацию: я соберу именно тот исходник, который вы предложите, и попытаюсь на нём показать в чём проблема.
                            Насколько я вас понимаю, хорошо бы ещё отказаться и от nc как клиента. Так что я готов принять от вас ссылку на реализацию клиента до кучи. (Хотя лучше просто в комментарий выложить исходник).
                              +2
                              Я вам привел ссылку выше, там есть примеры на plain C, клиент там не делает connect, то есть, если сервер примет echo на один интерфейс, а ответит с другого, клиент ответ примет, но произойдет вот это:

                                  /* Check that client and server are using same socket */
                                  if (echoserver.sin_addr.s_addr != echoclient.sin_addr.s_addr) {
                                      Die("Received a packet from an unexpected server");
                                  }
                              


                              Вот исходники клиента и сервера по ссылке выше.

                              #include <stdio.h>
                              #include <sys/socket.h>
                              #include <arpa/inet.h>
                              #include <stdlib.h>
                              #include <string.h>
                              #include <unistd.h>
                              #include <netinet/in.h>
                              
                              #define BUFFSIZE 255
                              void Die(char *mess) {
                                  perror(mess);
                                  exit(1);
                              }
                              
                              int main(int argc, char *argv[]) {
                                  int sock;
                                  struct sockaddr_in echoserver;
                                  struct sockaddr_in echoclient;
                                  char buffer[BUFFSIZE];
                                  unsigned int echolen, clientlen;
                                  int received = 0;
                              
                                  if (argc != 4) {
                                      fprintf(stderr, "USAGE: %s <server_ip> <word> <port>\n", argv[0]);
                                      exit(1);
                                  }
                              
                              
                                  /* Create the UDP socket */
                                  if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
                                      Die("Failed to create socket");
                                  }
                                  /* Construct the server sockaddr_in structure */
                                  memset(&echoserver, 0, sizeof(echoserver));       /* Clear struct */
                                  echoserver.sin_family = AF_INET;                  /* Internet/IP */
                                  echoserver.sin_addr.s_addr = inet_addr(argv[1]);  /* IP address */
                                  echoserver.sin_port = htons(atoi(argv[3]));       /* server port */
                              
                              
                                  /* Send the word to the server */
                                  echolen = strlen(argv[2]);
                                  if (sendto(sock, argv[2], echolen, 0,
                                             (struct sockaddr *) &echoserver,
                                             sizeof(echoserver)) != echolen) {
                                      Die("Mismatch in number of sent bytes");
                                  }
                              
                              
                                  /* Receive the word back from the server */
                                  fprintf(stdout, "Received: ");
                                  clientlen = sizeof(echoclient);
                                  if ((received = recvfrom(sock, buffer, BUFFSIZE, 0,
                                                           (struct sockaddr *) &echoclient,
                                                           &clientlen)) != echolen) {
                                      Die("Mismatch in number of received bytes");
                                  }
                                  /* Check that client and server are using same socket */
                                  if (echoserver.sin_addr.s_addr != echoclient.sin_addr.s_addr) {
                                      Die("Received a packet from an unexpected server");
                                  }
                                  buffer[received] = '\0';        /* Assure null terminated string */
                                  fprintf(stdout, buffer);
                                  fprintf(stdout, "\n");
                                  close(sock);
                                  exit(0);
                              }
                              
                              


                              #include <stdio.h>
                              #include <sys/socket.h>
                              #include <arpa/inet.h>
                              #include <stdlib.h>
                              #include <string.h>
                              #include <unistd.h>
                              #include <netinet/in.h>
                              
                              #define BUFFSIZE 255
                              void Die(char *mess) {
                                  perror(mess);
                                  exit(1);
                              }
                              
                              
                              int main(int argc, char *argv[]) {
                                  int sock;
                                  struct sockaddr_in echoserver;
                                  struct sockaddr_in echoclient;
                                  char buffer[BUFFSIZE];
                                  unsigned int echolen, clientlen, serverlen;
                                  int received = 0;
                              
                                  if (argc != 2) {
                                      fprintf(stderr, "USAGE: %s <port>\n", argv[0]);
                                      exit(1);
                                  }
                              
                                  /* Create the UDP socket */
                                  if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
                                      Die("Failed to create socket");
                                  }
                                  /* Construct the server sockaddr_in structure */
                                  memset(&echoserver, 0, sizeof(echoserver));       /* Clear struct */
                                  echoserver.sin_family = AF_INET;                  /* Internet/IP */
                                  echoserver.sin_addr.s_addr = htonl(INADDR_ANY);   /* Any IP address */
                                  echoserver.sin_port = htons(atoi(argv[1]));       /* server port */
                              
                                  /* Bind the socket */
                                  serverlen = sizeof(echoserver);
                                  if (bind(sock, (struct sockaddr *) &echoserver, serverlen) < 0) {
                                      Die("Failed to bind server socket");
                                  }
                              
                                  /* Run until cancelled */
                                  while (1) {
                                      /* Receive a message from the client */
                                      clientlen = sizeof(echoclient);
                                      if ((received = recvfrom(sock, buffer, BUFFSIZE, 0,
                                                               (struct sockaddr *) &echoclient,
                                                               &clientlen)) < 0) {
                                          Die("Failed to receive message");
                                      }
                                      fprintf(stderr,
                                              "Client connected: %s\n", inet_ntoa(echoclient.sin_addr));
                                      /* Send the message back to client */
                                      if (sendto(sock, buffer, received, 0,
                                                 (struct sockaddr *) &echoclient,
                                                 sizeof(echoclient)) != received) {
                                          Die("Mismatch in number of echo'd bytes");
                                      }
                                  }
                              }
                              
                                +1
                                Да, всё так, как вы описали. Вот так выглядит клиентский сокет.
                                udp        0      0 0.0.0.0:45721               0.0.0.0:*                               21444/./client.udp  
                                

                                Он действительно не устанавливал соединение ни с кем, так что примет не то что ответ, просто пакет откуда угодно.
                                Но особых плюсов это не даёт, имхо. Способов избежать «Received a packet from an unexpected server» я не вижу.
                                Вообще, почему я стал писать этот перевод: есть софт типа openvpn или asterisk, который использует udp, но всё же коннектится к удалённому хосту. И при использовании алиасов на интерфейсах, возникают различные фокусы, как в описанном выше варианте. Переписывать этот софт не получится, однако проблемы можно избежать сконфигурив сервис по другому. Перевод в основном для этого сделан ;)
                                Про отправку пакетов без connect'а — любопытно. Как-то я в таком виде клиентов не реализовывал…
                    +4
                    Тема кажется до удивления знакомой.
                    Сдаётся мне автор наступил на граблю, которую можно кратко проиллюстрировать следующим примером:

                    -- запускаем netcat слушать
                    
                    1$ nc -u -l 0.0.0.0 10000
                    
                    -- смотрим что он открыл
                    
                    2$ netstat -anpu | grep 10000
                    udp        0      0 0.0.0.0:10000               0.0.0.0:*                               26242/nc
                    2$ lsof -p 26242 | grep UDP
                    nc      26242 jcmvbkbc    3u  IPv4 388760      0t0      UDP *:ndmp
                    
                    -- посылаем ему пакет
                    
                    3$ echo Hello | nc 127.0.0.1 10000
                    ^C
                    
                    -- смотрим что с ним стало
                    
                    2$ netstat -anpu | grep 10000
                    udp        0      0 127.0.0.1:10000             127.0.0.1:51359             ESTABLISHED 26242/nc
                    2$ lsof -p 26242 | grep UDP
                    nc      26242 jcmvbkbc    3u  IPv4 388760      0t0      UDP localhost:ndmp->localhost:51359
                    
                    -- смотрим что он напечатал
                    
                    1$ nc -u -l 0.0.0.0 10000
                    Hello
                    
                    -- шлём ему что-нибудь ещё, с другого порта... не шлётся
                    
                    3$ echo Hello | nc 127.0.0.1 10000
                    nc: Write error: Connection refused
                    ^C
                    
                    


                    Грабля в том, что сокет открытый nc после приёма первой UDP датаграммы переходит в состояние «соединение установлено», как ни смешно это прозвучит, т.е. связывает локальный адрес с адресом подходящего интерфейса, а удалённые адрес и порт — с адресом отправителя.

                    ИМХО это глюк, в данном случае — глюк nc.

                    Если вместо nc использовать нижеприведённый пример, то всё работает:

                    #include <sys/types.h>
                    #include <sys/socket.h>
                    #include <sys/ioctl.h>
                    #include <unistd.h>
                    #include <netinet/in.h>
                    
                    int main(int argc,char *argv[])
                    {
                      int s = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
                      int port = 10000;
                    
                      sockaddr_in sa = {};
                      sa.sin_family = AF_INET;
                      sa.sin_port = htons(port);
                      bind(s, (sockaddr*)&sa, sizeof(sa));
                      for (;;) {
                          char buf[100000];
                          ssize_t sz;
                    
                          sz = read(s, buf, sizeof(buf));
                          write(STDOUT_FILENO, buf, sz);
                      }
                      return 0;
                    }
                    


                    А именно, получение UDP-датаграммы не вызывает связывания принимающего сокета.
                      +1
                      Все верно, nc делает connect (я выше описал) я не поленился, глянул в его исходники:

                      Функция:

                      int netcat_socket_new_connect(int domain, int type, const struct in_addr *addr,
                      		in_port_t port, const struct in_addr *local_addr,
                      		in_port_t local_port)
                      

                      для всех type'ов сокетов делает connect:

                        /* now launch the real connection.  Since we are in non-blocking mode, this
                           call will return -1 in MOST cases (on some systems, a connect() to a local
                           address may immediately return successfully) */
                        ret = connect(sock, (struct sockaddr *)&rem_addr, sizeof(rem_addr));
                        if ((ret < 0) && (errno != EINPROGRESS)) {
                          ret = -5;
                          goto err;
                        }
                      
                        0
                        вы пропустили два «struct» в своём исходнике (у меня не компилилось, по крайней мере, пока не поправил).
                        Вы не могли бы привести пример исходника, который при этом шлёт ответ клиенту? Проблема не на приёме, проблема в доставке ответа клиенту.
                          0
                          Блин, не туда ответил сначала ответ написал

                          Это исходники nc фаил network.c
                            0
                            по поводу nc я понял. nc не используется как сервер. Только как клиент. Кроме того, в некоторых версиях nc это поведение можно обойти. Клюс -k заставляет nc слушать последующие соединения после завершения текущего. Как этот ключ сочетаетс с udp я не очень представляю.
                            0
                            Например так (ничего неожиданного):

                            #include <sys/types.h>
                            #include <sys/socket.h>
                            #include <sys/ioctl.h>
                            #include <unistd.h>
                            #include <netinet/in.h>
                            
                            int main(int argc,char *argv[])
                            {
                              int s = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
                              int port = 10000;
                            
                              struct sockaddr_in sa = {};
                              sa.sin_family = AF_INET;
                              sa.sin_port = htons(port);
                              bind(s, (struct sockaddr*)&sa, sizeof(sa));
                              for (;;) {
                                  char buf[100000];
                                  struct sockaddr client_sa;
                                  socklen_t client_sa_sz = sizeof(client_sa);
                                  ssize_t sz;
                            
                                  sz = recvfrom(s, buf, sizeof(buf), 0, &client_sa, &client_sa_sz);
                                  write(STDOUT_FILENO, buf, sz);
                                  sendto(s, buf, sz, 0, &client_sa, client_sa_sz);
                              }
                              return 0;
                            }
                            
                              0
                              с Quasar_ru разобрались: дело в общем-то не в сервере, а в клиенте. В чём именно — тоже разобрались. Спасибо.
                          0
                          Это исходники nc фаил network.c
                            0
                            Дело, очевидно, в nc. А я бы вообще посоветовал использовать разные сокеты для приёма и передачи udp пакетов. Ваша завязка на цикл “послал–принял” практически лишает смысла использование stateless протокола udp.

                            Only users with full accounts can post comments. Log in, please.