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

ФИЗИКО-МАТЕМАТИЧЕСКИЕ НАУКИ

УДК 519.687.4

Н.А. Грамотеев, А.А. Медведев Курганский государственный университет

СОЗДАНИЕ СЕРВЕРА И КЛИЕНТСКОГО ПРИЛОЖЕНИЯ ДЛЯ АВТОМАТИЗИРОВАННОЙ СИСТЕМЫ КОНТРОЛЯ ЗНАНИЙ

Аннотация. В статье описывается процесс создания сервера и клиентского приложения, предназначенных для использования в составе автоматизированной системы контроля знаний обучаемых.

Ключевые слова: сервер, клиент, система контроля знаний.

N.A. Gramoteev, A.A. Medvedev Kurgan State University

THE CREATION OF A SERVER AND CLIENT APPLICATION FOR THE AUTOMATED SYSTEM OF KNOWLEDGE CONTROL

Annotation. The article describes the process of creating server and client applications designed for use in the automated system of control of knowledge of trainees.

Keywords: the server, client, control system knowledge.

ВВЕДЕНИЕ

В процессе создания сетевых систем контроля знаний рано или поздно возникает проблема разработки двух важных инструментов, первый из которых отвечал бы за рассылку заданий обучаемым, а второй - реализовывал бы отсылку полученных ответов. В данной статье мы подробно рассмотрим алгоритм создания таких приложений.

Понятно, что в процессе реализации такой связи между приложениями нужно определиться с сетевыми протоколами. Остановимся на использовании сте-

ка сетевых протоколов TCP/IP, на них построено всё взаимодействие пользователя (клиент) и сервера в IP-сетях (интернет, локальная сеть). Уровни стека TCP/ IP в общем случае можно разделить по следующим категориям:

• прикладной (Application layer) - протоколы HTTP, RTSP , FTP, служба DNS;

• транспортный (Transport layer) - протоколы TCP, UDP , SCTP , DCCP;

• сетевой (Internet layer) - стек TCP/IP либо IP-протокол;

• канальный (Link layer) - технологии Ethernet, IEEE 802.11 , Wireless Ethernet, SLIP, Token Ring, ATM, MPLS.

1 Начало работы

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

1) написать минимум кода для запуска сервера и клиента.

2) организовать целостный обмен данными между клиентом и сервером.

3) протестировать, выявить всевозможные уязвимости и устранить их.

4) организовать работу БД с сервером.

5) создать интерфейс клиента с соответствующими запросами на сервер и создать интерфейс на сервере для корректного ответа клиенту.

6) протестировать созданное web-приложение.

2 Создание сервера и клиентского приложения

Опишем процесс создания указанных приложений,

иллюстрируя его фрагментами кода.

Для создания сервера помимо стандартных библиотек будем использовать библиотеку ws2_32.lib, которая позволит нам организовать обмен данными, используя механизм сокетов:

#pragma comment

// заголовочный файл представляет собою интерфейс между

// приложением и транспортным протоколом

#include <WinSock2.h>;

Инициализируем библиотеку:

// структура для хранения информации

WSADATA data;

// инициализация библиотеки

if( WSAStartup (WINSOCK_VERSION, &data ) ) {

printf_s( "Error WSAStartup %d\n", WSAGetLastError() );

return -1; } // инициализация сокета

// если при создании произойдет ошибка, socket вернет -1

if( (mysocket = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP )) < 0 ) {

printf_s( "Error socket %d\n", WSAGetLastError() );

WSACleanup(); // отключить библиотеку return -1;}

//Инициализируем структуру конфигурации IP / PORT / Протокол соединения struct sockaddr_in local_addr; //указываем IP

local_addr.sin_addr.S_un.S_un_b.s_b1 = 192; local_a d d r. s i n_a d d r. S_u n . S_u n_b . s_b2 = 168;

local_addr.sin_addr.S_un.S_un_b.s_b3 = 0; local_addr.sin_addr.S_un.S_un_b.s_b4 = 14;

local_addr.sin_family = AF_INET; //- Протокол TCP/IP local_addr.sin_port = htons( PORT ); //далее настроим сервер на данную конфигурацию if (bind( mysocket, (struct sockaddr *)&local_addr, sizeof( local_addr ) ) ) { printf_s("Error bind %d\n", WSAGetLastError() ); closesocket(mysocket); // закрываем сокет WSACleanup(); return -1; }

//функция подготовит сокет к входящим подключениям //

if (listen (mysocket, 20)) {

printf( "Error listen %d\n", WSAGetLastError() ); closesocket( mysocket ); WSACleanup(); return -1; }

//теперь необходимо запустить «прослушку», //что бы быть всегда готовым обработать запрос клиента SOCKET client_socket = accept (mysocket, (struct sockaddr *)&client_addr, &client_addr_size); //его необходимо запускать в бесконечном цикле //теперь необходимо считывать приходящие данные от клиента while( (bytes_recv = recv (client_socket, buffi, 1024, 0 )) != SOCKET_ERROR ) { if (bytes_recv == 0) {

closesocket (client_socket);

continue; // если запрос пустой, то продолжаем слушать }

}

function getXmlHttp() { var xmlhttp;

try { xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {

try { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); }

catch (E) { xmlhttp = false; } }

if (!xmlhttp && typeof XMLHttpRequest!='undefined') { xmlhttp = new XMLHttpRequest(); }

return xmlhttp; }

Объект создан. Теперь необходимо сформировать сам запрос серверу и организовать ожидание («прослуш-ку») ответа:

var xmlhttp = getXmlHttp(); // Создаём объект XMLHTTP //открываем асинхронное соединение xmlhttp.open ('POST', 'http://192.168.0.14:22012/', true); //отправляем кодировку

xmlhttp.setRequestHeader

xmlhttp.send(encodeURIComponent('01:401/nИван/nИванов/nИванович));

xmlhttp.onreadystatechange = ^^ю^у/событие на ответ от сервера {

if (xmlhttp.readyState == 4) { //true, когда пришел ответ if(xmlhttp.status == 200) { //true - корректный ответ // ответ необходимо декодировать

var otvetServer=decodeURIComponent(xmlhttp.responseText); } }

}

Данный код отправит сообщение в POST-запросе и будет ожидать ответа, но раз запрос синхронный, то работа страницы не замораживается. Стоит заметить, что при отправке запроса с клиента, на клиенте запускается одноразовое событие ожидания («прослушки») ответа, вследствие чего его приходится инициализировать каждый раз при отправке запроса.

Посмотрим, что происходит на сервере (рисунок 1).

C^j ri g i n  l\De trjg'\Se rv er, еке

ccept:

kcept-Encoding: deflate

Accept-Language : ™-RU ,ru; <1=0.8, en-US ;ii=0.6,en;<j=O.4 !J0%:3Atest

Цамны!) IP уже б сети поток для него активен. Повторный вызов гаашел в 00: Тест:

роет / нтТР/1.1

Host: 192 -if,8.!D.l4:22012 ponneccion: keep-aliue Content-Length: 125

sHi-flHent: floz il la/5,0 <Windows NT 6.1; U0Wf,4> ЙщЛеЫеЬК 1^537.36 <KHTI1L, like ecko> Chrome/-47.0.2526.106 Saf ari/-537.36 fijjin: null

ontent-Tуре : text/plain ccept:

ccept-Kncoding: gaip, deflate

ccept -Language - fu-HU Fru;(j=0.il Fen-US ;д=0.6 ,en ;ij=0.4

Рисунок 1 - Полученный результат на сервере

Обратите внимание на нижние две строки: там располагаются символы «01» (так было передано в начале запроса), но остальной текст представлен в кодировке UTF-8. Получим его в более привычном виде.

Вначале текст сообщения преобразуем в тип wstring функцией URLDecode(buf), где buf содержит только текст запроса.

std::wstring URLDecode(std::string s) { //воспользуемся чтением из потока и //преобразуем каждая тройка символов char c;

std::string mbs, n; n.resize(2);

std::istringstream iss(s);

while (iss.get(c)) {

if (c == '%') { // разделитель int i;

iss >> n[0] >> n[1]; std::istringstream issn(n); issn >> std::hex >> i; c = static_cast<char>(i); } else if (c == '+') c = ' '; mbs += c; }

std::wstring_convert<std::codecvt_utf8<wchar_t>> cv; return cv.from_bytes(mbs);

};

Вызовем ее следующим образом: wstring bufRez = URLDecode (buffi);. Посмотрим, что написано в консоли (рисунок 2).

L_

Рисунок 2 - Содержимое консоли после преобразования

Приведем функцию ComeWebClients() класса OtpDanServClient, написанную нами для формирования ответа:

static void ComeWebClients(SOCKET socket2, char * srt1) {

std::stringstream response; //формирование HTTP запроса

std::stringstream response_body; //в данную переменную поместим ответ

response_body << srt1;

response << "HTTP/1.1 200 OK\r\n"

<< "Version: HTTP/1.1\r\n"

<< "Content-Length: " << response_body.str().length() //количество символов << "\r\n\r\n"

<< response_body.str(); //текст int result = (int)send(socket2, response.str().c_str(), response.str().length(), 0); if (result == SOCKET_ERROR) {

//произошла ошибка при отправлении данных

cerr << "Ошибка функция ComeWebClients: " << WSAGetLastError() << "\n"; }

}

