Выбрать главу

75  int n;

76  if ((n = pthread_mutex_lock(mptr)) == 0)

77   return;

78  errno = n;

79  err_sys("pthread_mutex_lock error");

80 }

ПРИМЕЧАНИЕ

Если аккуратно программировать на С, можно использовать макросы вместо функций, что обеспечивает небольшой выигрыш в производительности, однако функции- обертки редко, если вообще когда-нибудь бывают причиной недостаточной производительности программ.

Наш выбор — первая заглавная буква в названии функции — является компромиссом. Было предложено множество других стилей: подстановка префикса e перед названием функции (как сделано в [67, с. 182]), добавление _е к имени функции и т.д. Наш вариант кажется наименее отвлекающим внимание и одновременно дающим визуальное указание на то, что вызывается какая-то другая функция.

Эта технология имеет, кроме того, полезный побочный эффект: она позволяет проверять возникновение ошибок при выполнении таких функций, ошибки в которых часто остаются незамеченными, например close и listen.

На протяжении всей книги мы будем использовать эти функции-обертки, кроме тех случаев, когда нам нужно проверить ошибку явно и обрабатывать ее другим, отличным от прерывания программы, способом. Мы не приводим исходный код для всех наших собственных функций-оберток, но он свободно доступен в Интернете (см. предисловие).

Значение системной переменной Unix errno

Когда при выполнении функции Unix (например, одной из функций сокетов) происходит ошибка, глобальной переменной errno присваивается положительное значение, указывающее на тип ошибки, а возвращаемое значение функции обычно равно -1. Наша функция err_sys проверяет значение переменной errno и печатает строку с соответствующим сообщением об ошибке (например, «Время соединения истекло», если значение переменной errno равно ETIMEDOUT).

Переменная errno устанавливается равной определенному значению, только если при выполнении функции произошла какая-либо ошибка. Ее значение не определено, если функция не возвращает ошибки. Все положительные значения ошибок являются константами с именами в верхнем регистре, начинающимися на «E», и обычно определяются в заголовке <sys/errno.h>. Ни одна ошибка не имеет кода 0.

Переменную errno нельзя хранить как глобальную переменную в случае множества потоков, у которых все глобальные переменные являются общими. О решении этой проблемы мы расскажем в главе 23.

На протяжении всего текста книги мы использовали фразы типа «функция connect возвращает ECONNREFUSED» для сокращенного обозначения того, что при выполнении функции произошла ошибка (обычно при этом возвращаемое значение функции равно -1), и значение переменной errno стало равным указанной константе.

1.5. Простой сервер времени и даты

Мы можем написать простую версию сервера TCP для определения времени и даты, который будет работать с клиентом, описанным в разделе 1.2. Мы используем функции-обертки, описанные в предыдущем разделе. Код сервера приведен в листинге 1.5.

Листинг 1.5. TCP-сервер времени и даты

//intro/daytimetcpsrv.c

 1 #include "unp.h"

 2 #include <time.h>

 3 int

 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

10 Создание сокета TCP выполняется так же, как и в клиентском коде.

Связывание заранее известного порта сервера с сокетом

11-15 Заранее известный порт сервера (13 в случае сервера времени и даты) связывается с сокетом путем заполнения структуры адреса интернет-сокета и вызова функции bind. Мы задаем IP-адрес как INADDR_ANY, что позволяет серверу принимать соединение клиента на любом интерфейсе в том случае, если узел сервера имеет несколько интерфейсов. Далее мы рассмотрим, как можно ограничить прием соединений одним-единственным интерфейсом.

Преобразование сокета в прослушиваемый сокет

16 С помощью вызова функции listen сокет преобразуется в прослушиваемый, то есть такой, на котором ядро принимает входящие соединения от клиентов. Эти три этапа, socket, bind и listen, обычны для любого сервера TCP при создании того, что мы называем прослушиваемым дескриптором (listening descriptor) (в нашем примере это переменная listenfd).

Константа LISTENQ взята из нашего заголовочного файла unp.h. Она задает максимальное количество клиентских соединений, которые ядро ставит в очередь на прослушиваемом сокете. Более подробно мы расскажем о таких очередях в разделе 4.5.

Прием клиентского соединения, отправка ответа

17-21 Обычно процесс сервера блокируется при вызове функции accept, ожидая принятия подключения клиента. Для установки TCP-соединения используется трехэтапное рукопожатие (three-way handshake). Когда рукопожатие состоялось, функция accept возвращает значение, и это значение является новым дескриптором (connfd), который называется присоединенным дескриптором (connected descriptor). Этот новый дескриптор используется для связи с новым клиентом. Новый дескриптор возвращается функцией accept для каждого клиента, соединяющегося с нашим сервером.

ПРИМЕЧАНИЕ

Стиль, используемый в книге для обозначения бесконечного цикла, выглядит так:

for (;;) {

 ...

}

Библиотечная функция time возвращает количество секунд с начала эпохи Unix: 00:00:00 1 января 1970 года UTC (Universal Time Coordinated — универсальное синхронизированное время, среднее время по Гринвичу). Следующая библиотечная функция, ctime, преобразует целочисленное значение секунд в строку следующего формата, удобного для человеческого восприятия:

Fri Jan 12 14:27:52 1996

Возврат каретки и пустая строка добавляются к строке функцией snprintf, а результат передается клиенту функцией write.

ПРИМЕЧАНИЕ

Если вы еще не выработали у себя привычку пользоваться функцией snprintf вместо устаревшей sprintf, сейчас самое время заняться этим. Функция sprintf не в состоянии обеспечить проверку переполнения буфера получателя. Функция snprintf, наоборот, требует, чтобы в качестве второго аргумента указывался размер буфера получателя, переполнение которого таким образом предотвращается.