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

       

Сервер должен устанавливать опцию SO_REUSEADDR


| | |

В сетевых конференциях очень часто задают вопрос: «Когда сервер «падает» или нормально завершает сеанс, я пытаюсь его перезапустить и получаю ошибку «Address already in use». А через несколько минут сервер перезапускается нормаль но. Как сделать так, чтобы сервер рестартовал немедленно?» Чтобы проиллюстрировать эту проблему, напишем сервер эхо-контроля, который будет работа именно так (листинг 3.22).

Листинг 3.22. Некорректный сервер эхо-контроля

1    #include "etcp.h"

2    int  main( int   argc,   char   **argv)

3   {

4   struct sockaddr_in local;

5   SOCKET s;

6   SOCKET s1;

7   int rc;

8   char buf[ 1024 ];

9    INIT();

10   s = socket( PF_INET, SOCK_STREAM, 0 );



11   if ( !isvalidsock( s ) )

12     error( 1, errno, "He могу получить сокет" ) ;

13   bzero( &local, sizeof( local ) );

14   local.sin_family = AF_INET;

15   local.sin_port = htons( 9000 );

16   local.sin_addr.s_addr = htonl( INADDR_ANY );

17   if ( bind( s, ( struct sockaddr * )&local,

18     sizeof( local ) ) < 0 )

19     error( 1, errno, "He могу привязать сокет" );

20   if ( listen) s, NLISTEN ) < 0 )

21     error( 1, errno, "ошибка вызова listen" );

22   si = accept! s, NULL, NULL );

23   if ( !isvalidsock( s1 ) )

24     error( 1, errno, "ошибка вызова accept" );

25   for ( ;; )

26   {

27     rc = recv( s1, buf, sizeof( buf ), 0 );

28     if ( rc < 0 )

29      error( 1, errno, "ошибка вызова recv" );

30     if ( rc == 0 )

31      error( 1, 0, "Клиент отсоединился\n" );

32     rc = send( s1, buf, rc, 0 );

33     if ( rc < 0 )

34      error( 1, errno, "ошибка вызова send" );

35   }

36   }

На первый взгляд, сервер выглядит вполне нормально, только номер порта «зашит» в код. Если запустить его в одном окне и соединиться с ним с помощью программы telnet, запущенной в другом окне, то получится ожидаемый результат. (На рис. 3.9 опущены сообщения telnet об установлении соединения.)


Проверив, что сервер работает, останавливаете клиента, переходя в режим команд telnet и вводя команду завершения. Обратите внимание, что если немедленно повторить весь эксперимент, то будет тот же результат. Таким образом, adserver перезапускается без проблем.
А теперь проделайте все еще раз, но только остановите сервер. При попытке перезапустить сервер вы получите сообщение «Address already in use» (сообщение Разбито на две строчки). Разница в том, что во втором эксперименте вы остановили сервер, а не клиент рис. 3.10.

bsd $ badserver
badserver: Клиент отсоединился
bsd $ : badserver
badserver : Клиент отсоединился
bsd $
bsd $ telnet localhost 9000
hello
hello
^]
telnet> quit Клиент завершил сеанс.
Connection closed.
Сервер перезапущен.
bsd $ telnet localhost 9000
world
world
^]
telnet> quit Клиент завершил сеанс.
Connection closed
bsd $

Рис. 3.9. Завершение работы клиента

bsd $ badeerver
^C Сервер остановлен
bsd $ badserver
badserver: He могу привязать сокет:
Address already in use (48)
bsd $
bsd $ telnet localhost 9000
hello again
hello again
Connection closed by
foreign host
bsd $

