Цель:
Разработать сервис предоставляющий данные о погоде в городе Симферополе на момент запроса. В качестве источника данных о погоде используйте: http://openweathermap.org/. В состав сервиса входит: серверное приложение на языке С++ и клиентское приложение на языке Python.
Серверное приложение (далее Сервер) предназначенное для обслуживания клиентских приложений и минимизации количества запросов к сервису openweathermap.org. Сервер должен обеспечивать возможность получения данных в формате JSON и виде html виджета (для вставки виджета на страницу будет использоваться iframe).
Клиентское приложение должно иметь графический интерфейс отображающий сведения о погоде и возможность обновления данных по требованию пользователя.
Подробности указаны далее.
Общая схема http-запроса представлена на рисунке ниже:
Протокол: http, https, ftp и другие.
Хост: доменное имя или ip-адрес сервера. Обычно по одному ip-адресу находится несколько сайтов, в этом случае доменное имя используется сервером, чтобы выбрать правильного получателя запроса.
Сетевой порт: число от 0 до 65535. Своеобразное расширения IP-адреса, нужен, чтобы несколько приложений на компьютере одновременно могли работать с сетью. Запрос приходит на IP-адрес, а дальше операционная система отдаёт его тому приложению, которое слушает порт указанный в запросе. Несколько приложений не могут одновременно слушать один и тот же порт благодаря этому пакет Skype не попадёт к Discord и т.д. Обычно веб сервера слушают 80 и 443 порты. 80 - стандартный порт для http запросов, 443 - для https. Браузеры автоматически дописывают номера портов ориентируясь на название протокола, поэтому пользователю это делать не нужно.
Путь к ресурсу: просто строка. Приложение получившее запрос само решает как реагировать на эту строку. В простом случае путь к ресурсу может восприниматься как реальный путь к папке/файлу, где-то на серверном компьютере. В общем случае путь к ресурсу - это способ запустить какую-то функцию в приложении получившем запрос. В результате, функция может как сгенерировать html-страничку и отправить в ответ на запрос, так и запустить процесс форматирования дисков на сервере, если у программы достаточно прав.
query string: набор пар ключ-значение. Идёт после вопросительного знака, пары разделены одним амперсантом (&). Если путь к ресурсу воспринимать как функцию, то query string - это аргументы функции. Порядок пар значения не имеет, лишние пары обычно игнорируются.
Якорь: служит, для того, чтобы промотать у клиента страницу к нужному элементу (заголовку, картинке и т.д.), поэтому данный параметр не попадает в запрос к серверу. В качестве якоря, например можно указать атрибут id html-тега.
Примерно тоже, что и выше, но в видео формате: https://youtu.be/DXB-GJKKNcg и https://youtu.be/N5OKXZvIR1w.
Перейдите на главную страницу сервиса http://openweathermap.org;
Зарегистрируйтесь и войдите в сервис;
В разделе меню API keys сгенерируйте API key;
Изучите пример использования API key http://openweathermap.org/appid#use;
Составьте и протестируйте запрос в браузере (для этого понадобится ваш API Key). В ответ вы должны получить текст в формате JSON:
В разделе openweathermap.org -> API -> One Call API изучите:
Составьте запрос на получение прогноза погоды для Симферополя с почасовым интервалом, в градусах Цельсия, на русском языке;
Протестируйте запрос в браузере. Ответ удобнее изучать в виде дерева, например при помощи JSON Viewer или браузерного расширения, например: JsonDiscovery.
Сервис позволяет получить время в формате json, без регистрации. Составьте запрос для получения времени в Симферополе и изучите формат ответа: http://worldtimeapi.org/pages/schema.
Для работы с сетью используйте библиотеку: https://github.com/yhirose/cpp-httplib.
Клонируйте или скачайте в виде архива репозиторий библиотеки https://github.com/yhirose/cpp-httplib;
Создайте консольный C++ проект;
Перейдите в папку с главным .cpp
файлом проекта и создайте папку include
. В ней будут размещаться заголовочные файлы библиотек;
В папке include
создайте папку cpp_httplib
и скопируйте туда файл httplib.h
из скаченного ранее репозитория. В дальнейшем сам репозиторий больше не потребуется, его можно удалить;
Перейдите в свойства проекта, в раздел Свойства конфигурации 🠖 C/C++ 🠖 Общие. Справа в разделе Дополнительные каталоги включаемых файлов добавьте путь к каталогу include
;
Скопируйте и вставьте в главный .cpp
файл следующий код:
using namespace httplib;
// В этой функции формируем ответ сервера на запрос
void gen_response(const Request& req, Response& res) {
// Команда set_content задаёт ответ сервера и тип ответа:
// Hello, World! - тело ответа
// text/plain - MIME тип ответа (в данном случае обычный текст)
res.set_content("Hello, World!", "text/plain");
}
int main(){
Server svr; // Создаём сервер (пока-что не запущен)
svr.Get("/", gen_response); // Вызвать функцию gen_response если кто-то обратиться к корню "сайта"
std::cout << "Start server... OK\n";
svr.listen("localhost", 1234); // Запускаем сервер на localhost и порту 1234
}
Запустите программу и перейдите по ссылке: http://localhost:1234/. Если вы увидели сообщение: "Hello, World!"
, значит всё сделано правильно.
Это базовый проект сервера, в дальнейшем его нужно будет модифицировать.
Для получения данных от сервисов http://openweathermap.org/ и http://worldtimeapi.org нужно будет посылать им get-запросы. Для этих целей воспользуемся скачанной ранее библиотекой cpp-httplib
.
Создайте консольный C++ проект и настройте его аналогичным предыдущему проекту способом.
Скопируйте и вставьте в главный .cpp
файл следующий код:
xxxxxxxxxx
using namespace httplib;
int main(){
// Создаём клиент и привязываем к домену. Туда пойдут наши запросы
Client cli("http://worldtimeapi.org");
// Отправляем get-запрос и ждём ответ, который сохраняется в переменной res
auto res = cli.Get("/api/timezone/Europe/Simferopol");
// res преобразуется в true, если запрос-ответ прошли без ошибок
if (res) {
// Проверяем статус ответа, т.к. может быть 404 и другие
if (res->status == 200) {
// В res->body лежит string с ответом сервера
std::cout << res->body << std::endl;
}else{
std::cout << "Status code: " << res->status << std::endl;
}
}
else {
auto err = res.error();
std::cout << "Error code: " << err << std::endl;
}
}
Если вы всё сделали правильно, увидите ответ от worldtimeapi.org в виде JSON.
В этом примере мы отправляем get-запрос по адресу http://worldtimeapi.org/api/timezone/Europe/Simferopol. Для этого выполняем три действия:
Создаём переменную с именем cli типа Client и указываем сайт куда будем отправлять запросы. Это обычная переменная и создавать их можно сколько нужно и где нужно.
Нужно указывать только название сайта и только протокол http. Для работы с https придётся выполнить дополнительные настройки.
res->body
. Это обычное значение типа std::string
. Попробуйте отправить запрос к сервису openweathermap.org который вы составили в пункте I.7
Данный проект в дальнейшем не потребуется, он нужен был только, чтобы попрактиковаться отправлять запросы.
Для работы с JSON используйте библиотеку: https://github.com/nlohmann/json.
Скачайте файл json.hpp
отсюда: https://github.com/nlohmann/json/releases. Его также можно найти в репозитории проекта https://github.com/nlohmann/json в папке: single_include/nlohmann
.
Создайте консольный C++ проект;
Перейдите в папку с главным .cpp
файлом проекта и создайте папку include
. В ней будут размещаться заголовочные файлы библиотек;
В папке include
создайте папку nlohmann
и скопируйте туда файл json.hpp
.
Перейдите в свойства проекта, в раздел Свойства конфигурации 🠖 C/C++ 🠖 Общие. Справа в разделе Дополнительные каталоги включаемых файлов добавьте путь к каталогу include
;
Скопируйте и вставьте в главный .cpp
файл следующий код:
xxxxxxxxxx
using json = nlohmann::json;
using namespace std;
int main()
{
// Какой-то JSON в виде строки
string str =
R"({
"pi": 3.141,
"happy": true,
"name": "Niels",
"nothing": null,
"answer": {
"everything": 42
},
"list": [1, 0, 2],
"object": {
"currency": "USD",
"value": 42.99
}
})";
// Парсим строку и получаем объект JSON
json j = json::parse(str);
// Достаём значения
double pi = j["pi"]; cout << "pi " << pi << endl;
bool happy = j["happy"]; cout << "happy " << happy << endl;
string name = j["name"]; cout << "name " << name << endl;
double value = j["object"]["value"]; cout << "value " << value << endl;
cout << "every " << to_string(j["answer"]["everything"].get<int>()) << endl;
// Идём по массиву
for (int i = 0; i < j["list"].size(); i++)
cout << "list[" << i << "] = " << j["list"][i] << endl;
// Пустой JSON
json j2;
// Заполняем разными данными
j2["num"] = 1;
j2["array"] = json::array();
j2["array"].push_back(1);
j2["array"].push_back(2);
j2["object"] = json::object();
j2["object"].push_back({"PI", pi});
j2["object"].push_back({"exp", 2.71});
// Преобразуем в строку и выводим
std::cout << j2.dump(4);
}
В примере используется сырой (raw) строкой литерал для записи JSON в тексте программы. Всё, что написано после R"(
и перед )";
воспринимается не как часть кода, а как текст, то есть не нужно экранировать кавычки и все нажатия клавиши Enter будут записаны в строку как '\n'. Это НЕ является обязательным требованием, и сделано, чтобы не загромождать код лишними символами.
Если значение нужно достать и передать в функцию или выполнить с ним какое-то действие, то нужно использовать метод get<тип>()
с указанием типа данных, например как тут: j["answer"]["everything"].get<int>()
или j["name"].get<string>()
.
Изучите пример. Принимать и отправлять данные мы будем в виде строк.
Алгоритм работы серверного приложения (далее Сервера):
Сервер запускается и слушает get запросы приходящие на 3000 порт localhost;
На запрос Сервер должен ответить информацией о погоде на текущий момент. При этом считается, что данные указанные в прогнозе тоже точны и нет необходимости делать лишний запрос к openweathermap.org.
Если приходит запрос на "/", сервер формирует и отправляет html-виджет:
Выполняется запрос на сервис worldtimeapi.org для получения точного текущего времени. Для простоты используйте поле unixtime
;
Т.к. запрос на сервис openweathermap.org возвращает прогноз на ближайшие двое суток с интервалом в 1 час, то после запроса к сервису openweathermap.org будем сохранять ответ в переменную (далее Кэш).
Если запрос к openweathermap.org ещё не делали, то делаем и ответ сохраняем Кэш.
Если запрос к openweathermap.org уже был, то проверяем, что в Кэше есть информация для требуемого времени. Для простоты будем брать информацию о погоде, на начало следующего часа. То есть в Кэше, в массиве "hourly"
нужно найти наименьший элемент с меткой времени больше текущей, если просматривать элементы с конца.
Если такого элемента нет, значит Кэш устарел. Обновляем Кэш и выполняем пункт 4.
После того, как нужные данные найдены загружаем шаблон виджета;
В данных видеоуроках показано, как работать с файлами: загружать и сохранять. Чтобы загрузить весть текст шаблона в одну строковую переменную можно написать так:
xxxxxxxxxx
ifstream file("file_name"); // Файловая переменная
// Тут нужно ещё добавить проверку, что файл успешно открылся.
string str; // Буфер. Тут будет текст файла
getline(file, str, '\0'); // Читаем все пока не встретим символ '\0'
В текстовых файлах не должно быть символа '\0' или он может присутствовать в конце файла, поэтому getline
прочитает весь файл до конца.
Далее ищем в шаблоне следующие фрагменты и заменяем на конкретные данные:{hourly[i].weather[0].description}, {hourly[i].weather[0].icon}, {hourly[i].temp};
Температуру округлить до целых. Так должен выглядеть готовый виджет.
Могут пригодиться функции: find, replace, round, to_string.
Заполненный шаблон отправляется в ответ на запрос. Не забудьте поменять MIME на text/html
.
Если приходит запрос на "/raw", сервер формирует и отправляет json:
unixtime
;"hourly"
нужно найти наименьший элемент с меткой времени больше текущей, если просматривать элементы с конца.text/json
.Графический интерфейс реализуйте при помощи Tkinter стандартного модуля Python. Для выполнения лабораторной работы достаточно базового умения работы с Tkinter, а именно создание текстовых меток (Label), компоновки виджетов (pack или grid) и привязки событий мышки (bind).
Видео лекции объясняющее основы работы с Tkinter можно посмотреть тут.
Макет клиентского приложения представлен на рисунке ниже. Полное (попиксельное) совпадения не нужно, достаточно общего сходства.
Для работы с json используйте стандартный модуль json описание модуля на английском языке приведено в документации.
Про базовые способы работы с модулем json на руссом языке можно почитать тут: https://python-scripts.com/json.
Видео пример работы с модулем json: https://youtu.be/rIhygmw9HZM и https://youtu.be/Wt4WAcqEje8
Для работы с сетью используйте модуль requests. Прежде, чем начать работать с данным модулем его необходимо установить. Для этого откройте окно командной строки/терминала и наберите команду:
xxxxxxxxxx
pip install requests
Описание модуля на руссом языке приведено например тут.
Алгоритм работы Клиента:
Клиент запускается.
Клиент отправляет запрос на Сервер, например http://localhost:3000/raw. Адрес можно записать прямо в коде.
Клиент получает от Сервера данные в формате json и заполняет элементы интерфейса. Текстовые данные полученные от Сервера могут ошибочно выводится в неправильной кодировке (Latin-1 (ISO 8859-1)), чтобы поправить кодировку используйте:
xxxxxxxxxx
"тут корявый текст".encode('l1').decode()
Если пользователь щелкнет в любом месте окна правой кнопкой мыши, перейти на пункт 2.
Для защиты работы скачайте программу ngrok https://ngrok.com/. Даная программа позволит создать туннель к localhost, то есть из интернета можно будет получить доступ к Серверу запущенному на вашем компьютере, без необходимости иметь "белый" IP- адрес.
Скачайте программу и распакуйте в любое место;
Запустите. Откроется окно командной строки;
Введите команду:
xxxxxxxxxx
ngrok.exe http 3000
ngrok запустится и по ссылке указанной в Forwarding
ваш сервер будет доступен из интернета;
Если прописать в браузере http://127.0.0.1:4040 вы увидите статистику по запросам проходящим через туннель.
Видео пример использования (всё что касается регистрации можно пропустить): https://youtu.be/e4igm6JQiHw
Отчёт по лабораторной работе оформляется в соответствии с указанными в разделе Правила оценивания требованиями.
В отчёте создайте раздел (заголовок второго уровня) Постановка задачи и продублируйте туда соответствующий блок из этого документа.
Создайте раздел (заголовок второго уровня) Выполнение работы и текстом подробно опишите всё, что делали в процессе выполнения. В описании обязательно должны присутствовать:
В папке с лабораторной работой должно быть: