Московский государственный
университет им. М.В. Ломоносова
Факультет вычислительной математики и кибернетики
Волкова И.А., Головин И.Г., Кузина Л.Н., Мальковский М.Г.
Модельный SQL-интерпретатор
(Издание третье, переработанное)
2005
УДК 519.6+681.3.06
В данном методическом пособии описывается задание практикума на ЭВМ для
студентов 2 курса факультета вычислительной математики и кибернетики в поддержку основного курса «Системы программирования». Приводятся подробные методические пояснения и рекомендации.
Третье издание переработано в связи с изменением языка реализации — переход на объектно-ориентированный язык программирования Си++.
Авторы пособия благодарят М.О.Проскурню за ценные советы и замечания.
Рецензенты:
проф. А.Н.Сосников доц. Л.С.Корухова Волкова И.А., Головин И.Г., Кузина Л.Н., Мальковский М.Г.
«Модельный SQL-интерпретатор (Методическое пособие)» — издание третье, переработанное.
Издательский отдел факультета ВМиК МГУ (лицензия ЛР №040777 от 23.07.96), 2005 — 32 с.
Печатается по решению Редакционно-Издательского Совета факультета Вычислительной Математики и Кибернетики МГУ им. М.В. Ломоносова.
ISBN 5-89407-033- © Издательский отдел факультета вычислительной математики и кибернетики МГУ им. М.В. Ломоносова, Замечания по данной электронной версии присылайте на сmсmsu.infо@gmail.cоm Содержание 1. ПОСТАНОВКА ЗАДАЧИ
1.1. ВАРИАНТЫ ЗАДАНИЯ
1.2. ТРЕБОВАНИЯ К РЕАЛИЗАЦИИ
1.3. СОДЕРЖАНИЕ ОТЧЕТА
2. МЕТОДИЧЕСКИЕ УКАЗАНИЯ
2.1. Моделирование архитектуры «Клиент — Сервер»
2.2. Пример-оболочка программы «Клиент»
2.3. Пример-оболочка программы «Сервер»
2.4. Пример-оболочка программы «Клиент» в объектно-ориентированном стиле
2.5. Пример-оболочка программы «Сервер» в объектно-ориентированном стиле
3. Средства межпроцессного взаимодействия для сети ЭВМ........... 3.1. Преобразование сетевого имени в сетевой адрес
3.2. Порядок байтов в сети
3.3. Функции работы с сетью ЭВМ
3.4. Пример-оболочка программы «Сервер» для сети ЭВМ
3.5. Пример-оболочка программы «Клиент» для сети ЭВМ в объектноориентированном стиле
3.6. Пример-оболочка программы «Сервер» для сети ЭВМ в объектноориентированном стиле
4. БД и СУБД
4.1. Описание модельного языка SQL
4.2. Примеры предложений модельного SQL
5. ПРИЛОЖЕНИЕ
6. ЛИТЕРАТУРА
Модельный SQL-интерпретатор. Методическое пособие.
1. ПОСТАНОВКА ЗАДАЧИ
Задача реализации модельного SQL-интерпретатора разбивается на следующие подзадачи:1. Реализация архитектуры «Клиент — Сервер».
2. Реализация SQL-интерпретатора.
3. Реализация модельного SQL-сервера на базе разработанного интерпретатора и предоставленной библиотеки классов для работы с файлами данных.
4. Реализация интерфейса пользователя с модельным SQL-сервером.
5. Реализация модельной базы данных, демонстрирующей работоспособность программ.
Язык реализации — Си++ [1].
Архитектура «Клиент — Сервер» предполагает наличие одного или нескольких (в зависимости от варианта задания) процессов-клиентов, принимающих запрос к реляционной базе данных, записанный на языке SQL. Клиентский процесс передает запрос процессу-серверу, который осуществляет поиск в базе данных в соответствии с запросом и передает результат поиска клиенту.
SQL-интерпретатор реализует некоторое подмножество предложений языка SQL, называемое далее модельным SQL. Описание модельного SQL приведено в разделе «Методические указания». Синтаксический анализ SQL-предложений рекомендуется проводить методом рекурсивного спуска [2]. Интерпретацию SQL-предложений можно реализовать с помощью объектно-ориентированной библиотеки классов для работы с файлами данных, описание которой приведено в разделе «БД и СУБД».
Интерфейс пользователя должен давать возможность вводить и редактировать SQLзапросы и просматривать результаты поиска в табличном виде. Конкретные детали интерфейса определяются преподавателем.
1.1. ВАРИАНТЫ ЗАДАНИЯ I. Архитектура «Клиент — Сервер».
1. Один клиент. Клиент и сервер на одной ЭВМ.
2. Один клиент. Клиент и сервер в сети ЭВМ.
3. Несколько клиентов. Клиенты и сервер на одной ЭВМ.
4. Несколько клиентов. Клиенты и сервер в сети ЭВМ.
II. Функции и способ взаимодействия клиента и сервера.
1. Клиент получает от пользователя запрос на модельном SQL и, не анализируя его, передает серверу. Сервер анализирует запрос и в случае его корректности выполняет запрос и передает клиенту ответ. Если же запрос некорректен, сервер передает клиенту код ошибки. Клиент выдает пользователю либо ответ на его запрос, либо сообщение об ошибке.
2. Клиент получает от пользователя запрос на SQL, анализирует его и в случае ошибки сообщает об этом пользователю, иначе передает серверу запрос в некотором внутреннем представлении. Сервер обращается к БД, определяет ответ на запрос и передает его клиенту. Клиент выдает пользователю ответ 1.2. ТРЕБОВАНИЯ К РЕАЛИЗАЦИИ Необходимо реализовать модельный SQL-интерпретатор в объектно-ориентированном стиле с использованием дополнительных возможностей языка реализации Си++ по сравнению, например, с Си:
— разработать архитектуру программы, — продумать и зафиксировать содержание и иерархию вводимых классов, — использовать встроенный в Си++ аппарат обработки исключительных ситуаций в программе, — при необходимости использовать стандартные библиотеки Си++.
1.3. СОДЕРЖАНИЕ ОТЧЕТА 1. Постановка задачи (конкретный вариант).
2. Описание способа взаимодействия процесса-клиента и процесса-сервера.
3. Описание архитектуры программы и основных классов.
4. Описание интерфейса пользователя с процессом-клиентом, перечень типов обнаруживаемых ошибок и выдаваемых диагностических сообщений.
2. МЕТОДИЧЕСКИЕ УКАЗАНИЯ
2.1. Моделирование архитектуры Межпроцессное взаимодействие можно организовать, используя модель «Клиент — Сервер». В этой модели один процесс, называемый сервером, отвечает за обработку запросов, получаемых им от других процессов — клиентов. Таким образом, клиентсерверная архитектура тесно связана с механизмами межпроцессного взаимодействия.Традиционный механизм информационных каналов (pipe) не подходит для клиентсерверных приложений, так как каналы могут связывать только процессы, запущенные одним пользователем. Кроме того, клиентские и серверные процессы могут выполняться на разных компьютерах (и даже в разных операционных системах), что предъявляет особые требования к механизму межпроцессной связи. Таким требованиям удовлетворяет механизм сокетов, кратко описываемый ниже.
Обычно, когда вызывается программа сервер, она запрашивает у операционной системы сокет (socket) (средство для соединения, связи процессов). Когда сервер получает сокет, он связывает с ним некоторое стандартное имя, чтобы программыклиенты могли общаться с сервером через данный сокет по этому имени.
После присваивания имени сокету, сервер «слушает» на этом сокете требования связи (запросы) от процессов-клиентов. Когда запрос появляется, сервер может допустить или запретить связь клиента с сервером. Если связь допустима, ОС соединяет клиента с сервером, и сервер получает возможность получать сообщения от клиента и посылать ему ответы так же, как и при использовании механизма информационных каналов.
Модельный SQL-интерпретатор. Методическое пособие.
Клиент также запрашивает у ОС свой сокет для взаимодействия с другим процессом (сервером), а затем сообщает имя сокета, с которым он хотел бы связаться. ОС пытается найти сокет c заданным именем и, если находит его, посылает серверу, слушающему этот сокет, запрос связи. Если сервер допускает связь, ОС создает специальный сокет, соединяющий два процесса, и клиент получает возможность посылать и получать данные от сервера так же, как и при использовании механизма информационных каналов.
Далее описывается реализация механизма сокетов, используемая в ОС UNIX [3].
Средства межпроцессного взаимодействия для одной ЭВМ 2.1.1. Функция socket Эта функция используется для создания сокета.
int socket (int domain, int type, int protocol);
Первый параметр — домен — накладывает определенные ограничения на формат используемых процессом адресов и их интерпретацию. При работе с одной ЭВМ используется UNIX-домен, где адреса интерпретируются как имена файлов в UNIX. В этом случае в качестве первого параметра указывается константа AF_UNIX (AF — Address Family).
Второй параметр определяет тип канала связи с сокетом, который должен быть использован.
Существует несколько типов каналов связи с сокетом, доступных при межпроцессном взаимодействии в UNIX, но обычно используются следующие два:
SOCK_STREAM — при этом типе связи поступающим в канал байтам информации гарантируется «доставка» в порядке их поступления; пока непрерывный поток байтов не прекратится, никакие другие данные приниматься каналом не будут (аналогом такой связи является доставка письма с уведомлением о вручении);
SOCK_DGRAM — этот тип связи используется для посылки отдельных пакетов информации, называемых дейтаграммами (сообщениями); при этом не гарантируется, что пакеты будут доставлены на место назначения в порядке поступления, а в действительности не гарантируется, что они все вообще будут доставлены (пример такого типа связи — доставка незаказного письма через обычную почту).
Третий параметр позволяет программисту выбрать нужный протокол для канала связи.
Если этот параметр равен нулю, ОС выберет нужный протокол автоматически.
Функция socket возвращает целое положительное число — номер сокет-дескриптора (который можно использовать, например, в функциях read и write аналогично файловому дескриптору). Если же сокет по каким-либо причинам не был создан (например, очень много открытых файлов), возвращается 1, а в переменную errno записывается причина неудачи.
Константы, используемые в качестве аргументов при вызове socket, определены во включаемых файлах и.
2.1.2. Функция bind Эта функция используется сервером для присваивания сокету имени. До выполнения функции bind (т.е. присваивания какого-либо имени, вид которого зависит от адресного домена) сокет недоступен программам-клиентам.
int socket (int domain, int type, int protocol);
Первый параметр — сокет-дескриптор, который данная функция именует. Второй параметр — указатель на структуру имени сокета struct sockaddr. Тип этой структуры зависит от домена. Для UNIX-домена этот тип называется sockaddr_un, он определен во включаемом файле sys/un.h и выглядит таким образом:
struct sockaddr_un { В качестве первого элемента структуры, обозначающего класс адресов, мы будем использовать константу AF_UNIX, второй элемент — имя файла, который будет соответствовать используемому сокету.
Файл с именем, указанным в sun_path, действительно создается, поэтому после окончания работы с данным сокетом надо выполнить функцию unlink, в противном случае другие программы, которые попытаются использовать данное имя, получат сообщение об ошибке.
2.1.3. Функция listen Функция listen используется сервером, чтобы информировать ОС, что он ожидает («слушает») запросы связи на данном сокете. Без такой функции всякое требование связи с этим сокетом будет отвергнуто.
int listen(int s, int backlog);
Первый аргумент — сокет для прослушивания, второй аргумент (backlog) — целое положительное число, определяющее, сколько запросов связи может быть принято на сокет одновременно. В большинстве систем это значение должно быть не больше пяти.
Заметим, что это число не имеет отношения к числу соединений, которое может поддерживаться сервером. Аргумент backlog имеет отношение только к числу запросов на соединение, которые приходят одновременно. Число установленных соединений может превышать это число.
2.1.4. Функция accept Эта функция используется сервером для принятия связи на сокет. Сокет должен быть уже слушающим в момент вызова функции. Если сервер устанавливает связь с клиентом, то функция accept возвращает новый сокет-дескриптор, через который и происходит общение клиента с сервером. Пока устанавливается связь клиента с сервером, функция accept блокирует другие запросы связи с данным сервером, а после установления связи «прослушивание» запросов возобновляется.
int accept(int s, struct sockaddr * name, int * anamelen);
Первый аргумент функции — сокет-дескриптор для принятия связей от клиентов.
Второй аргумент — указатель на адрес клиента (структура sockaddr) для Модельный SQL-интерпретатор. Методическое пособие.
соответствующего домена. Третий аргумент — указатель на целое число, содержащее длину буфера для записи адреса клиента. Второй и третий аргументы заполняются соответствующими значениями в момент установления связи клиента с сервером и позволяют серверу точно определить, с каким именно клиентом он общается. Если заданная длина буфера для хранения адреса меньше необходимой, функция возвращает ошибку (1). Если же сервер не интересуется адресом клиента, в качестве второго и третьего аргументов можно задать NULL-указатели.
2.1.5. Функция connect Функция connect используется процессом-клиентом для установления связи с сервером.
int connect(int s, struct sockaddr * name, int namelen);
Первый аргумент — сокет-дескриптор клиента. Второй аргумент — указатель на адрес сервера (структура sockaddr) для соответствующего домена. Третий аргумент — целое число — длина структуры адреса.
Функция возвращает 0, если вызов успешный, и 1 иначе.
2.1.6. Функция send Функция служит для записи данных в сокет.
int send(int s, void * buf, int len, int flags);
Первый аргумент — сокет-дескриптор, в который записываются данные. Второй и третий аргументы — соответственно, адрес и длина буфера с записываемыми данными.
Четвертый параметр — это комбинация битовых флагов, управляющих режимами записи. Если аргумент flags равен нулю, то запись в сокет (и, соответственно, считывание) происходит в порядке поступления байтов. Если значение flags есть MSG_OOB, то записываемые данные передаются потребителю вне очереди.
Функция возвращает число записанных в сокет байтов (в нормальном случае должно быть равно значению параметра len) или 1 в случае ошибки. Отметим, что запись в сокет не означает, что данные приняты на другом конце соединения процессомпотребителем. Для этого процесс-потребитель должен выполнить функцию recv (см.
ниже). Таким образом, функции чтения и записи в сокет выполняются асинхронно.
2.1.7. Функция recv Функция служит для чтения данных из сокета.
int recv(int s, void * buf, int len, int flags);
Первый аргумент — сокет-дескриптор, из которого читаются данные. Второй и третий аргументы — соответственно, адрес и длина буфера для записи читаемых данных.
Четвертый параметр — это комбинация битовых флагов, управляющих режимами чтения. Если аргумент flags равен нулю, то считанные данные удаляются из сокета.
Если значение flags есть MSG_PEEK, то данные не удаляются и могут быть считаны последующим вызовом (или вызовами) recv.
Функция возвращает число считанных байтов или 1 в случае ошибки. Следует отметить, что нулевое значение не является ошибкой. Оно сигнализирует об отсутствии записанных в сокет процессом-поставщиком данных.
2.1.8. Функция shutdown Эта функция используется для немедленного закрытия всех или части связей на сокет.
int shutdown(int s, int how);
Первый аргумент функции — сокет-дескриптор, который должен быть закрыт. Второй аргумент — целое значение, указывающее, каким образом закрывается сокет, а именно:
— сокет закрывается для чтения;
— сокет закрывается для записи;
— сокет закрывается для чтения и для записи.
RDWR 2.1.9. Функция close Эта функция закрывает сокет и разрывает все соединения с этим сокетом. В отличие от функции shutdown функция close может дожидаться окончания всех операций с сокетом, обеспечивая «нормальное», а не аварийное закрытие соединений.
Аргумент функции — закрываемый сокет-дескриптор.
Ниже приводятся примеры программ, демонстрирующих использование описанных выше функций.
2.2. Пример-оболочка программы «Клиент»
#define ADDRESS “mysocket” // адрес для связи struct sockaddr_un sa;
// получаем свой сокет-дескриптор:
if ((s = socket (AF_UNIX, SOCK_STREAM, 0)) < 0){ perror (“client: socket”);
// создаем адрес, по которому будем связываться с сервером:
sa.sun_family = AF_UNIX;
strcpy (sa.sun_path, ADDRESS);
// пытаемся связаться с сервером:
len = sizeof ( sa.sun_family) + strlen ( sa.sun_path);
Модельный SQL-интерпретатор. Методическое пособие.
if ( connect ( s, (struct sockaddr *)&sa, len) < 0 ){ perror (“client: connect”);
/*--------------------------------------------- */ // читаем сообщения сервера, пишем серверу:
/*
/*
2.3. Пример-оболочка программы «Сервер»
#define ADDRESS “mysocket” // адрес для связи struct sockaddr_un sa, ca;
// получаем свой сокет-дескриптор:
if((d = socket (AF_UNIX, SOCK_STREAM, 0)) < 0) { // создаем адрес, c которым будут связываться клиенты sa.sun_family = AF_UNIX;
strcpy (sa.sun_path, ADDRESS);
// связываем адрес с сокетом;
// уничтожаем файл с именем ADDRESS, если он существует, // для того, чтобы вызов bind завершился успешно len = sizeof ( sa.sun_family) + strlen (sa.sun_path);
if ( bind ( d, (struct sockaddr *)&sa, len) < 0 ) { // связываемся с клиентом через неименованный сокет с дескриптором d1:
if (( d1 = accept ( d, (struct sockaddr *)&ca, &ca_len)) < 0 ) { /* ------------------------------------------ */ // читаем запросы клиента, пишем клиенту:
/*
/*
/*
Приведенные выше примеры написаны в традиционном процедурном стиле. Язык Си++ позволяет программировать как в таком стиле, так и в объектноориентированном стиле, используя мощное и выразительное понятие класса. Во втором случае программы получаются существенно более компактными и понятными.
Далее в качестве примеров приводятся оболочки программ для «Клиента» и «Сервера», основанных на объектно-ориентированном интерфейсе для работы с сокетами (одна из возможных реализаций интерфейса приводится в приложении).
2.4. Пример-оболочка программы «Клиент» в объектно-ориентированном стиле using namespace std;
using namespace ModelSQL;
const char * address = "mysocket" // имя сокета int main(int argc, char* argv[]) UnClientSocket sock( address );
// устанавливаем соединение sock.PutString("Hello from client!");
Модельный SQL-интерпретатор. Методическое пособие.
if ( bind ( d, (struct sockaddr *)&sin, sizeof (sin)) < 0 ){ // связываемся с клиентом через неименованный сокет с дескриптором d1;
// после установления связи адрес клиента содержится в структуре fromsin if((d1 = accept ( d, (struct sockaddr *)&fromsin, &fromlen)) < 0){ /* ----------------------------------------- */ // читаем сообщения клиента, пишем клиенту:
/*
/*
/*
Ниже приводятся примеры оболочек программ для «Клиента» и «Сервера» для сети ЭВМ, основанных на объектно-ориентированном интерфейсе для работы с сокетами (одна из возможных реализаций интерфейса приводится в приложении).
3.5. Пример-оболочка программы «Клиент» для сети ЭВМ в объектно-ориентированном #include "sock_wrap.h" //см. приложение using namespace std;
using namespace ModelSQL;
#define PORT_NUM 1234 // номер порта процесса-сервера // В этом примере клиент и сервер выполняются на одном компьютере, // но программа легко обобщается на случай разных компьютеров. Для // этого можно, например, использовать сетевое имя не собственного // компьютера, как ниже, а имя компьютера, на котором выполняется // процесс-сервер int main(int argc, char* argv[]) // запрос сетевого имени собственной ЭВМ if (gethostname(host, sizeof host) < 0) { // ошибка --- досрочно завершаем выполнение cerr AddText("Name",10)->AddLong("Age");
ITable * Table = ITable::Create ( Struct );
После формирования новой таблицы описание её структуры Struct агрегируется внутри экземпляра Table, поэтому вручную уничтожать Struct не нужно. Для открытия уже существующей таблицы необходимо указать только имя.
ITable * Table = ITable::Open ( "mytable" );
Операции чтения/изменения записей таблицы происходят с использованием скрытого курсора. В качестве «окна» значений текущей записи таблицы выступает набор полей, Модельный SQL-интерпретатор. Методическое пособие.
для доступа к которым служит метод ITable::GetField, возвращает указатель на экземпляр поля, реализующий интерфейс IField. Контроль за жизненным циклом полей записи осуществляет ITable (аналогично структуре таблицы), поэтому вручную уничтожать их не надо.
IField * Field1 = Table->GetField ( "Name" );
IField * Field2 = Table->GetField ( "Age" );
Для добавления новой записи служит метод Add, который добавит в таблицу новую запись с текущими значениями «окна».
Field1->Text () = "Sasha";
Field2->Long () = 19;
Table->Add ();
Для прохода по записям таблицы предназначена группа методов ReadFirst, ReadNext.
При каждом смещении скрытого курсора «окно» обновляется на актуальные значения из файла таблицы. Операции обновления/удаления (Update/Delete) работают с текущей записью, на которую указывает скрытый курсор.
4.1. Описание модельного языка SQL Язык SQL [5] — простой и достаточно мощный язык взаимодействия пользователя с реляционными базами данных, разработанный фирмой IBM. Все необходимые пользователю операции, совершаемые над реляционными базами данных, могут задаваться с помощью SQL-предложений.
Модельный язык SQL будет включать лишь шесть основных предложений стандарта языка SQL, возможности которых также будут существенно ограничены. Это:
SELECT — выбрать из БД данные, соответствующие запросу;
INSERT — вставить новую строку данных в таблицу;
UPDATE — обновить значения данных в существующей строке;
DELETE — удалить строку из таблицы;
CREATE — создать таблицу;
DROP — уничтожить таблицу.
Каждое предложение модельного SQL будет относиться только к одной таблице, имя которой в нем указано. Получив предложение, интерпретатор должен открыть указанную таблицу, выполнить предложение, запомнить результат и закрыть таблицу.
Результатом выполнения каждого предложения (кроме DROP) будет вообще говоря новая таблица, которая в некотором внутреннем представлении должна быть передана клиенту в качестве ответа на запрос. Если же по каким-либо причинам ответить на запрос не удалось, клиент должен получить сообщение о причине неудачи.
SELECT FROM
Получив SELECT-предложение, интерпретатор выбирает из таблицы перечисленные поля в тех строках, которые удовлетворяют WHERE-клаузе.* — обозначает все поля таблицы.
Результатом выполнения SELECT-предложения является новая, в общем случае меньшая по размерам (возможно пустая) таблица, состоящая из отобранных строк и столбцов (полей).