UNIX: разработка сетевых приложений
4 main(int argc, char **argv)5 {6 int listenfd, connfd;7 struct sockaddr_in servaddr;8 char buff[MAXLINE];9 time_t ticks;10 listenfd = Socket(AF_INET, SOCK_STREAM, 0);11 bzero(&servaddr, sizeof(servaddr));12 servaddr.sin_family = AF_INET;13 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);14 servaddr.sin_port = htons(13); /* сервер времени и даты */15 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));16 Listen(listenfd, LISTENQ);17 for (;;) {18 connfd = Accept(listenfd, (SA*)NULL, NULL);19 ticks = time(NULL);20 snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));21 Write(connfd. buff, strlen(buff));22 Close(connfd);23 }24 }Создание сокета TCPСоздание сокета TCP выполняется так же, как и в клиентском коде.10Связывание заранее известного порта сервера с сокетомЗаранее известный порт сервера (13 в случае сервера времени и даты) связывается с сокетом путем заполнения структуры адреса интернет-сокета и вызова функции11-15. Мы задаем IP-адрес какbind, что позволяет серверу принимать соединение клиента на любом интерфейсе в том случае, если узел сервера имеет несколько интерфейсов. Далее мы рассмотрим, как можно ограничить прием соединений одним-единственным интерфейсом.INADDR_ANYПреобразование сокета в прослушиваемый сокетС помощью вызова функции16сокет преобразуется в прослушиваемый, то есть такой, на котором ядро принимает входящие соединения от клиентов. Эти три этапа,listen,socketиbind, обычны для любого сервера TCP при создании того, что мы называем прослушиваемым дескриптором (listening descriptor) (в нашем примере это переменнаяlisten).listenfdКонстанта
взята из нашего заголовочного файлаLISTENQ. Она задает максимальное количество клиентских соединений, которые ядро ставит в очередь на прослушиваемом сокете. Более подробно мы расскажем о таких очередях в разделе 4.5.unp.hПрием клиентского соединения, отправка ответаОбычно процесс сервера блокируется при вызове функции17-21, ожидая принятия подключения клиента. Для установки TCP-соединения используется трехэтапное рукопожатие (three-way handshake). Когда рукопожатие состоялось, функция accept возвращает значение, и это значение является новым дескриптором (accept), который называется присоединенным дескриптором (connected descriptor). Этот новый дескриптор используется для связи с новым клиентом. Новый дескриптор возвращается функциейconnfdдля каждого клиента, соединяющегося с нашим сервером.acceptПРИМЕЧАНИЕСтиль, используемый в книге для обозначения бесконечного цикла, выглядит так:
for (;;) {...}Библиотечная функция
возвращает количество секунд с начала эпохи Unix: 00:00:00 1 января 1970 года UTC (Universal Time Coordinated — универсальное синхронизированное время, среднее время по Гринвичу). Следующая библиотечная функция,time, преобразует целочисленное значение секунд в строку следующего формата, удобного для человеческого восприятия:ctimeFri Jan 12 14:27:52 1996Возврат каретки и пустая строка добавляются к строке функцией
, а результат передается клиенту функциейsnprintf.writeПРИМЕЧАНИЕЕсли вы еще не выработали у себя привычку пользоваться функцией snprintf вместо устаревшей sprintf, сейчас самое время заняться этим. Функция sprintf не в состоянии обеспечить проверку переполнения буфера получателя. Функция snprintf, наоборот, требует, чтобы в качестве второго аргумента указывался размер буфера получателя, переполнение которого таким образом предотвращается.
Функция snprintf была добавлена в стандарт ANSI С относительно нравно, в версии ISO C99. Практически все поставщики программного обеспечения уже сейчас включают эту функцию в стандартную библиотеку языка С. Существуют и свободно распространяемые реализации. В нашей книге мы используем функцию snprintf и рекомендуем вам пользоваться ею в своих программах для повышения их надежности.
Удивительно много сетевых атак было реализовано хакерами с использованием незащищенности sprintf от переполнения буфера. Есть еще несколько функций, с которыми нужно быть аккуратными: gets, strcat и strcpy. Вместо них лучше использовать fgets, strncat и strncpy. Еще лучше работают более современные функции strlcat и strlcpy, возвращающие в качестве результата правильно завершенную строку. Полезные советы, касающиеся написания надежных сетевых программ, можно найти в главе 23 книги [32].
Завершение соединенияСервер закрывает соединение с клиентом, вызывая функцию22. Это инициирует обычную последовательность прерывания соединения TCP: пакет FIN посылается в обоих направлениях, и каждый пакет FIN распознается на другом конце соединения. Более подробно трехэтапное рукопожатие и четыре пакета TCP, используемые для прерывания соединения, будут описаны в разделе 2.6.closeСервер времени и даты был рассмотрен нами достаточно кратко, как и клиент из предыдущего раздела. Запомните следующие моменты.
■ Сервер, как и клиент, зависим от протокола IPv4. В листинге 11.7 мы покажем версию, не зависящую от протокола, которая использует функцию
.getaddrinfo■ Наш сервер обрабатывает только один запрос клиента за один раз. Если приблизительно в одно время происходит множество клиентских соединений, ядро ставит их в очередь, максимальная длина которой регламентирована, и передает эти соединения функции accept по одному за один раз. Наш сервер времени и даты, который требует вызова двух библиотечных функций, time и ctime, является достаточно быстрым. Но если у сервера обслуживание каждого клиента занимает больше времени (допустим, несколько секунд или минуту), нам будет необходимо некоторым образом организовать одновременное обслуживание нескольких клиентов.