Если обратить внимание на аргументы функции, а именно на указатель char * srtl, то становится понятным, что тип wstring не подойдет, если серверу нужно также отправлять данные запроса. В общем случае для обработки запросов используются функции, требующие в качестве аргументов значения типа char, следовательно, необходимо преобразовать текст к этому типу.

Для выполнения этой операции мы воспользовались функцией wstrtostr(), преобразующей значение типа wstring к типу string без потери данных. Вот ее текст: string wstrtostr(const std::wstring& s) { int len;

int slength = (int)s.length() + 1;

len = WideCharToMultiByte(CP_ACP, 0, s.c_str(), slength, 0, 0, 0, 0); std::string r(len, '\0');

WideCharToMultiByte(CP_ACP, 0, s.c_str(), slength, &r[0], len, 0, 0); return r; }

Обращение к этой функции имеет следующий вид: string s = wstrtostr (bufRez);

Подведем промежуточные итоги. На данный момент мы имеем работающий сервер и клиент, который может отправлять запросы и принимать ответы. 3 Использование СУБД MySQL

Далее был написан интерфейс клиента с запросами, на которые сервер дает соответствующие ответы. Обратите внимание, что в нашем примере (рисунок 1) регистрационные данные предварялись символами «01». Это уникальный идентификатор (ID) запроса, по нему определяется соответствующий ответ сервера. Извлечь его из сообщения можно следующей универсальной функцией prIDserver(): char * prIDserver(char* buff) {

static char IDenT[3]; //резервируем статическую область памяти memset(&IDenT, '\0', sizeof(IDenT)); //очищаем ее

char *ost = new char[1024]; // задаем размер для временного хранения данных memset(ost, '\0', 1024);

strtok_s(buff, ":", &ost); // помещаем ID в буфер memcpy_s(IDenT, 2, buff, 2); // копируем ID из буфера IDenT[2] = '\0';

memcpy_s(buff, 1024, ost, strlen(ost)); //копируем в буфер оставшееся сообщение

buff[strlen(ost)] = '\0';

return IDenT; // возвращаем ID };

Вызов данной функции может быть следующим:

char *IDenT = IDenT = prIDserver( &buff[0] ); .

При регистрации пользователя его данные должны где-то храниться. Для этого мы используем СУБД MySQL. Структуру таблиц мы приводить не будем, а остановимся на вопросе доступа к базе данных (БД) из среды С++.

Для организации доступа к такой БД нужно взять dll-библиотеку libmysql.dll из библиотек сервера MySQL и подключить ее к проекту стандартным способом: #pragma comment(lib,"libmysql.lib"), также включить в проект заголовочный файл из стандартного набора: #include <mysql.h>. В общем случае алгоритм работы с БД MySQL выглядит так: //создадим переменные для работы MYSQL *connection, mysql; MYSQL_RES *result; MYSQL_ROW row; //инициализируем MySQL объект mysql_init( &mysql );

//подключаемся к БД

mysql_real_connect( &mysql, "localhost", "root", "1234", "clients", 3306, 0, 0 );

//кодировку можно не указывать, она стандартно выставлена при установке MySQL,

//но при необходимости это можно сделать так:

mysql_query( &mysql, "set names 'cp1251';" );

mysql_set_character_set( &mysql, "cp1251" );

mysql_query( &mysql, "SET character_set_client='cp1251'" );

mysql_query( &mysql, "SET character_set_connection='cp1251'" );

mysql_query( &mysql, "SET character_set_results='cp1251'" );

//перед отправкой запроса необходимо организовать связь

mysql_store_result (&mysql);

//теперь можно отправить сам запрос

query_state = mysql_query (connection, "select * from t401_2015");

//проверить, успешен ли запрос?

query_state = mysql_query (connection, zapros);

//получить данные, которые пришли

result = mysql_store_result (&mysql);

//использование полученных данных

//по завершению работы с БД необходимо освободить выделенную ранее память //и разорвать соединение с БД mysql_free_result( result ); mysql_close( connection );

Вот, пожалуй, и весь набор используемых команд на сервере.

В заключение рассмотрим вопрос загруженности сервера. Если несколько десятков обучаемых (клиентов) нажмут кнопку регистрации практически одновременно, то образуется очередь, вследствие чего сервер может работать достаточно медленно. Как же решить данную проблему?

В качестве решения мы предлагаем создать поток на каждого клиента для обработки запросов. Вот пример создания такого потока: DWORD thID; HANDLE hThread;

hThread = CreateThread (NULL, NULL, ClientThread, &client_socket, NULL, &thID);

Функция-обработчик для потока:

DWORD WINAPI ClientThread (LPVOID client_socket) {

//перепишем сокет в локальную область

SOCKET my_sock = *((SOCKET*)client_socket);

//тут располагается исполняемый код

closesocket( my_sock );// закроем сокет после завершения обработки запроса return 0; }

Таким образом, основную работу выполняет другой (вспомогательный) поток, а основной поток, принимающий запросы, существенно разгружен. 4 Применение XML-файлов

В своей работе мы широко использовалиXML-файлы, которые служат для хранения конфигурации сервера, вариантов заданий, ответов пользователей. Для работы с ними был создан отдельный класс. Ниже приведена структура самого класса, но из за большого объема класса приведем только его основные элементы. class XML_TEMPLATENIK { private:

struct CLIENTNIK {

xml_document _doc; //хранит всю создаваемую структуру файла. xml_node _task; //узел списка задач

int _task_count; //количество добавленных в документ задач

int_id_client; //идентификатор клиента

string _filename; //имя файла };

vector<CLIENTNIK*> ^^^^/массив документов клиентов

//вывод сообщений в консоль

void _printLog (const char *message, ...);

//проверка на уникальность идентификаторов bool _proverkaID(_In_ int id);

public:

XML_TEMPLATENIK(); ~XML_TEMPLATENIK();

//предварительное создание выделенного ID для клиента bool CreateClientId (_In_ int id);

//создание нового документа

//( id - идентификатор клиента, filename - имя файла) bool CreateDocument(_In_ int id, _In_ const char* filename); //добавляет задание к существующему документу

// (nomerZadanie - номер решенного задания, otvetFirst - ответ на первое задание, // zadanieFirst - текст первого задания, typeZadanie - тип первого задания) bool AddZadanie (_In_ int id, _In_ const int nomerZadanie, _In_ const char* otvetZadanie, _In_ const char* textZadanie, _In_ const char * typeZadanie); // функция получения текста задачи

//

char *_cdecl GetTextZadanie (_In_ const int id, _In_ const char * filename);

//получить значение атрибута

//

bool_cdecl GetAttrZadanie (_In_ const int id, _In_ const char * filename,

_In_ const char * attr, _Out_ char * out); // из какого файла , какой атрибут , куда записать

char *_cdecl GetTextConfig (_In_ const char * filename, _In_ const char * attr,

_In_ const char * attr_znah, _Out_ char * out);

//получить количество заданий в документе

int GetCountZadanie(_In_ const char * filename); };

При регистрации на каждого нового пользователя заводится уникальный ID, который передается при общении сервера и клиента:

bool XML_TEMPLATENIK::CreateClientId (_In_ int id) { // создали структуру для нового клиента CLIENTNIK *newClient = new CLIENTNIK; newClient->_id_client = id; newClient->_task_count = 0; newClient->_filename = " "; _client.push_back( newClient ); return true; }

После этого распознать пользователя будет достаточно легко по его идентификатору (ID), созданному при регистрации. Как, например, при создании документа с результатами ответа пользователя: bool XML_TEMPLATENIK::CreateDocument (_In_ int id, _In_ const char* filename) { if( _proverkaID( id ) == false ) // проверяется, существует ли ID {return false; } _client[id]->_filename = filename; //создаем узлы

xml_node n = _client[id]->_doc.append_child( "TASK_BASE" ); //главный узел

_client[id]->_task = n.append_child( "tasks" ); //дочерний узел

if( _client[id]->_doc.save_file( filename ) == false ) //сохраним файл save_file();

_printLog( "^Ошибка сохранения файла: %s, уникальный ID клиента :'%i'. Функция CreateDocument \n", filename, id ); else

_printLog( "\nСоздан файл: %s, уникальный ID клиента :'%i'. Функция CreateDocument \n", filename, id );

return true; } Получится файл со структурой (рисунок 3).

75rsion=''l. О ". : 5 <taaka>

z=d£Eir='Zl' typg= уравнений jive^'12'Решить первое ураЕнеше ЗлЛ2-||4^0-;Лазк_|]С|0>

<tasli_0i)l z;dsrii;="S2 ' ъурч=''ураврчввд ог','е-='3''>-ешпь Еторое урфвеще 5я-2=2-:/;;азк_<Ш>

zida^ir='S3'1 туре=,|¥раввдвие оъ7еь="4 ';-ешить ipeibe уравнение 5:c-2y*3=2-;/tisk_|]02>

t/taslH* </IASK_BA5E>

Рисунок 3 - Структура XML-файла с ответами

Выше упоминалось, что варианты заданий также хранятся в XML-файлах. Приведем пример такого файла (рисунок 4).

:?mL '.•еГ5П.СП="1.(Г?!-. П<ТМу_БМЕ>-

ichar lnt="i" Ясет.=,Ч" f'r i В -ctaakai-

<15±k_UПLl anfc= '31' -ire='10" г.yp ypвдща^ие 1 z^ir.t"";£= 'С: ^sefcVjr^ft png">Рш-1тъ первое ураенение 3xA2-4jr=0-i/taak_000>

-ita3i[_00L cit.="S2" r.i!re="5" z£ineT.;-:a="E :\set\yra:2 .png" ^Решить второе ¿/равнение 2=2 /ta s01 >

- - a £ ri CTi™"23" " it== 15 = Ev,.a :.-Lr.=" l. .\set\gra:l ,рпу">Режитъ третье травление 5x-2y*3=2<; = 002>

ii Ц

-i/TASi: 3ssi5-

Рисунок 4 - Структура XML-файла с заданиями

Все функции XML класса реализованы с помощью подключаемой библиотеки: pugixml, которая, на наш взгляд, имеет определенные плюсы по сравнению с другими библиотеками (например TinyXML):

1 DOM-подобный интерфейс библиотеки, достаточно понятный программистам, работающим с языком JavaScri pt .

2 Очень быстрый непроверяющий XML-парсер, который строит DOM-дерево из XML-файла.

3 Полная поддержка Unicode с вариантами интерфейса Unicode и автоматического преобразования кодирования.

5 Тестирование

Для тестирования приложения была реализована локальная сеть из трёх персональных компьютеров. Приведем протокол взаимодействия сервера и клиента (все задания и ответы на них взяты для примера).

Вначале необходимо запустить сам сервер с корректно заполненным файлом конфигурации. Если сервер запущен, появится примерно такое окно (рисунок 5).

C:\i rig i n al\Debug\Server,ei

л конунгурацни обнаружен, IP: 192.168.Ш.14. ORT: 22Ш12 ayUariants: C:N\ aySaweFile: C:\4 роисхадит запуск сервера ервер запцтен цспешно жнданне подключений

пуск предварительных настроек сервера

Рисунок 5 - Пример успешно запущенного сервера

После появления сообщения «Ожидание подключений» можно запускать клиент При успешном присоединении к серверу бордюр области регистрации (рисунок 6) будет зеленым и серым - противном случае.

Рисунок 6 - Окно регистрации

После регистрации появляется окно выбора варианта, далее пользователь попадает в основное окно клиента (рисунок 7). В левом блоке написаны номер задания и текст самого задания. Правый блок отведен под данные пользователя, ввод ответа и кнопки управления. В нижнем блоке указанно время, отведенное

на решение задания с датой тестирования. Также для ознакомления с инструкцией пользователя в правом верхнем углу присутствует меню справки с калькулятором. После завершения работы либо по истечении времени, отведенного на выполнение работы, выводятся результаты.

В настоящее время разработанные сервер и клиент находятся в стадии тестирования.

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

УДК 519.688

С.О. Козлов, А.А. Медведев

Курганский государственный университет

ИСПОЛЬЗОВАНИЕ ТЕХНОЛОГИИ CUDA ПРИ РАЗРАБОТКЕ ПРИЛОЖЕНИЙ ДЛЯ ПАРАЛЛЕЛЬНЫХ ВЫЧИСЛИТЕЛЬНЫХ УСТРОЙСТВ

Аннотация. В статье приводится краткое описание технологии CUDA и рассматриваются вопросы, связанные с эффективным применением этой технологии при разработке приложений для устройств, поддерживающих параллельные вычисления.

Ключевые слова: технология CUDA, программы, параллельные вычисления.

S.O. Kozlov, A.A. Medvedev Kurgan State University

THE USE OF CUDA TECHNOLOGY WHEN DEVELOPING APPLICATIONS FOR PARALLEL COMPUTING DEVICES

Annotation. The article provides a brief description of CUDA technology and discusses the issues associated with the effective use of this technology when developing applications for devices that support parallel computing.

Keywords: CUDA technology, program, parallel computing.

Рисунок 7 - Основное окно клиентского приложения