Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 54 (всего у книги 67 страниц)
Приложение для работы с базой данных компакт-дисков
Сейчас подходящий момент для модификации вашего приложения, управляющего базой данных компакт-дисков, с помощью средств IPC, с которыми вы познакомились в этой главе.
Вы могли бы применить множество разных комбинаций трех разновидностей средств IPC, но поскольку информация, которую следует передавать, очень мала по объему, есть смысл реализовать передачу запросов и ответов непосредственно с помощью очередей сообщений.
Если объемы данных, которые вы должны передавать, были бы велики, можно было бы рассмотреть передачу реальных данных в совместно используемой памяти, одновременно применяя семафоры или сообщения для отправки маркера или "опознавательного знака", информирующего другой процесс о наличии данных в совместно используемой памяти.
Интерфейс очереди сообщений устраняет проблему, которая у вас была в главе 11, когда вы нуждались в открытом канале у обоих процессов в момент передачи данных. Применение очередей сообщений позволяет одному процессу поместить сообщения в очередь, даже если этот процесс в данный момент – единственный пользователь очереди.
Вам нужно ответить лишь на один важный вопрос: как возвращать ответы клиентам? Простым решением было бы наличие одной очереди для сервера и по одной очереди для каждого клиента. Если одновременно существует много клиентов, такой подход может вызвать проблемы, т.к. потребуется большое количество очередей. Используя в сообщении поле идентификатора сообщения, вы сможете разрешить всем клиентам пользоваться одной очередью и адресовать ответные сообщения конкретным клиентским процессам с помощью включенного в сообщение идентификатора клиентского процесса. Далее каждый клиент может извлекать сообщения, адресованные только ему, оставляя сообщения для других клиентов в очереди.
Для преобразования приложения, работающего с базой данных компакт-дисков, с помощью средств IPC вам придется заменить только файл pipe_imp.c из сопроводительного программного кода к главе 13. Далее мы рассмотрим важные разделы замещающего файла ipc_imp.c.
Пересмотр функций сервераСначала нужно обновить серверные функции.
1. Прежде всего, включите необходимые заголовочные файлы, объявите несколько ключей очередей сообщений и структуру для хранения данных сообщения:
#include «cd_data.h»
#include «cliserv.h»
#include
#define SERVER_MQUEUE 1234
#define CLIENT_MQUEUE 4321
struct msg_passed {
long int msg_key; /* Используется для клиентского pid */
message_db_t real message;
};
2. Две глобальные переменные хранят идентификаторы двух очередей, возвращаемые функцией msgget
:
static int serv_qid = -1;
static int cli_qid = -1;
3. Сделайте сервер ответственным за создание обеих очередей сообщений:
int server starting() {
#if DEBUG_TRACE
printf(«%d :– server_starting()n», getpid());
#endif
serv_qid = msgget((key_t)SERVER_MQUEUE, 0666 | IPC_CREAT);
if (serv_qid == -1) return(0);
cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666 | IPC_CREAT);
if (cli_qid == -1) return(0);
return(1);
}
4. За удаление очереди, если она существует, также отвечает сервер. Когда сервер заканчивает работу, вы задаете недопустимые значения вашим глобальным переменным. Это позволит выловить любые ошибки при попытке сервера отправить сообщения после вызова функции server_ending
:
void server_ending() {
#if DEBUG_TRACE
printf(«%d :– server_ending()n», getpid());
#endif
(void)msgctl(serv_qid, IPC_RMID, 0);
(void)msgctl(cli_qid, IPC_RMID, 0);
servqid = -1;
cliqid = -1;
}
5. Серверная функция read
читает из очереди сообщение любого типа (т.е. от любого клиента) и возвращает часть сообщения с данными (пропуская тип сообщения):
int read_request_from_client(message_db_t *rec_ptr) {
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(«%d :– read_request_from_client()n», getpid());
#endif
if (msgrcv(serv_qid, (void *)&my_msg, sizeof(*rec_ptr), 0, 0) == -1) {
return(0);
}
*rec_ptr = my_msg.real_message;
return(1);
}
6. При отправке сообщения для его адресации используется ID клиентского процесса, хранящийся в запросе:
int send_resp_to_client(const message_db_t mess_to_send) {
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(«%d :– send_resp_to_client()n», getpid());
#endif
my_msg.real_message = mess_to_send;
my_msg.msg_key = mess_to_send.client_pid;
if (msgsnd(cli_qid, (void *)&my_msg, sizeof(mess_to_send), 0) == -1) {
return(0);
}
return(1);
}
Теперь нужно внести изменения в клиентские функции.
1. Когда клиент стартует, ему нужно найти идентификаторы серверной и клиентской очередей. Клиент не создает очереди. Если сервер не работает, эта функция завершится аварийно, поскольку не существует очередей сообщений.
int client starting() {
#if DEBUG_TRACE
printf(«%d :– client_startingn», getpid());
#endif
serv_qid = msgget((key_t)SERVER_MQUEUE, 0666);
if (serv_qid == -1) return(0);
cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666);
if (cli_qid == -1) return(0);
return(1);
}
2. Как и в случае сервера, когда клиент завершает работу, вы задаете некорректные значения глобальных переменных. Это позволит выявить ошибки при попытке клиента отправлять сообщения после вызова функции client_ending
.
void client_ending() {
#if DEBUG_TRACE
printf(«%d :– client_ending()n», getpid());
#endif
serv_qid = -1;
cli_qid = -1;
}
3. Для отправки сообщения серверу сохраните данные в своей структуре. Учтите, что вы должны задать ключ сообщения. Поскольку 0 – недопустимое значение для ключа, незаданный ключ означает, что он принимает (очевидно) случайное значение, поэтому иногда эта функция может возвращать ошибку, если значение оказывается нулевым.
int send_mess_to_server(message_db_t mess_to_send) {
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(«%d send_mess_to_server()n», getpid());
#endif
my_msg.real_message = mess_to_send;
my_msg.msg_key = mess_to_send.client_pid;
if (msgsnd(serv_qid, (void *)&my_msg, sizeof(mess_to_send) , 0) == -1) {
perror(«Message send failed»);
return(0);
}
return(1);
}
4. При получении сообщения от сервера клиент использует ID процесса для получения только сообщений, адресованных ему, пропуская сообщения, предназначенные другим клиентам.
int read_resp_from_server(message_db_t *rec_ptr) {
struct msg_passed mymsg;
#if DEBUG_TRACE
printf(«%d :– read_resp_from_server()n», getpid());
#endif
if (msgrcv(cli_qid, (void *)&my_msg, sizeof(*rec_ptr), getpid(), 0) == -1) {
return(0);
}
*rec_ptr = my_msg.real_message;
return(1);
}
5. Для сохранения совместимости с файлом pipe_imp.c необходимо объявить четыре дополнительные функции. Но в вашей программе они будут пустыми. Операции, которые они реализовывали в случае применения каналов, больше не нужны.
int start_resp_to_client(const message_db_t mess_to_send) {
return(1);
}
void end_resp_to_client(void) {}
int start_resp_from_server(void) {
return(1);
}
void end_resp_from_server(void) {}
Теперь вы можете просто запустить сервер, выполняющий в фоновом режиме реальное сохранение и извлечение данных, и затем выполнить клиентское приложение для подключения к серверу с помощью сообщений.
Все, что вы должны сделать, – это заменить интерфейсные функции из главы 11 другой реализацией, применяющей очереди сообщений. Преобразование приложения для использования очередей сообщений показывает мощь этого средства IPC, т.к. вам требуется меньше функций, чем в случае применения каналов, и даже эти необходимые функции гораздо проще, чем в предыдущей версии приложения.
Команды состояния IPC
Несмотря на то, что для соответствия требованиям X/Open этого не требуется, большинство систем Linux предоставляет набор команд, обеспечивающих доступ к данным IPC в режиме командной строки и удаление потерянных средств IPC. Существуют команды ipcs
и ipcrm
, очень полезные при разработке программ.
Один из досадных недостатков средств IPC состоит в том, что плохо написанная программа или программа, по какой-либо причине завершившаяся аварийно, может оставить свои ресурсы IPC (например, данные в очереди сообщений) еще долго блуждающими в системе без определенной цели после завершения программы. Такое поведение может привести к аварийному завершению нового запуска программы, поскольку она рассчитывает начать выполнение в очищенной системе, а на самом деле находит эти блуждающие ресурсы. Команды состояния (ipcs
) и удаления (ipcrm
) позволяют проверить систему и очистить ее от ненужных средств IPC.
Для проверки состояния семафоров в системе примените команду ipcs -s
. Если какие-то семафоры присутствуют, вывод команды будет выглядеть следующим образом:
$ ipcs -s
– Semaphore Arrays –
key semid owner perms nsems
0x4d00df1a 768 rick 666 1
Для удаления семафоров, случайно оставленных программами, вы можете использовать команду ipcrm
. Для удаления только что отображенного семафора примените (в Linux) следующую команду:
$ ipcrm -s 768
В некоторых более старых системах Linux используется несколько иной синтаксис команды:
$ ipcrm sem 768
Но этот устаревший стиль редко встречается в наше время. Формат, подходящий для вашей конкретной системы, ищите на страницах интерактивного справочного руководства.
Отображение состояния совместно используемой памятиМногие системы предоставляют программы режима командной строки для доступа не только к сведениям о семафорах, но и к подробным данным совместно используемой памяти. К ним относятся команды ipcs -m
и ipcrm -m <id>
(или ipcrm shm <id>
).
Далее приведен пример вывода команды ipcs -m
:
$ ipcs -m
– Shared Memory Segments –
key shmid owner perms bytes nattch status
0x00000000 384 rick 666 4096 2 dest
Здесь показан единственный сегмент совместно используемой памяти объемом 4 Кбайт, присоединенный к двум процессам.
Команда ipcrm -m <id>
позволяет удалить совместно используемую память. Она бывает полезной, когда программа завершается аварийно при попытке убрать такую память.
Для очередей сообщений предназначены команды ipcs -q
и ipcrm -q <id
> (или ipcrm msg <id>
).
Далее приведен пример вывода команды ipcs -q
:
$ ipcs -q
– Message Queues –
key msqid owner perms used-bytes messages
0x000004d2 3384 rick 666 2048 2
В нем показаны в очереди сообщений два сообщения общим объемом 2048 байтов. Команда ipcrm -q <id>
позволяет удалить очередь сообщений.
Резюме
В этой главе вы познакомились с тремя разновидностями средств взаимосвязи процессов, которые стали широко применяться в ОС UNIX System V.2 и были доступны в системе Linux, начиная с ранних версий ее дистрибутивов. Вы рассмотрели предлагаемые ими сложные функциональные возможности и, после того как поняли принципы их функционирования, оценили обеспечиваемое ими эффективное решение для удовлетворения многих потребностей межпроцессного взаимодействия.
Глава 15
Сокеты
В этой главе вы познакомитесь с еще одним способом взаимодействия процессов, существенно отличающимся от тех, которые мы обсуждали в главах 13 и 14. До настоящего момента все рассматриваемые нами средства основывались на совместно используемых ресурсах одного компьютера. Ресурсы могли быть разными: областью файловой системы, сегментами совместно используемой памяти или очередями сообщений, но использовать их могли только процессы, выполняющиеся на одной машине.
В версию ОС Berkeley UNIX было включено новое средство коммуникации – интерфейс сокетов, – являющееся расширением концепции канала, обсуждавшейся в главе 13. В системах Linux также есть интерфейсы сокетов.
Вы можете применять сокеты во многом так же, как каналы, но они поддерживают взаимодействие в пределах компьютерной сети. Процесс на одной машине может использовать сокеты для взаимосвязи с процессом на другом компьютере, что делает возможным существование клиент-серверных систем, распределенных в сети. Процессы, выполняющиеся на одной машине, также могут применять сокеты.
Кроме того, интерфейс сокетов стал доступен в ОС Windows благодаря общедоступной спецификации Windows Sockets или WinSock. Сервисы сокетов в ОС Windows предоставляются системным файлом Winsock.dll. Стало быть, программы под управлением Windows могут взаимодействовать по сети с компьютерами под управлением Linux и UNIX и наоборот, реализуя, таким образом, клиент-серверные системы. Несмотря на то, что программный интерфейс для WinSock не совпадает полностью с интерфейсом сокетов в UNIX, в основе его лежат те же сокеты.
В одной-единственной главе мы не сможем дать исчерпывающее описание всех многообразных сетевых возможностей Linux, поэтому вы найдете здесь лишь основные программные сетевые интерфейсы, которые позволят вам писать собственные программы, работающие в сети.
Более подробно мы рассмотрим следующие темы:
□ как действует соединение с помощью сокетов;
□ атрибуты сокетов, адреса и обмен информацией;
□ сетевая информация и интернет-демон (inetd/xinetd);
□ клиенты и серверы.
Что такое сокет?
Сокет – это средство связи, позволяющее разрабатывать клиент-серверные системы для локального, на одной машине, или сетевого использования. Функции ОС Linux, такие как вывод, подключение к базам данных и обслуживание Web-страниц, равно как и сетевые утилиты, например rlogin
, предназначенная для удаленной регистрации, и ftp
, применяемая для передачи файлов, обычно используют сокеты для обмена данными.
Сокеты создаются и используются не так, как каналы, потому что они подчеркивают явное отличие между клиентом и сервером. Механизм сокетов позволяет создавать множество клиентов, присоединенных к единственному серверу.
Соединения на базе сокетов
Соединения на базе сокетов можно рассматривать как телефонные звонки в учреждение. Телефонный звонок поступает в организацию, и на него отвечает секретарь приемной, направляющий вызов в соответствующий отдел (серверный процесс) и оттуда к нужному сотруднику (сокет сервера). Каждый входящий телефонный звонок (клиент) направляется к соответствующей конечной точке, и промежуточные операторы могут заниматься последующими телефонными звонками. Прежде чем рассматривать установку соединений с помощью сокетов в системах Linux, нужно понять, как они ведут себя в приложениях сокетов, поддерживающих соединения.
Сначала серверное приложение создает сокет, который как файловый дескриптор представляет собой ресурс, присваиваемый единственному серверному процессу. Сервер создает его с помощью системного вызова socket
, и этот сокет не может использоваться совместно с другими процессами.
Далее сервер присваивает сокету имя. Локальные сокеты с заданными именами файлов в файловой системе Linux часто размещаются в каталоге /tmp или /usr/tmp. У сетевых сокетов имя файла будет идентификатором сервиса (номер порта/точка доступа), относящегося к конкретной сети, к которой могут подключаться клиенты. Этот идентификатор, задавая определенный номер порта, соответствующий корректному серверному процессу, позволяет Linux направлять входящие подключения по определенному маршруту. Например, Web-сервер обычно создает сокет для порта 80, идентификатор, зарезервированный для этой цели. Web-обозреватели знают о необходимости применять порт 80 для своих HTTP-подключений к Web– сайтам, которые пользователь хочет читать. Именуется сокет с помощью системного вызова bind
. Далее серверный процесс ждет подключения клиента к именованному сокету. Системный вызов listen
формирует очередь входящих подключений. Сервер может принять их с помощью системного вызова accept
.
Когда сервер вызывает accept
, создается новый сокет, отличающийся от именованного сокета. Этот новый сокет применяется только для взаимодействия с данным конкретным клиентом. Именованный сокет сохраняется для дальнейших подключений других клиентов. Если сервер написан корректно, он может извлечь выгоду из многочисленных подключений. Web-сервер добивается этого за счет одновременного предоставления страниц многих клиентам. В случае простого сервера все последующие клиенты ждут в очереди до тех пор, пока сервер не будет готов снова.
Клиентская сторона системы с применением сокетов гораздо проще. Клиент создает неименованный сокет с помощью вызова socket
. Затем он вызывает connect
для подключения к серверу, используя в качестве адреса именованный сокет сервера.
Будучи установлены, сокеты могут применяться как низкоуровневые файловые дескрипторы, обеспечивая двунаправленный обмен данными.
Выполните упражнения 15.1 и 15.2.
Упражнение 15.1. Простой локальный клиент
Далее приведен пример очень простой клиентской программы client1.с. В ней неименованный сокет создается и затем подключается к сокету сервера, названному server_socket
. Системный вызов socket
мы подробно рассмотрим чуть позже, когда будем обсуждать некоторые проблемы адресации.
1. Включите нужные заголовочные файлы и задайте переменные:
#include
#include
#include
#include
#include
#include
int main() {
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch = 'A';
2. Создайте сокет для клиента:
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
3. Назовите сокет по согласованию с сервером:
address.sun_family = AF_UNIX;
strcpy(address.sun_path, «server_socket»);
len = sizeof(address);
4. Соедините ваш сокет с сокетом сервера:
result = connect(sockfd, (struct sockaddr *)&address, len);
if (result == -1) {
perror(«oops : client1»);
exit(1);
}
5. Теперь вы можете читать и писать через sockfd
:
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf(«char from server = %cn», ch);
close(sockfd);
exit(0);
}
Эта программа завершится аварийно, если вы попытаетесь выполнить ее, потому что еще не создан именованный сокет сервера, (Точное сообщение об ошибке может отличаться в разных системах.)
$ ./client1
oops: client1: No such file or directory
$
Упражнение 15.2. Простой локальный сервер
Далее приведена программа простого сервера server1.с, которая принимает запрос на соединение от клиента. Она создает сокет сервера, присваивает ему имя, создает очередь ожидания и принимает запросы на соединения.
1. Включите необходимые заголовочные файлы и задайте переменные:
#include
#include
#include
#include
#include
#include
int main() {
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
2. Удалите все старые сокеты и создайте неименованный сокет для сервера:
unlink(«server_socket»);
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
3. Присвойте имя сокету:
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, «server_socket»);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
4. Создайте очередь запросов на соединение и ждите запроса клиента:
listen(server_sockfd, 5);
while(1) {
char ch;
printf(«server waitingn»);
5. Примите запрос на соединение:
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
6. Читайте и записывайте данные клиента с помощью client_sockfd
:
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}
Как это работает
В этом примере серверная программа в каждый момент времени может обслуживать только одного клиента. Она просто читает символ, поступивший от клиента, увеличивает его и записывает обратно. В более сложных системах, где сервер должен выполнять больше работы по поручению клиента, такой подход будет неприемлемым, потому что другие клиенты не смогут подключиться до тех пор, пока сервер не завершит работу. Позже вы увидите пару методов, позволяющих подключаться многочисленным клиентам.
Когда вы выполняете серверную программу, она создает сокет и ждет запросов на соединение. Если вы запустите ее в фоновом режиме, т.е. она будет выполняться независимо, вы сможете затем запускать клиентов как высокоприоритетные задачи.
$ ./server1 &
[1] 1094
$ server waiting
Ожидая запросы на соединения, сервер выводит сообщение. В приведенном примере сервер ждет запрос с сокета файловой системы, и вы сможете увидеть его с помощью обычной команды ls
.
Хорошо взять за правило удалять сокет после окончания работы с ним, даже в случае аварийного завершения программы из-за получения сигнала. Это убережет файловую систему от загромождения неиспользуемыми файлами.
$ ls -lF server socket
srwxr-xr-x 1 neil users 0 2007-06-23 11:41 server_socket=
Здесь тип устройства – сокет, на что указывает символ s
перед правами доступа и символ =
в конце имени. Сокет был создан как обычный файл с правами доступа, модифицированными текущей umask
. Если применить команду ps
, то можно увидеть сервер, выполняющийся в фоновом режиме. Он показан спящим (параметр STAT
равен s
) и, следовательно, не потребляющим ресурсы ЦП.
$ ps lх
F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND
0 1000 23385 10689 17 0 1424 312 361800 S pts/1 0:00 ./server1
Теперь, когда вы запустите программу, то успешно подключитесь к серверу. Поскольку сокет сервера существует, вы можете соединиться с ним и обмениваться данными.
$ ./client1
server waiting char from server = В
$
На терминале вывод сервера и клиента перемешаны, но можно увидеть, что сервер получил символ от клиента, увеличил его и вернул. Далее сервер продолжает выполняться и ждет следующего клиента. Если вы запустите несколько клиентов вместе, они будут обслуживаться по очереди, хотя полученный вывод может оказаться еще более перемешанным.
$ ./client1 & ./client1 & ./client1 &
[2] 23412
[3] 23413
[4] 23414
server waiting
char from server = В
server waiting
char from server = В
server waiting
char from server = В
server waiting
[2] Done client1
[3]– Done client1
[4]+ Done client1
$