Эффективное программирование TCP-IP

       

Разберитесь, что такое подсоединенный UDP-сокет


| | |

Здесь рассказывается об использовании вызова connect применительно к протоколу UDP. Из совета 1 вам известно, что UDP - это протокол, не требующий установления соединений. Он передает отдельные адресованные конкретному получателю датаграммы, поэтому кажется, что слово «connect» (соединить) тут неуместно. Следует, однако, напомнить, что в листинге 3.6 вы уже встречались с примером, где вызов connect использовался в запускаемом через inetd UDP-сервере, чтобы получить (эфемерный) порт для этого сервера. Только так inetd мог продолжать прослушивать датаграммы, поступающие в исходный хорошо из­вестный порт.

Прежде чем обсуждать, зачем нужен вызов connect для UDP-сокета, вы должны четко представлять себе, что собственно означает «соединение» в этом контексте. При использовании TCP вызов connect инициирует обмен информацией о состоянии между сторонами с помощью процедуры трехстороннего квитирова­ния (рис. 3.14). Частью информации о состоянии является адрес и порт каждой стороны, поэтому можно считать, что одна из функций вызова connect в прото­коле TCP - это привязка адреса и порта удаленного хоста к локальному сокету.

Хотя полезность вызова connect в протоколе UDP может показаться сомнительной, но вы увидите, что, помимо некоторого повышения производительности, он позволяет выполнить такие действия, которые без него были бы невозможны. Рассмотрим причины использования соединенного сокета UDP сначала с точки зрения отправителя, а потом - получателя.

Прежде всего, от подсоединенного UDP-сокета вы получаете возможность ис-Щользования вызова send или write (в UNIX) вместо sendto.

Примечание: Для подсоединенного UDP-сокета можно использовать и вызов sendto, но в качестве указателя на адрес получателя надо задавать NULB, а в качестве его длины - нуль. Возможен, конечно, и вызов sendmsg, но и в этом случае поле msg_name в структуре msghdr должно содержать NULL, а поле msg_namel en - нуль.

Само по себе это, конечно, немного, но все же вызов connect действительно дает заметный выигрыш в производительности.


В реализации BSD sendto - это частный случай connect. Когда датаграмма посылается с помощью sendto, ядро временно соединяет сокет, отправляет датаграмму, после чего отсоединяет сокет. Изучая систему 4.3BSD и тесно связан­ную с ней SunOS 4.1.1, Партридж и Пинк [Partridge and Pink 1993] заметили, что такой способ соединения и разъединения занимает почти треть времени, уходяще­го на передачу датаграммы. Если не считать усовершенствования кода, который служит для поиска, управляющего блока протокола (РСВ - protocol control block) и ассоциирован с сокетом, исследованный этими авторами код почти без измене­ний вошел в систему 4.4BSD и основанные на ней, например FreeBSD. В частности, эти стеки по-прежнему выполняют временное соединение и разъединение. Таким образом, если вы собираетесь посылать последовательность UDP-дата-грамм одному и тому же серверу, то эффективность можно повысить, предварительно вызвав connect.
Этот выигрыш в производительности характерен только для некоторых реализаций. А основная причина, по которой отправитель UDP-датаграмм подсоединяет сокет, - это желание получать уведомления об асинхронных событиях. Пред­ставим, что надо послать UDP-датаграмму, но никакой процесс на другой стороне не прослушивает порт назначения. Протокол UDP на другом конце вернет ICMP-сообщение о недоступности порта, информируя тем самым ваш стек TCP/IP, но если сокет не подсоединен, то приложение не получит уведомления. Когда вы вызываете sendto, в начало сообщения добавляется заголовок, после чего оно передается уровню IP, где инкапсулируется в IP-датаграмму и помещается в выходную очередь интерфейса. Как только датаграмма внесена в очередь (или отослана, если очередь пуста), sendto возвращает управление приложению с кодом нормального завершения. Иногда через некоторое время (отсюда и термин асинхронный) приходит ICMP-сообщение от хоста на другом конце. Хотя в нем есть копия UDP-заголовка, у вашего стека нет информации о том, какое приложение посылало датаграмму (вспомните совет 1, где говорилось, что из-за отсутствия уста­новленного соединения система сразу забывает об отправленных датаграммах). Если же сокет подсоединен, то этот факт отмечается в управляющем блоке прото­кола, связанном с сокетом, и стек TCP/IP может сопоставить полученную копию UDP-заголовка с тем, что хранится в РСВ, чтобы определить, в какой сокет на­править ICMP-сообщение.


Можно проиллюстрировать данную ситуацию с помощью вашей программы udpclient (листинг 3.5) из совета 17 - следует отправить датаграмму в порт, который не прослушивает ни один процесс:
bsd: $ udpclient bed 9000


