POCO — кроссплатформенная open-source библиотека на С++ под Boost Software License: ru.wikipedia.org/wiki/POCO.
POCO имеет в своем составе средства для создания веб-сервисов c RESTful API.
В данной статье рассмотрено создание такого сервиса на примере TODO.
Простейшее приложение TODO — это список задач с возможностью добавить новую или удалить выполненную задачи.
Задача реализована в CTodo. Каждой задаче в списке присваиваются уникальный идентификатор (id) и пользовательское описание (text).
Список задач реализован в CTodoList. Управление списком задач производится методами CRUD Для хранения списка задач используется std::map.
Http сервер бэкенда реализован в TodoServerApp. Данный класс содержит методы CRUD, а также список задач и мьютекс для синхронизации доступа к нему. Метод main вызывается из базового класса POCO ServerApplication.
При вызове метода main в классе TodoServerApp создается Http сервер с заданными параметрами (порт 8000 и т.д.). Также серверу передается фабрика обработки запросов TodoRequestHandlerFactory.
После инициализации сервера запускается бесконечный цикл.
В данном случае фабрика TodoRequestHandlerFactory имеет всего два обработчика: CFileHandler для отдачи статики и CTodoHandler собственно для самого REST API.
Обработчик REST API CTodoHandler определяет тип запроса (GET/POST/PUT/DELETE). Для методов PUT/DELETE вычисляется идентификатор по URI. В соответствии с типом запроса производятся необходимые действия с данными списка.
Поскольку данное приложение очень простое, метод PUT не используется. Выдача информации для GET по id конкретного задания также не реализовано.
Далее производится формирование ответа в выходной поток сервера. С этой целью для задачи CTodo и списка задач CTodoList перегружен оператор <<.
Независимо от типа запроса сервис всегда возвращает текущий список задач в формате JSON.
Фронтенд загружается из ./www/ в дирректории приложения.
Для фронтенда используется AngularJs. Код фронтенда практически полностью взят из статьи scotch.io/tutorials/creating-a-single-page-todo-app-with-node-and-angular
Вид в index.html состоит из заголовка со счетчиком заданий, списка заданий и формы добавления нового задания.
Контроллер js/app.js вызывает GET для списка при инициализации. Далее — POST при добавлении нового задания из формы, либо DELETE при клике на чекбокс задания.
Репозиторий проекта: github.com/spot62/PocoAngularTodo
POCO имеет в своем составе средства для создания веб-сервисов c RESTful API.
В данной статье рассмотрено создание такого сервиса на примере TODO.
Простейшее приложение TODO — это список задач с возможностью добавить новую или удалить выполненную задачи.
Задача реализована в CTodo. Каждой задаче в списке присваиваются уникальный идентификатор (id) и пользовательское описание (text).
Список задач реализован в CTodoList. Управление списком задач производится методами CRUD Для хранения списка задач используется std::map.
Http сервер бэкенда реализован в TodoServerApp. Данный класс содержит методы CRUD, а также список задач и мьютекс для синхронизации доступа к нему. Метод main вызывается из базового класса POCO ServerApplication.
TodoServerApp.h
#pragma once
#include <Poco/Mutex.h>
#include <Poco/Net/HTTPServerRequest.h>
#include <Poco/Net/HTTPRequestHandler.h>
#include <Poco/Net/HTTPServerResponse.h>
#include <Poco/Net/HTTPServer.h>
#include <Poco/Net/HTTPRequestHandlerFactory.h>
#include <Poco/Net/HTMLForm.h>
#include <Poco/Path.h>
#include <Poco/ScopedLock.h>
#include <Poco/StringTokenizer.h>
#include <Poco/URI.h>
#include <Poco/Util/ServerApplication.h>
using namespace Poco;
using namespace Poco::Net;
using namespace Poco::Util;
using namespace std;
/**
Todo
*/
class CTodo {
size_t id;
string text;
public:
CTodo(string text): text(text){
}
/* getters & setters */
size_t getId(){ return id; }
void setId(size_t id){ this->id = id; }
string getText(){ return text; }
};
/**
Список Todo
*/
class CTodoList {
size_t id;
map<size_t, CTodo> todos;
public:
CTodoList():id(0){}
/* CRUD */
void create(CTodo& todo){ todo.setId(++id); todos.insert(pair<size_t,CTodo>(id, todo)); };
map<size_t, CTodo>& readList(){ return todos; }
void del(size_t id){ todos.erase(id); };
};
/**
Сервер
*/
class TodoServerApp : public ServerApplication
{
public:
/* CRUD */
static void createTodo(CTodo& todo);
static CTodoList& readTodoList();
//static void updateTodo(size_t id, CTodo& todo);
static void deleteTodo(size_t id);
protected:
int main(const vector<string> &);
static Mutex todoLock;
static CTodoList todoList;
};
При вызове метода main в классе TodoServerApp создается Http сервер с заданными параметрами (порт 8000 и т.д.). Также серверу передается фабрика обработки запросов TodoRequestHandlerFactory.
После инициализации сервера запускается бесконечный цикл.
В данном случае фабрика TodoRequestHandlerFactory имеет всего два обработчика: CFileHandler для отдачи статики и CTodoHandler собственно для самого REST API.
Обработчик REST API CTodoHandler определяет тип запроса (GET/POST/PUT/DELETE). Для методов PUT/DELETE вычисляется идентификатор по URI. В соответствии с типом запроса производятся необходимые действия с данными списка.
Поскольку данное приложение очень простое, метод PUT не используется. Выдача информации для GET по id конкретного задания также не реализовано.
Далее производится формирование ответа в выходной поток сервера. С этой целью для задачи CTodo и списка задач CTodoList перегружен оператор <<.
TodoServerApp.cpp
#include <iostream>
#include <string>
#include "TodoServerApp.h"
Mutex TodoServerApp::todoLock;
CTodoList TodoServerApp::todoList;
ostream& operator<<(ostream& os, CTodo& todo)
{
os << "{ \"_id\": "<< todo.getId() << ", \"text\": \"" << todo.getText() << "\" }";
return os;
}
ostream& operator<<(ostream& os, CTodoList& todoList)
{
map<size_t, CTodo> todos = todoList.readList();
os << "[";
if(!todos.empty())
{
if(todos.size() == 1)
os << todos.begin()->second;
else
for ( map<size_t, CTodo>::iterator it = todos.begin();;)
{
os << it->second ;
if(++it != todos.end())
os << ',';
else
break;
}
}
os << "]\n";
return os;
}
class CTodoHandler : public HTTPRequestHandler
{
public:
void handleRequest(HTTPServerRequest &req, HTTPServerResponse &resp)
{
URI uri(req.getURI());
string method = req.getMethod();
cerr << "URI: " << uri.toString() << endl;
cerr << "Method: " << req.getMethod() << endl;
StringTokenizer tokenizer(uri.getPath(), "/", StringTokenizer::TOK_TRIM);
HTMLForm form(req,req.stream());
if(!method.compare("POST"))
{
cerr << "Create:" << form.get("text") << endl;
CTodo todo(form.get("text"));
TodoServerApp::createTodo(todo);
}
else if(!method.compare("PUT"))
{
cerr << "Update id:" << *(--tokenizer.end()) << endl;
cerr << "Update text:" << form.get("text") << endl;
//size_t id=stoull(*(--tokenizer.end()));
//TodoServerApp::updateTodo(id, form.get("text"));
}
else if(!method.compare("DELETE"))
{
cerr << "Delete id:" << *(--tokenizer.end()) << endl;
size_t id=stoull(*(--tokenizer.end()));
TodoServerApp::deleteTodo(id);
}
resp.setStatus(HTTPResponse::HTTP_OK);
resp.setContentType("application/json");
ostream& out = resp.send();
cerr << TodoServerApp::readTodoList() << endl;
out << TodoServerApp::readTodoList() << endl;
out.flush();
}
};
#include <iostream> // std::cout
#include <fstream> // std::ifstream
#include <map> // std::ifstream
class CFileHandler : public HTTPRequestHandler
{
typedef std::map<const std::string, const std::string> TStrStrMap;
TStrStrMap CONTENT_TYPE = {
#include "MimeTypes.h"
};
string getPath(string& path){
if(path == "/"){
path="/index.html";
}
path.insert(0, "./www");
return path;
}
string getContentType(string& path){
string contentType("text/plain");
Poco::Path p(path);
TStrStrMap::const_iterator i=CONTENT_TYPE.find(p.getExtension());
if (i != CONTENT_TYPE.end())
{ /* Found, i->first is f, i->second is ++-- */
contentType = i->second;
}
if(contentType.find("text/") != std::string::npos)
{
contentType+="; charset=utf-8";
}
cerr << path << " : " << contentType << endl;
return contentType;
}
public:
void handleRequest(HTTPServerRequest &req, HTTPServerResponse &resp)
{
cerr << "Get static page: ";
//system("echo -n '1. Current Directory is '; pwd");
URI uri(req.getURI());
string path(uri.getPath());
ifstream ifs (getPath(path).c_str(), ifstream::in);
if(ifs)
{
resp.setStatus(HTTPResponse::HTTP_OK);
resp.setContentType(getContentType(path));
ostream& out = resp.send();
char c = ifs.get();
while (ifs.good()) {
out << c;
c = ifs.get();
}
out.flush();
}
else
{
resp.setStatus(HTTPResponse::HTTP_NOT_FOUND);
ostream& out = resp.send();
out << "File not found" << endl;
out.flush();
}
ifs.close();
}
};
class TodoRequestHandlerFactory : public HTTPRequestHandlerFactory
{
public:
virtual HTTPRequestHandler* createRequestHandler(const HTTPServerRequest & request)
{
if (!request.getURI().find("/api/"))
return new CTodoHandler;
else
return new CFileHandler;
}
};
void TodoServerApp::createTodo(CTodo& todo)
{
ScopedLock<Mutex> lock(todoLock);
todoList.create(todo);
}
CTodoList& TodoServerApp::readTodoList()
{
ScopedLock<Mutex> lock(todoLock);
return todoList;
}
void TodoServerApp::deleteTodo(size_t id)
{
ScopedLock<Mutex> lock(todoLock);
todoList.del(id);
}
int TodoServerApp::main(const vector<string> &)
{
HTTPServerParams* pParams = new HTTPServerParams;
pParams->setMaxQueued(100);
pParams->setMaxThreads(16);
HTTPServer s(new TodoRequestHandlerFactory, ServerSocket(8000), pParams);
s.start();
cerr << "Server started" << endl;
waitForTerminationRequest(); // wait for CTRL-C or kill
cerr << "Shutting down..." << endl;
s.stop();
return Application::EXIT_OK;
}
Независимо от типа запроса сервис всегда возвращает текущий список задач в формате JSON.
Response.json
[
{
"_id": 1,
"text": "First"
},
{
"_id": 2,
"text": "Second"
}
]
Фронтенд загружается из ./www/ в дирректории приложения.
Для фронтенда используется AngularJs. Код фронтенда практически полностью взят из статьи scotch.io/tutorials/creating-a-single-page-todo-app-with-node-and-angular
Вид в index.html состоит из заголовка со счетчиком заданий, списка заданий и формы добавления нового задания.
Контроллер js/app.js вызывает GET для списка при инициализации. Далее — POST при добавлении нового задания из формы, либо DELETE при клике на чекбокс задания.
index.html
<!-- index.html -->
<!doctype html>
<!-- ASSIGN OUR ANGULAR MODULE -->
<html ng-app="pocoTodo">
<head>
<!-- META -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><!-- Optimize mobile viewport -->
<title>POCO/Angular Todo App</title>
<!-- SCROLLS -->
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"><!-- load bootstrap -->
<style>
html { overflow-y:scroll; }
body { padding-top:50px; }
#todo-list { margin-bottom:30px; }
</style>
<!-- SPELLS -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script><!-- load jquery -->
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script><!-- load angular -->
<script src="js/app.js"></script>
</head>
<!-- SET THE CONTROLLER AND GET ALL TODOS -->
<body ng-controller="mainController">
<div class="container">
<!-- HEADER AND TODO COUNT -->
<div class="jumbotron text-center">
<h2>POCO/Angular Todo App <span class="label label-info">{{ todos.length }}</span></h2>
</div>
<!-- TODO LIST -->
<div id="todo-list" class="row">
<div class="col-sm-4 col-sm-offset-4">
<!-- LOOP OVER THE TODOS IN $scope.todos -->
<div class="checkbox" ng-repeat="todo in todos">
<label>
<input type="checkbox" ng-click="deleteTodo(todo._id)"> {{ todo.text }}
</label>
</div>
</div>
</div>
<!-- FORM TO CREATE TODOS -->
<div id="todo-form" class="row">
<div class="col-sm-8 col-sm-offset-2 text-center">
<form>
<div class="form-group">
<!-- BIND THIS VALUE TO formData.text IN ANGULAR -->
<input type="text" class="form-control input-lg text-center" placeholder="I want to buy a puppy that will love me forever" ng-model="formData.text">
</div>
<!-- createToDo() WILL CREATE NEW TODOS -->
<button type="submit" class="btn btn-primary btn-lg" ng-click="createTodo()">Add</button>
</form>
</div>
</div>
</div>
</body>
</html>
js/app.js
// js/app.js
var pocoTodo = angular.module('pocoTodo', []);
function mainController($scope, $http) {
$scope.formData = {};
$http.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";
// when landing on the page, get all todos and show them
$http.get('/api/todos')
.success(function(data) {
$scope.todos = data;
console.log(data);
})
.error(function(data) {
console.log('Error: ' + data);
});
// when submitting the add form, send the text to the node API
$scope.createTodo = function() {
$http.post('/api/todos', $.param($scope.formData)) // $scope.formData)
.success(function(data) {
$scope.formData = {}; // clear the form so our user is ready to enter another
$scope.todos = data;
console.log(data);
})
.error(function(data) {
console.log('Error: ' + data);
});
};
// delete a todo after checking it
$scope.deleteTodo = function(id) {
$http.delete('/api/todos/' + id)
.success(function(data) {
$scope.todos = data;
console.log(data);
})
.error(function(data) {
console.log('Error: ' + data);
});
};
}
Репозиторий проекта: github.com/spot62/PocoAngularTodo