Рис. 3.10. Завершение работы сервера
Чтобы разобраться, что происходит, нужно помнить о двух вещах:
  • состоянии TIME-WAIT протокола TCP;

  • TCP-соединение полностью определено четырьмя факторами (локальный адрес, локальный порт, удаленный адрес, удаленный порт).

  • Как было сказано в совете 22, сторона соединения, которая выполняет актив­ное закрытие (посылает первый FIN), переходит в состояние TIME-WAIT и остается в нем в течение 2MSL. Это первый ключ к пониманию того, что вы наблюдали в двух предыдущих примерах: если активное закрытие выполняет клиент, то можно перезапустить обе стороны соединения. Если же активное закрытие выполняет сервер, то его рестартовать нельзя. TCP не позволяет это сделать, так как предыдущее соединение все еще находится в состоянии TIME-WAIT.
    Если бы сервер перезапустился и с ним соединился клиент, то возникло новое соединение, возможно, даже с другим удаленным хостом. Как было сказано, TCP-соединение полностью определяется локальными и удаленными адресами и номерами портов, так что даже если с вами соединился клиент с того же у ленного хоста, проблемы не возникнет при другом номере удаленного порта.


    Примечание: Даже если клиент с того же удаленного хоста воспользуется тем же номером порта, проблемы может и не возникнуть. Традиционно реализация BSD разрешает такое соединение, если только порядковый номер посланного клиентом сегмента SYN больше последнего порядкового номера, зарегистрированного соединением, которое находится в состоянии TIME- WAIT.
    Возникает вопрос: почему TCP возвращает ошибку, когда делается попытка перезапустить сервер? Причина не в TCP, который требует только уникальности указанных факторов, а в API сокетов, требующем двух вызовов для полного определения этой четверки. В момент вызова bind еще неизвестно, последует ли за ним connect, и, если последует, то будет ли в нем указано новое соединение, или он попытается повторно использовать существующее. В книге [Torek 1994] автор - и не он один - предлагает заменить вызовы bind, connect и listen одной функцией, реализующей функциональность всех трех. Это даст возможность TCP выявить, действительно ли задается уже используемая четверка, не отвергая попы­ток перезапустить закрывшийся сервер, который оставил соединение в состоянии TIME-WAIT. К сожалению, элегантное решение Терека не было одобрено.
    Но существует простое решение этой проблемы. Можно разрешить TCP при­вязку к уже используемому порту, задав опцию сокета SO_REUSEADDR. Чтобы про­верить, как это работает, вставим между строками 7 и 8 файла badserver. с строку
    const int on = 1;
    а между строками 12 и 13 - строки
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeoff on ) ) )
    error( 1, errno, "ошибка  вызова  setsockopt");
    Заметьте, что вызов setsockopt должен предшествовать вызову bind. Если назвать исправленную программу goodserver и повторить эксперимент (рис. 3.11), то получите такой результат:

    bsd $ goodserver
    ^С Сервер остановлен.
    bsd $
    bsd $ telnet localhost 9000
    hello once again
    hello once again
    Connection closed by foreign host
    Сервер перезапущен.
    bsd $ telnet localhoet 9000
    hello one last time
    hello one last time
    <


    Рис. 3.11. Завершение работы сервера, в котором используется опция SO_REUSEADDR
    Теперь вы смогли перезапустить сервер, не дожидаясь выхода предыдущего соединения из состояния TIME-WAIT. Поэтому в сервере всегда надо устанавливать опцию сокета SO_REUSEADDR. Обратите внимание, что в предлагаемом каркасе в функции tcp_server это уже делается.
    Некоторые, в том числе авторы книг, считают, что задание опции SO_REUSEADDR опасно, так как позволяет TCP создать четверку, идентичную уже используемой, и таким образом создать проблему. Это ошибка. Например, если попытаться создать два идентичных прослушивающих сокета, то TCP отвергнет операцию привязки даже если вы зададите опцию SO_REUSEADDR:
    bsd $ goodserver &
    [1] 1883
    bsd $ goodserver
    goodserver: He могу привязать сокет: Address already in use (48)
    bsd $
    Аналогично если вы привяжете одни и те же локальный адрес и порт к двум разным клиентам, задав SO_REUSEADDR, то bind для второго клиента завершится успешно. Однако на попытку второго клиента связаться с тем же удаленным хос­том и портом, что и первый, TCP ответит отказом.
    Помните, что нет причин, мешающих установке опции SO_REUSEADDR в сервере. Это позволяет перезапустить сервер сразу после его завершения. Если же этого не сделать, то сервер, выполнявший активное закрытие соединения, не перезапустится.
    Примечание: В книге [Stevens 1998] отмечено, что с опцией SO_REUSEADDR связана небольшая проблема безопасности. Если сервер привязывает универсальный адрес INADDR_ANY, как это обычно и делается, то другой сервер может установить опцию SO_REUSEADDR и привязать тот же порт, но с конкретным адресом, «похитив» тем самым соединение у первого сервера. Эта проблема действительно существует, особенно для сетевой файловой системы (NFS) даже в среде UNIX, поскольку NFS привязывает порт 2049 из открытого всем диапазона. Однако такая опасность существует не из-за использования NFS опции SO_REUSEADDR, а потому что это может сделать другой сервер. Иными словами, эта опасность имеет место независимо от установки SO_REUSEADDR,так что это не причина для отказа от этой опции.


    Следует отметить, что у опции SO_REUSEADDR есть и другие применения. Предположим, например, что сервер работает на машине с несколькими сетевыми интерфейсами и ему необходимо иметь информацию, какой интерфейс клиент указал в качестве адреса назначения. При работе с протоколом TCP это легко, так как серверу достаточно вызвать getsockname после установления соедине­ния. Но, если реализация TCP/IP не поддерживает опции сокета IP_RECVDSTADDR, то UDP-сервер так поступить не может. Однако UDP-сервер может решить эту задачу, установив опцию SO_REUSEADDR и привязав свой хорошо известный порт к конкретным, интересующим его интерфейсам, а универсальный адрес INADDR_ANY - ко всем остальным интерфейсам. Тогда сервер определит указанный клиентом адрес по сокету, в который поступила датаграмма.
    Аналогичная схема иногда используется TCP- и UDP-серверами, которые хотят предоставлять разные варианты сервиса в зависимости от адреса, указанного клиентом. Допустим, вы хотите использовать свою версию tcpmux (совет 18) Для предоставления одного набора сервисов, когда клиент соединяется с интерфейсов
    По адресу 198.200.200.1, и другого - при соединении клиента с иным интерфейсом. Для этого запускаете экземпляр tcpmux со специальными сервисами на ин­терфейсе 198.200.200.1, а экземпляр со стандартными сервисами - на всех остальных интерфейсах, указав универсальный адрес INADDR_ANY. Поскольку tcpmux устанавливает опцию SO_REUSEADDR, TCP позволяет повторно привязать порт 1, хотя при второй привязке указан универсальный адрес.
    И, наконец, SO_REUSEADDR используется в системах с поддержкой группового вещания, чтобы дать возможность одновременно нескольким приложениям прослушивать входящие датаграммы, вещаемые на группу. Подробнее это рассматри­вается в книге [Stevens 1998].

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