Hello, World!
^C     Клиент "зависает" и прерывается вручную.
bsd: $
Теперь модифицируем клиент, добавив такие строки
if ( connect! s, ( struct sockaddr * )&peer, sizeof( peer ) ) ) error( 1, errno, "ошибка вызова connect" );
сразу после вызова функции udp_client. Если назвать эту программу udpcona и запустить ее, то вы получите следующее:
bsd: $  udpconnl bed 9000
Hello,  World!
updconnl: ошибка вызова sendto: Socket is already connected (56)
bsd: $
Ошибка произошла из-за того, что вы вызвали sendto для подсоединенного сокета. При этом sendto потребовал от UDP временно подсоединить сокет. Но UDP определил, что сокет уже подсоединен и вернул код ошибки EISCONN.
Чтобы исправить ошибку, нужно заменить обращение к sendto на
rс = send( s, buf, strlen(   buf ), 0 );
Назовем новую программу udpconn2. После ее запуска получится такой результат:
bsd:   $ udpconnl bed 9000
Hello,  World!
updconn2: ошибка  вызова recvfrom: Connection refused (61)
bsd:   $
На этот раз ошибку ECONNREFUSED вернул вызов recvfrom. Эта ошибка - результат получения приложением ICMP-сообщения о недоступности порта.
Обычно у получателя нет причин соединяться с отправителем (если, конечно, ему самому не нужно стать отправителем). Однако иногда такая ситуация может быть полезна. Вспомним аналогию с телефонным разговором и почтой (совет 1) TCP-соединение похоже на частный телефонный разговор - в нем только два участника. Поскольку в протоколе TCP устанавливается соединение, каждая сторона зна­ет о своем партнере и может быть уверена, что всякий полученный байт действительно послал партнер.
С другой стороны, приложение, получающее UDP-датаграммы, можно сравнить с почтовым ящиком. Как любой человек может отправить письмо по данному адресу, так и любое приложение или хост может послать приложению-получателю датаграмму, если известны адрес и номер порта.


Иногда нужно получать датаграммы только от одного приложения. Получающее приложение добивается этого, соединившись со своим партнером. Чтобы увидеть, как это работает, напишем UDP-сервер эхо-контроля, который соединяется с первым клиентом, отправившим датаграмму (листинг 3.35).
Листинг 3.35. UDP-сервер эхо-контроля, выполняющий соединение
1    #include   "etcp.h"     :
2    int main( int argc, char **argv )
3    {
4    struct sockaddr_in peer;
5    SOCKET s;
6    int rс;
7    int len;
8    char buf[ 120 ];
9    INIT();
10   s = udp_server( NULL, argv[ 1 ] ) ;
11   len = sizeof( peer );
12   rс = recvfrom( s, buf, sizeoff buf ),
13   0, ( struct sockaddr * )&peer, &len );
14   if ( rс < 0 )
15     error( 1, errno, "ошибка вызова recvfrom" );
16   if ( connect( s, ( struct sockaddr * )&peer, len ) )
17     error( 1, errno, "ошибка вызова connect" );
18   while ( strncmp( buf, "done", 4 ) != 0 )
19   {
20     if ( send( s, buf, rс, 0 ) < 0 )
21      error( 1, errno, "ошибка вызова send" );
22     rc = recv( s, buf, sizeof( buf ), 0 );
23     if ( rс < 0 )
24      error( 1, errno, "ошибка вызова recv" );
25   }
26   EXIT( 0 );
27   }
9-15 Выполняем стандартную инициализацию UDP и получаем первую датаграмму, сохраняя при этом адрес и порт отправителя в переменной peer.
16-17 Соединяемся с отправителем.
18-25 В цикле отсылаем копии полученных датаграмм, пока не придет датаграмма, содержащая единственное слово «done».
Для экспериментов с сервером udpconnserv можно воспользоваться клиентом udpconn2. Сначала запускается сервер для прослушивания порта 9000 в ожи­дании датаграмм:
udpconnserv  9000
а затем запускаются две копии udpconn2, каждая в своем окне.

bsd: $  udpconn2 bsd 9000
one
one
three
three
done
^C
bsd:  $
bsd: $  udpconn2 bsd 9000
two
udpconn2: ошибка вызова  recvfroin:
   Connection refused   (61)
bsd:  $

Когда в первом окне вы набираете one, сервер udpconnserv возвращает копию датаграммы. Затем во втором окне вводите two, но recvf rom возвращает код ошибки ECONNREFUSED. Это происходит потому, что UDP вернул ICMP-сообщение о недоступности порта, так как ваш сервер уже соединился с первым экземпляром udpconn2 и не принимает датаграммы с других адресов.
Примечание: Адреса отправителя у обоих экземпляров udpconn2, конечно, одинаковы, но эфемерные порты, выбранные стеком TCP/IP, различны. В первом окне вы набираете three, дабы убедиться, что udpconnserv все еще функционирует, а затем — done, чтобы остановить сервер. В конце прерываем вручную первый экземпляр udpconn2.
Как видите, udpconnserv не только отказывается принимать датаграммы от другого отправителя, но также информирует приложение об этом факте, посылая ICMP-сообщение. Разумеется, чтобы получить это сообщение, клиент также должен подсоединиться к серверу. Если бы вы прогнали этот тест с помощью первоначальной версии клиента udpclient вместо udpconn2, то второй экземпляр клента просто «завис» после ввода слова «done».

Содержание раздела